aexiaoliou vor 1 Jahr
Commit
cf1ab84c22
100 geänderte Dateien mit 7640 neuen und 0 gelöschten Zeilen
  1. 17 0
      api/.example.env
  2. 5 0
      api/.gitignore
  3. 21 0
      api/README.md
  4. 1 0
      api/app/.htaccess
  5. 22 0
      api/app/AppService.php
  6. 94 0
      api/app/BaseController.php
  7. 100 0
      api/app/ExceptionHandle.php
  8. 8 0
      api/app/Request.php
  9. 20 0
      api/app/admin/attr/Permission.php
  10. 2 0
      api/app/admin/common.php
  11. 129 0
      api/app/admin/controller/Admin.php
  12. 27 0
      api/app/admin/controller/Allocation.php
  13. 176 0
      api/app/admin/controller/Base.php
  14. 59 0
      api/app/admin/controller/BaseAuthorized.php
  15. 32 0
      api/app/admin/controller/Config.php
  16. 62 0
      api/app/admin/controller/Good.php
  17. 35 0
      api/app/admin/controller/GoodClass.php
  18. 28 0
      api/app/admin/controller/Index.php
  19. 40 0
      api/app/admin/controller/Io.php
  20. 16 0
      api/app/admin/controller/IoDetail.php
  21. 26 0
      api/app/admin/controller/Login.php
  22. 117 0
      api/app/admin/controller/Menu.php
  23. 78 0
      api/app/admin/controller/Message.php
  24. 33 0
      api/app/admin/controller/Repo.php
  25. 26 0
      api/app/admin/controller/Role.php
  26. 36 0
      api/app/admin/controller/Stock.php
  27. 20 0
      api/app/admin/controller/Test.php
  28. 17 0
      api/app/admin/controller/Upload.php
  29. 5 0
      api/app/admin/event.php
  30. 5 0
      api/app/admin/middleware.php
  31. 93 0
      api/app/admin/middleware/Auth.php
  32. 41 0
      api/app/admin/middleware/CheckPermissionAttr.php
  33. 145 0
      api/app/admin/middleware/Login.php
  34. 224 0
      api/app/common.php
  35. 52 0
      api/app/common/ErrorCode.php
  36. 2 0
      api/app/common/common.php
  37. 37 0
      api/app/common/controller/JwtAuthorizedTrait.php
  38. 120 0
      api/app/common/controller/JwtBaseController.php
  39. 5 0
      api/app/common/event.php
  40. 12 0
      api/app/common/exception/CatchException.php
  41. 5 0
      api/app/common/middleware.php
  42. 63 0
      api/app/common/middleware/AllowCrossDomain.php
  43. 40 0
      api/app/common/middleware/WriteLog.php
  44. 200 0
      api/app/common/model/Admin.php
  45. 66 0
      api/app/common/model/Allocation.php
  46. 37 0
      api/app/common/model/AllocationDetail.php
  47. 27 0
      api/app/common/model/Base.php
  48. 24 0
      api/app/common/model/Check.php
  49. 25 0
      api/app/common/model/CheckDetail.php
  50. 68 0
      api/app/common/model/Config.php
  51. 26 0
      api/app/common/model/Good.php
  52. 16 0
      api/app/common/model/GoodClass.php
  53. 84 0
      api/app/common/model/Io.php
  54. 94 0
      api/app/common/model/IoDetail.php
  55. 33 0
      api/app/common/model/Repo.php
  56. 38 0
      api/app/common/model/Role.php
  57. 30 0
      api/app/common/model/Stock.php
  58. 30 0
      api/app/common/model/Transit.php
  59. 26 0
      api/app/common/model/TransitDetail.php
  60. 8 0
      api/app/common/model/User.php
  61. 245 0
      api/app/common/service/AllocationService.php
  62. 15 0
      api/app/common/service/CheckService.php
  63. 52 0
      api/app/common/service/FileService.php
  64. 79 0
      api/app/common/service/GoodClassService.php
  65. 286 0
      api/app/common/service/GoodService.php
  66. 51 0
      api/app/common/service/IoDetailService.php
  67. 420 0
      api/app/common/service/IoService.php
  68. 75 0
      api/app/common/service/RepoService.php
  69. 264 0
      api/app/common/service/Service.php
  70. 164 0
      api/app/common/service/StockService.php
  71. 49 0
      api/app/common/util/AliSms.php
  72. 135 0
      api/app/common/util/ArithmeticCount.php
  73. 34 0
      api/app/common/util/Channel.php
  74. 94 0
      api/app/common/util/DonationCert.php
  75. 90 0
      api/app/common/util/Email.php
  76. 37 0
      api/app/common/util/Encryption.php
  77. 413 0
      api/app/common/util/ExcelHelper.php
  78. 66 0
      api/app/common/util/IdCard.php
  79. 128 0
      api/app/common/util/ImgCompress.php
  80. 35 0
      api/app/common/util/LogHelper.php
  81. 175 0
      api/app/common/util/MiniProgram.php
  82. 33 0
      api/app/common/util/OrderHelper.php
  83. 176 0
      api/app/common/util/PHPExcel.php
  84. 123 0
      api/app/common/util/PhpOffice.php
  85. 210 0
      api/app/common/util/PhpSpreadsheetExport.php
  86. 169 0
      api/app/common/util/PhpSpreadsheetExportV2.php
  87. 76 0
      api/app/common/util/PhpSpreadsheetImport.php
  88. 43 0
      api/app/common/util/Power.php
  89. 57 0
      api/app/common/util/QrCode.php
  90. 89 0
      api/app/common/util/Result.php
  91. 135 0
      api/app/common/util/Rsa.php
  92. 85 0
      api/app/common/util/Salary.php
  93. 59 0
      api/app/common/util/SingleObjectClass.php
  94. 64 0
      api/app/common/util/Tree.php
  95. 82 0
      api/app/common/util/TxySms.php
  96. 31 0
      api/app/common/util/Upload.php
  97. 130 0
      api/app/common/util/Util.php
  98. 212 0
      api/app/common/util/WhereBuilder.php
  99. 31 0
      api/app/common/util/WxTemplate.php
  100. 0 0
      api/app/common/util/YcApiHprose.php

+ 17 - 0
api/.example.env

@@ -0,0 +1,17 @@
+APP_DEBUG = true
+
+[APP]
+DEFAULT_TIMEZONE = Asia/Shanghai
+
+[DATABASE]
+TYPE = mysql
+HOSTNAME = qqyun.ycxxkj.com
+DATABASE = lechang_storage_dev
+USERNAME = yfb
+PASSWORD = yfb123#
+HOSTPORT = 38006
+CHARSET = utf8mb4
+DEBUG = true
+
+[LANG]
+default_lang = zh-cn

+ 5 - 0
api/.gitignore

@@ -0,0 +1,5 @@
+/.idea
+/.vscode
+/vendor
+*.log
+.env

+ 21 - 0
api/README.md

@@ -0,0 +1,21 @@
+## 运行
+
+### build 
+```bash
+cd ../docker
+docker build -t lechang-storage-backend:8.0-apache -f ./lechang-storage-backend.dockerfile .
+```
+
+### run
+```bash
+cd api
+composer install
+docker run -d --rm -it -v $PWD:/var/www/html -p 8880:80 lechang-storage-backend:8.0-apache
+
+# docker nginx-proxy env
+docker run -d --rm -it -v $PWD:/var/www/html -e VIRTUAL_HOST='~..*' \
+    -e VIRTUAL_PATH='/lechang-storage' \
+    -e VIRTUAL_DEST='/public' \
+    --name lechang-storage \
+    --net use-proxy lechang-storage-backend:8.0-apache
+```

+ 1 - 0
api/app/.htaccess

@@ -0,0 +1 @@
+deny from all

+ 22 - 0
api/app/AppService.php

@@ -0,0 +1,22 @@
+<?php
+declare (strict_types = 1);
+
+namespace app;
+
+use think\Service;
+
+/**
+ * 应用服务类
+ */
+class AppService extends Service
+{
+    public function register()
+    {
+        // 服务注册
+    }
+
+    public function boot()
+    {
+        // 服务启动
+    }
+}

+ 94 - 0
api/app/BaseController.php

@@ -0,0 +1,94 @@
+<?php
+declare (strict_types = 1);
+
+namespace app;
+
+use think\App;
+use think\exception\ValidateException;
+use think\Validate;
+
+/**
+ * 控制器基础类
+ */
+abstract class BaseController
+{
+    /**
+     * Request实例
+     * @var \think\Request
+     */
+    protected $request;
+
+    /**
+     * 应用实例
+     * @var \think\App
+     */
+    protected $app;
+
+    /**
+     * 是否批量验证
+     * @var bool
+     */
+    protected $batchValidate = false;
+
+    /**
+     * 控制器中间件
+     * @var array
+     */
+    protected $middleware = [];
+
+    /**
+     * 构造方法
+     * @access public
+     * @param  App  $app  应用对象
+     */
+    public function __construct(App $app)
+    {
+        $this->app     = $app;
+        $this->request = $this->app->request;
+
+        // 控制器初始化
+        $this->initialize();
+    }
+
+    // 初始化
+    protected function initialize()
+    {}
+
+    /**
+     * 验证数据
+     * @access protected
+     * @param  array        $data     数据
+     * @param  string|array $validate 验证器名或者验证规则数组
+     * @param  array        $message  提示信息
+     * @param  bool         $batch    是否批量验证
+     * @return array|string|true
+     * @throws ValidateException
+     */
+    protected function validate(array $data, $validate, array $message = [], bool $batch = false)
+    {
+        if (is_array($validate)) {
+            $v = new Validate();
+            $v->rule($validate);
+        } else {
+            if (strpos($validate, '.')) {
+                // 支持场景
+                [$validate, $scene] = explode('.', $validate);
+            }
+            $class = false !== strpos($validate, '\\') ? $validate : $this->app->parseClass('validate', $validate);
+            $v     = new $class();
+            if (!empty($scene)) {
+                $v->scene($scene);
+            }
+        }
+
+        $v->message($message);
+
+        // 是否批量验证
+        if ($batch || $this->batchValidate) {
+            $v->batch(true);
+        }
+
+        return $v->failException(true)->check($data);
+    }
+
+}

+ 100 - 0
api/app/ExceptionHandle.php

@@ -0,0 +1,100 @@
+<?php
+
+namespace app;
+
+use Throwable;
+use think\Response;
+use think\facade\Log;
+use app\common\util\Result;
+use think\exception\Handle;
+use Firebase\JWT\ExpiredException;
+use think\exception\HttpException;
+use Firebase\JWT\BeforeValidException;
+use think\exception\ValidateException;
+use app\common\exception\CatchException;
+use think\exception\HttpResponseException;
+use Firebase\JWT\SignatureInvalidException;
+use app\common\exception\ParentHttpException;
+use think\db\exception\DataNotFoundException;
+use think\db\exception\ModelNotFoundException;
+
+/**
+ * 应用异常处理类
+ */
+class ExceptionHandle extends Handle
+{
+    /**
+     * 不需要记录信息(日志)的异常类列表
+     * @var array
+     */
+    protected $ignoreReport = [
+        HttpException::class,
+        HttpResponseException::class,
+        ModelNotFoundException::class,
+        DataNotFoundException::class,
+        //ValidateException::class,
+    ];
+
+    /**
+     * 记录异常信息(包括日志或者其它方式记录)
+     *
+     * @access public
+     * @param Throwable $exception
+     * @return void
+     */
+    public function report(Throwable $exception): void
+    {
+        // 使用内置的方式记录异常日志
+        parent::report($exception);
+    }
+
+    /**
+     * Render an exception into an HTTP response.
+     *
+     * @access public
+     * @param \think\Request $request
+     * @param Throwable $e
+     * @return Response
+     */
+    public function render($request, Throwable $e): Response
+    {
+        $className = get_class($e);
+        if (!in_array($className, $this->ignoreReport)) {
+            Log::debug("尝试捕获异常[{$e->getCode()}]:" . get_class($e) . " | 提示消息:{$e->getMessage()}" . ($e->getPrevious() ? ' | 父错误:' . get_class($e->getPrevious()) : ''));
+        }
+
+        // 应该被捕获的异常
+        if ($e instanceof CatchException) {
+            // 常规错误
+            return Result::restf($e->getCode(), $e->getMessage());
+        }
+
+        // 参数验证错误
+        if ($e instanceof ValidateException) {
+            return Result::restf(777, "参数验证错误:\n " . $e->getMessage());
+        }
+
+        if ($e instanceof SignatureInvalidException) {
+            // provided JWT signature verification failed.
+            return Result::restf(601, '无效的Jwt签名');
+        } elseif ($e instanceof BeforeValidException) {
+            // provided JWT is trying to be used before "nbf" claim OR
+            // provided JWT is trying to be used before "iat" claim.
+            return Result::restf(602, 'Jwt在授权期之前');
+        } elseif ($e instanceof ExpiredException) {
+            // provided JWT is trying to be used after "exp" claim.
+            return Result::restf(603, 'Jwt授权已过期');
+        }
+
+        // 请求异常
+        if ($e instanceof HttpException && request()->isAjax()) {
+            return response($e->getMessage(), $e->getStatusCode());
+        }
+        if (!in_array($className, $this->ignoreReport)) {
+            Log::error('未捕获异常:' . $e->getTraceAsString());
+        }
+
+        // 其他错误交给系统处理
+        return parent::render($request, $e);
+    }
+}

+ 8 - 0
api/app/Request.php

@@ -0,0 +1,8 @@
+<?php
+namespace app;
+
+// 应用请求对象类
+class Request extends \think\Request
+{
+
+}

+ 20 - 0
api/app/admin/attr/Permission.php

@@ -0,0 +1,20 @@
+<?php
+
+namespace app\admin\attr;
+use Attribute;
+
+#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]
+class Permission
+{
+    public $value;
+
+    /**
+     * __construct
+     *
+     * @param string $value 需要的权限
+     */
+    public function __construct($value)
+    {
+        $this->value = $value;
+    }
+}

+ 2 - 0
api/app/admin/common.php

@@ -0,0 +1,2 @@
+<?php
+// 这是系统自动生成的公共文件

+ 129 - 0
api/app/admin/controller/Admin.php

@@ -0,0 +1,129 @@
+<?php
+
+namespace app\admin\controller;
+
+class Admin extends BaseAuthorized
+{
+    public function init()
+    {
+        $roleList = \app\common\model\Role::field("id,name,valid,remark")->order("name asc ,id desc")->select();
+        $result = [
+            "roleList" => $roleList,
+        ];
+        return $this->success($result);
+    }
+
+    public function list()
+    {
+        //第1段:校验输入
+        $param = request()->param();
+        $rules = [];
+        $this->autoValid($rules, $param);
+        $listRow = input("pageSize", 20);
+        $keyword = input("keyword", "");
+        //第2段:执行业务
+        $res = \app\common\model\Admin::getList($keyword, $listRow);
+        //第3段:格式化输出
+        if ($res["code"] != 0) {
+            $this->error($res['msg'], $res["code"]);
+        }
+        return $this->success($res["data"]);
+
+//        abort(200,'aaa');
+    }
+
+
+    public function add()
+    {
+        //第1段:校验输入
+        $param = request()->param();
+        $rules = [
+            'name|账号' => 'require',
+            'password|密码' => 'require',
+            'phone|手机号' => 'require',
+            'role_id|角色' => 'require',
+            'valid|状态' => 'require',
+        ];
+        $this->autoValid($rules, $param);
+        //第2段:执行业务
+        $res = \app\common\model\Admin::add($param["name"], $param["password"], $param["phone"], $param["role_id"], $param["valid"]);
+        //第3段:格式化输出
+        if ($res["code"] != 0) {
+            $this->error($res['msg'], $res["code"]);
+        }
+        return $this->success($res["data"], "新增成功");
+    }
+
+    public function edit()
+    {
+        //第1段:校验输入
+        $param = request()->param();
+        $rules = [
+            'id|id' => 'require',
+            'name|账号' => 'require',
+            'phone|手机号' => 'require',
+            'role_id|角色' => 'require',
+            'valid|状态' => 'require',
+        ];
+        $this->autoValid($rules, $param);
+        //第2段:执行业务
+        $res = \app\common\model\Admin::edit($param["id"], $param["phone"], $param["role_id"], $param["valid"]);
+        //第3段:格式化输出
+        if ($res["code"] != 0) {
+            $this->error($res['msg'], $res["code"]);
+        }
+        return $this->success($res["data"], "更新成功");
+    }
+
+    /**
+     * 删除
+     * @return void
+     */
+    public function delete()
+    {
+        //第1段:校验输入
+        $param = request()->param();
+        $rules = [
+            'ids|删除项' => 'require',
+        ];
+        $this->autoValid($rules, $param);
+        //第2段:执行业务
+        $res = \app\common\model\Admin::del($param["ids"]);
+//        Log::record("res:" . print_r($res, true), "debug");
+        //第3段:格式化输出
+        if ($res["code"] != 0) {
+            $this->error($res['msg'], $res["code"]);
+        }
+        return $this->success($res["data"]);
+    }
+
+    public function detail()
+    {
+        $param = request()->param();
+        $rules = [
+            'id|id' => 'require',
+        ];
+        $this->autoValid($rules, $param);
+        $admin = \app\common\model\Admin::with(['role'])->find($param["id"]);
+        if (!$admin) {
+            $this->error("记录未找到");
+        }
+        return $this->success($admin);
+    }
+
+    public function resetPwd()
+    {
+        $param = request()->param();
+        $rules = [
+            'id|id' => 'require',
+            'password|密码' => 'require',
+        ];
+        $this->autoValid($rules, $param);
+        $res = \app\common\model\Admin::resetPwd($param["id"], $param["password"]);
+        if ($res["code"] != 0) {
+            $this->error($res['msg'], $res["code"]);
+        }
+        return $this->success($res["data"],"重置成功");
+    }
+
+}

+ 27 - 0
api/app/admin/controller/Allocation.php

@@ -0,0 +1,27 @@
+<?php
+
+namespace app\admin\controller;
+use app\admin\attr\Permission;
+
+#[Permission('allocation')]
+class Allocation extends BaseAuthorized
+{
+    public function page()
+    {
+        return $this->AllocationService()->page();
+    }
+
+    public function info()
+    {
+        return $this->AllocationService()->info();
+    }
+    public function create()
+    {
+        return $this->AllocationService()->createTrans($this->admin);
+    }
+
+    public function revert()
+    {
+        return $this->AllocationService()->revertTrans($this->admin);
+    }
+}

+ 176 - 0
api/app/admin/controller/Base.php

@@ -0,0 +1,176 @@
+<?php
+
+
+namespace app\admin\controller;
+
+
+use think\App;
+use think\Response;
+use think\facade\Log;
+use app\BaseController;
+use app\common\ErrorCode;
+use app\common\model\Admin;
+use app\middleware\AutoResult;
+use app\common\middleware\WriteLog;
+use think\annotation\route\Middleware;
+use think\exception\ValidateException;
+use think\exception\HttpResponseException;
+
+#[Middleware([AutoResult::class, WriteLog::class])]
+class Base extends BaseController
+{
+    protected $middleware = [AutoResult::class, WriteLog::class];
+
+    protected $checkTokenOpen = false; //是否校验token
+    protected $checkApiSignOpen = false; //是否校验签名
+    public $admin; //管理员
+
+
+    public function __construct(App $app)
+    {
+        parent::__construct($app);
+        if ($this->checkApiSignOpen) {
+            $this->checkApiSign();
+        }
+        if ($this->checkTokenOpen) {
+            $this->checkToken();
+        }
+    }
+
+
+    /**
+     * 获取token
+     * @return array|mixed|string|null
+     */
+    protected function getToken()
+    {
+        $token = null;
+        if (!$token) {
+            //from header
+            $token = request()->header("token");
+        }
+        if (!$token) {
+            //from url
+            $token = input("token");
+        }
+        return $token;
+    }
+
+    /**
+     * 检测token
+     * token规则
+     * token由base64编码,解码后分为密文、主键、过期时间(时间戳)三部分,用竖线|隔开
+     */
+    public function checkToken()
+    {
+        $token = $this->getToken();
+        if (!$token) {
+            $this->error(ErrorCode::getError(ErrorCode::CODE_TOKEN_NONE), ErrorCode::CODE_TOKEN_NONE);
+        }
+        $tokerReal = base64_decode($token);
+        $tokenArr = explode("|", $tokerReal); //拆分token
+
+        if (count($tokenArr) != 3) {
+            $this->error(ErrorCode::getError(ErrorCode::CODE_TOKEN_FORMAT_ERR), ErrorCode::CODE_TOKEN_FORMAT_ERR);
+        }
+        //判断token有没有超时
+        if (time() > $tokenArr[2]) {
+            $this->error(ErrorCode::getError(ErrorCode::CODE_TOKEN_EXPIRE), ErrorCode::CODE_TOKEN_EXPIRE);
+        }
+        //以下部分根据自己的业务实现
+
+        //$field = "id,login_name,valid,last_login_time,login_count,token";
+        $user = \app\common\model\Admin::where("token", "=", $token)->find(); //找到token
+        if (!$user) {
+            $this->error(ErrorCode::getError(ErrorCode::CODE_TOKEN_ERR), ErrorCode::CODE_TOKEN_ERR);
+        }
+        $this->admin = $user;
+        bind(Admin::class, $this->admin);
+    }
+
+
+    /**
+     *
+     * 返回成功信息
+     * @param $data
+     * @param string $msg
+     */
+    public function success($data, $msg = "")
+    {
+        Log::record("response:" . mb_substr(json_encode($data, JSON_UNESCAPED_UNICODE), 0, 1000) . ",code:0", "debug");
+        return $data;
+    }
+
+    /**
+     *
+     * 简易错误提示
+     * @param $code
+     */
+    public function errorSimple($code)
+    {
+        $this->error(ErrorCode::getError($code), $code);
+    }
+
+    /**
+     *
+     * 返回失败信息
+     * @param $msg
+     * @param int $code
+     * @param array $data
+     */
+    public function error($msg, $code = 999, $data = [])
+    {
+        $res = returnFormat($code, $msg, $data);
+        Log::record("response:" . mb_substr(json_encode($res, JSON_UNESCAPED_UNICODE), 0, 1000) . ",code:" . $code, "debug");
+        throw new HttpResponseException(Response::create($res, "json"));
+    }
+
+    /**
+     * 自动校验
+     * @param $rules 规则
+     * @param $param 验证对象
+     */
+    public function autoValid($rules, $param)
+    {
+        try {
+            validate($rules)->check($param);
+        } catch (ValidateException $e) {
+            // 验证失败 输出错误信息
+            $this->error($e->getError());
+        }
+    }
+
+    /**
+     * 检查签名
+     */
+    public function checkApiSign()
+    {
+
+        $timestampLimit = 20;
+        $param = request()->param();
+
+        $this->autoValid([
+            "_timestamp" => "require",
+            "_sign" => "require",
+        ], $param);
+        if (!($param["_timestamp"] >= time() - $timestampLimit * 60 && $param["_timestamp"] <= time() + $timestampLimit * 60)) {
+            $this->error("时间戳不合法,请刷新");
+        }
+        $sign = $param["_sign"];
+        unset($param["_sign"]);
+        ksort($param);
+
+        $param['_timestamp'] = $param['_timestamp'];
+        $secret = config("common.api_sign_secret");
+        $signStr = stripslashes(json_encode($param, JSON_UNESCAPED_UNICODE) . $secret);
+
+        $sign2 = md5($signStr);
+
+        if ($sign !== $sign2) {
+            Log::record("签名错误:sign: $sign sign2: $sign2", "debug");
+            Log::record("sign2 签名key:" . $secret, "debug");
+            Log::record("sign2 签名字符串:" . $signStr, "debug");
+            $this->error("签名错误。" . $signStr);
+        }
+    }
+}

+ 59 - 0
api/app/admin/controller/BaseAuthorized.php

@@ -0,0 +1,59 @@
+<?php
+
+
+namespace app\admin\controller;
+
+use app\middleware\AutoResult;
+use app\common\service\IoService;
+use app\common\middleware\WriteLog;
+use app\common\service\GoodService;
+use app\common\service\RepoService;
+use app\common\service\StockService;
+use app\common\service\IoDetailService;
+use app\common\exception\CatchException;
+use app\common\service\GoodClassService;
+use app\common\service\AllocationService;
+use app\admin\middleware\CheckPermissionAttr;
+
+/**
+ * 需要登录 的基类
+ * Class AuthBase
+ * @package app\api\controller
+ */
+class BaseAuthorized extends Base
+{
+    protected $middleware = [CheckPermissionAttr::class, AutoResult::class, WriteLog::class];
+    
+    protected $checkTokenOpen = true;
+
+    public function IoService(): IoService
+    {
+        return (new IoService($this->app))->exceptionClass(CatchException::class);
+    }
+    public function IoDetailService(): IoDetailService
+    {
+        return (new IoDetailService($this->app))->exceptionClass(CatchException::class);
+    }
+    public function GoodClassService(): GoodClassService
+    {
+        return (new GoodClassService($this->app))->exceptionClass(CatchException::class);
+    }
+    public function GoodService(): GoodService
+    {
+        return (new GoodService($this->app))->exceptionClass(CatchException::class);
+    }
+    public function RepoService(): RepoService
+    {
+        return (new RepoService($this->app))->exceptionClass(CatchException::class);
+    }
+
+    public function StockService(): StockService
+    {
+        return (new StockService($this->app))->exceptionClass(CatchException::class);
+    }
+
+    public function AllocationService(): AllocationService
+    {
+        return (new AllocationService($this->app))->exceptionClass(CatchException::class);
+    }
+}

+ 32 - 0
api/app/admin/controller/Config.php

@@ -0,0 +1,32 @@
+<?php
+
+namespace app\admin\controller;
+
+use app\admin\attr\Permission;
+use app\common\model\Config as ConfigModel;
+use app\admin\controller\BaseAuthorized;
+
+#[Permission('config')]
+class Config extends BaseAuthorized
+{
+    public function update()
+    {
+        $code = input('code');
+        $name = input('name');
+        $content = input('content');
+
+        $config = ConfigModel::getConfig($code);
+
+        $config->code = $code;
+        $config->name = $name;
+        $config->content = $content;
+        $config->save();
+
+        return $config;
+    }
+
+    public function content($code)
+    {
+        return ConfigModel::getConfigContent($code);
+    }
+}

+ 62 - 0
api/app/admin/controller/Good.php

@@ -0,0 +1,62 @@
+<?php
+
+namespace app\admin\controller;
+
+use app\admin\attr\Permission;
+use app\common\util\Result;
+use think\annotation\route\Get;
+use think\annotation\route\Group;
+use app\common\model\Good as GoodModel;
+use app\admin\controller\BaseAuthorized;
+
+#[Permission('good')]
+#[Group('good')]
+class Good extends BaseAuthorized
+{
+    public function page()
+    {
+        return $this->GoodService()->page();
+    }
+
+    #[Get(rule: 'get/name')]
+    public function getByName($name)
+    {
+        return GoodModel::getByName($name) ?? Result::failed('不存在的商品');
+    }
+
+    #[Get(rule: 'get/no')]
+    public function getByNo($no)
+    {
+        return GoodModel::getByNo($no) ?? Result::failed('不存在的商品');
+    }
+
+    public function create()
+    {
+        return $this->GoodService()->create();
+    }
+
+    public function delete()
+    {
+        return $this->GoodService()->delete();
+    }
+
+    public function update()
+    {
+        return $this->GoodService()->update();
+    }
+
+    public function template()
+    {
+        return download(public_path('static/execl') . 'GOOD_TEMPLATE.xlsx', '物品导入模板.xlsx');
+    }
+
+    public function import()
+    {
+        return $this->GoodService()->import();
+    }
+
+    public function export()
+    {
+        return $this->GoodService()->export();
+    }
+}

+ 35 - 0
api/app/admin/controller/GoodClass.php

@@ -0,0 +1,35 @@
+<?php
+
+namespace app\admin\controller;
+
+use app\admin\attr\Permission;
+use app\admin\controller\BaseAuthorized;
+
+#[Permission('good_class')]
+class GoodClass extends BaseAuthorized
+{
+    public function page()
+    {
+        return $this->GoodClassService()->page();
+    }
+
+    public function all()
+    {
+        return $this->GoodClassService()->all();
+    }
+
+    public function create()
+    {
+        return $this->GoodClassService()->create();
+    }
+
+    public function update()
+    {
+        return $this->GoodClassService()->update();
+    }
+
+    public function delete()
+    {
+        return $this->GoodClassService()->delete();
+    }
+}

+ 28 - 0
api/app/admin/controller/Index.php

@@ -0,0 +1,28 @@
+<?php
+namespace app\admin\controller;
+
+use app\admin\attr\Permission;
+use app\common\model\Io;
+use app\common\model\Repo;
+use app\common\model\Stock;
+use app\common\model\Allocation;
+
+#[Permission('index')]
+class Index extends BaseAuthorized
+{
+    public function statistics()
+    {
+        $ioCount = Io::count();
+        $checkCount = Io::where('change_type', '=', Io::CHANGE_TYPE_CHECK)->count();
+        $allcationCount = Allocation::count();
+        $goodTotal = Stock::sum('num');
+        $repoCount = Repo::count();
+        return [
+            'ioCount' => $ioCount,
+            'checkCount' => $checkCount,
+            'allcationCount' => $allcationCount,
+            'goodTotal' => $goodTotal,
+            'repoCount' => $repoCount
+        ];
+    }
+}

+ 40 - 0
api/app/admin/controller/Io.php

@@ -0,0 +1,40 @@
+<?php
+
+namespace app\admin\controller;
+
+use app\admin\attr\Permission;
+use app\admin\controller\BaseAuthorized;
+
+#[Permission('io')]
+class Io extends BaseAuthorized
+{
+    public function page()
+    {
+        return $this->IoService()->page();
+    }
+
+    public function info()
+    {
+        return $this->IoService()->info();
+    }
+
+    public function create()
+    {
+        return $this->IoService()->createTrans(admin: $this->admin);
+    }
+
+    public function complete()
+    {
+        return $this->IoService()->completeTrans(admin: $this->admin);
+    }
+
+    public function revert()
+    {
+        return $this->IoService()->revertTrans(admin: $this->admin);
+    }
+
+    public function export()
+    {
+        return $this->IoService()->export();
+    }
+}

+ 16 - 0
api/app/admin/controller/IoDetail.php

@@ -0,0 +1,16 @@
+<?php
+
+namespace app\admin\controller;
+
+class IoDetail extends BaseAuthorized
+{
+    public function page()
+    {
+        return $this->IoDetailService()->page();
+    }
+
+    public function transits()
+    {
+        return $this->IoDetailService()->transits();
+    }
+}

+ 26 - 0
api/app/admin/controller/Login.php

@@ -0,0 +1,26 @@
+<?php
+
+namespace app\admin\controller;
+
+class Login extends Base
+{
+    public function doLogin(){
+        //第1段:校验输入
+        $param = request()->param();
+        $rules = [
+            "phone|用户名" => "require",
+            'password|密码' => 'require',
+        ];
+        $this->autoValid($rules, $param);
+        //第2段:执行业务
+        $res = \app\common\model\Admin::login($param["phone"], $param["password"]);
+        //第3段:格式化输出
+        if ($res["code"] != 0) {
+            $this->error($res['msg'], $res["code"]);
+        }
+        return $res["data"];
+    }
+
+
+
+}

+ 117 - 0
api/app/admin/controller/Menu.php

@@ -0,0 +1,117 @@
+<?php
+
+namespace app\admin\controller;
+
+use app\admin\middleware\Login;
+use app\Request;
+
+class Menu extends Base
+{
+    /**
+     * 中间件校验
+     * 1.权限
+     * @var array[]
+     */
+    protected $middleware = [
+        //except 除了某个方法不校验,其余校验
+        //only 仅仅校验某个方法
+        Login::class => ['except' => ['']],
+//        Auth::class => ['except' => ['']],
+    ];
+
+    /**
+     * 获取菜单
+     * @param Request $request
+     * @return void
+     */
+    public function list(Request $request)
+    {
+        $menuJson = file_get_contents(root_path() . "config/json/menu.json");
+        $menu = json_decode($menuJson, true);
+
+//        $admin = $request->admin;
+        $role_codes = [];
+//        if ($admin->role && $admin->role->codes) {
+//            $role_codes = explode(',', $admin->role->codes);
+//        }
+        $role_codes[] = 'home';
+        $role_codes[] = 'admin';
+        $fin_menu = $this->getFinMenu($menu, $role_codes,1);
+        $result = [
+            'menu' => $fin_menu,
+            'role' => $role_codes,
+        ];
+        return $this->success($result);
+    }
+
+
+    /**
+     * @param $menu_list
+     * @param $role_codes
+     * @param int $is_root
+     * @return array
+     */
+    public function getFinMenu($menu_list, $role_codes, int $is_root = 0): array
+    {
+
+        $fin_menu = [];
+        foreach ($menu_list as $key => $val) {
+
+            if (isset($val['children'])) {
+                $child_list = $this->getChild($val['children']);
+                $parents = false;
+                foreach ($child_list as $kes => $ves) {
+                    if (in_array($ves['role'], $role_codes) || $is_root) {
+                        $parents = true;
+                        break;
+                    }
+                }
+                if (in_array($val['role'], $role_codes) || $parents) {
+                    $item = $val;
+                    $item['children'] = $this->getFinMenu($val['children'], $role_codes, $is_root);
+                    $fin_menu[] = $item;
+                }
+            } else {
+                if (isset($val['role'])) {
+                    if (in_array($val['role'], $role_codes) || $is_root || $val['role'] == 'index') {
+                        $fin_menu[] = $val;
+                    }
+                }
+            }
+        }
+
+        return $fin_menu;
+    }
+
+    /**
+     * 获取所有子节点
+     * @param array $arrData
+     * @param string $strChild
+     * @return array|mixed
+     */
+    public function getChild(array $arrData = [], string $strChild = "children"): mixed
+    {
+        if (empty($arrData) || !is_array($arrData)) {
+            return $arrData;
+        }
+        $arrRes = [];
+        foreach ($arrData as $k => $v) {
+            $arrTmp = $v;
+            if (isset($arrTmp[$strChild])) {
+                unset($arrTmp[$strChild]);
+            }
+            $arrRes[] = $arrTmp;
+            if (isset($v[$strChild])) {
+                if (!empty($v[$strChild])) {
+                    $arrTmp = $this->getChild($v[$strChild]);
+                    $arrRes = array_merge($arrRes, $arrTmp);
+                }
+            }
+        }
+        return $arrRes;
+    }
+}
+
+
+
+

+ 78 - 0
api/app/admin/controller/Message.php

@@ -0,0 +1,78 @@
+<?php
+
+namespace app\admin\controller;
+
+use app\admin\middleware\Auth;
+use app\admin\middleware\Login;
+use app\Request;
+use app\common\model\Message as MessageModel;
+
+class Message extends Base
+{
+    /**
+     * 中间件校验
+     * 1.权限
+     * @var array[]
+     */
+    protected $middleware = [
+        //except 除了某个方法不校验,其余校验
+        //only 仅仅校验某个方法
+        Login::class => ['except' => ['']],
+        Auth::class => ['except' => ['']],
+    ];
+
+
+    /**
+     * 列表
+     * @param Request $request
+     * @return void
+     */
+    public function list(Request $request)
+    {
+        $param = $request->param();
+
+        return $this->success([]);
+    }
+
+    /**
+     * 更新阅读状态
+     * @param Request $request
+     * @return void
+     */
+    public function edit(Request $request)
+    {
+        $param = $request->param();
+        $this->autoValid(\app\admin\validate\Message::class, $param, $request->action());
+        $array['is_read'] = 1;
+        (new MessageModel)->where('id', 'in', $param['ids'])->save($array);
+        return $this->success();
+    }
+
+    /**
+     * 删除/批量删除
+     * @param Request $request
+     * @return void
+     */
+    public function del(Request $request)
+    {
+        $param = $request->param();
+        $this->autoValid(\app\admin\validate\Message::class, $param, $request->action());
+        $ids = explode(',', $param['ids']);
+        foreach ($ids as $item) {
+            $machine = (new MessageModel())->detail($item);
+            if (!$machine) {
+                $this->error('记录未找到或已删除');
+            }
+        }
+        $res = MessageModel::doDelStatic($param['ids']);
+
+        if (!$res) {
+            $this->error();
+        }
+        return $this->success();
+    }
+}
+
+
+
+

+ 33 - 0
api/app/admin/controller/Repo.php

@@ -0,0 +1,33 @@
+<?php
+
+namespace app\admin\controller;
+use app\admin\attr\Permission;
+
+#[Permission('repo')]
+class Repo extends BaseAuthorized
+{
+    public function page()
+    {
+        return $this->RepoService()->page();
+    }
+
+    public function all()
+    {
+        return $this->RepoService()->all();
+    }
+
+    public function create()
+    {
+        return $this->RepoService()->create();
+    }
+
+    public function update()
+    {
+        return $this->RepoService()->update();
+    }
+
+    public function delete()
+    {
+        return $this->RepoService()->delete();
+    }
+}

+ 26 - 0
api/app/admin/controller/Role.php

@@ -0,0 +1,26 @@
+<?php
+
+namespace app\admin\controller;
+use app\admin\attr\Permission;
+
+#[Permission('role')]
+class Role extends BaseAuthorized
+{
+    public function getList()
+    {
+        //第1段:校验输入
+        $param = request()->param();
+        $rules = [];
+        $this->autoValid($rules, $param);
+        $listRow = input("pageSize", 20);
+        $keyword = input("keyword", "");
+        //第2段:执行业务
+        $res = \app\common\model\Role::getList($keyword, $listRow);
+        //第3段:格式化输出
+        if ($res["code"] != 0) {
+            $this->error($res['msg'], $res["code"]);
+        }
+        return $res["data"];
+    }
+
+}

+ 36 - 0
api/app/admin/controller/Stock.php

@@ -0,0 +1,36 @@
+<?php
+
+namespace app\admin\controller;
+use app\admin\attr\Permission;
+use think\annotation\route\Get;
+use think\annotation\route\Group;
+
+#[Group('stock')]
+#[Permission('stock')]
+class Stock extends BaseAuthorized
+{
+    public function page()
+    {
+        return $this->StockService()->page();
+    }
+    public function allByRepo()
+    {
+        return $this->StockService()->allByRepo();
+    }
+
+    public function info()
+    {
+        return $this->StockService()->info();
+    }
+
+    #[Get('get/good-repo')]
+    public function getByGoodAndRepo()
+    {
+        return $this->StockService()->getByGoodAndRepo();
+    }
+
+    public function export()
+    {
+        return $this->StockService()->export();
+    }
+}

+ 20 - 0
api/app/admin/controller/Test.php

@@ -0,0 +1,20 @@
+<?php
+
+namespace app\admin\controller;
+
+use app\common\model\Admin;
+
+class Test extends Base
+{
+    public function index()
+    {
+        return $this->success([], "成功了");// returnFormat(0,"succ",[]);
+    }
+
+    public function page()
+    {
+        $list = Admin::where([])->paginate(10);
+        return $this->success($list);
+    }
+
+}

+ 17 - 0
api/app/admin/controller/Upload.php

@@ -0,0 +1,17 @@
+<?php
+
+namespace app\admin\controller;
+
+use app\common\exception\CatchException;
+use app\common\service\FileService;
+
+class Upload extends Base
+{
+    /**
+     * 图片上传
+     */
+    public function img()
+    {
+        return (new FileService($this->app))->exceptionClass(CatchException::class)->uploadImage('admin');
+    }
+}

+ 5 - 0
api/app/admin/event.php

@@ -0,0 +1,5 @@
+<?php
+// 这是系统自动生成的event定义文件
+return [
+
+];

+ 5 - 0
api/app/admin/middleware.php

@@ -0,0 +1,5 @@
+<?php
+// 这是系统自动生成的middleware定义文件
+return [
+    \app\common\middleware\AllowCrossDomain::class,
+];

+ 93 - 0
api/app/admin/middleware/Auth.php

@@ -0,0 +1,93 @@
+<?php
+declare (strict_types=1);
+
+namespace app\admin\middleware;
+
+use Closure;
+use app\Request;
+use think\exception\HttpResponseException;
+use think\facade\Db;
+use think\Response;
+
+/**
+ * 全局权限校验
+ * Class WriteLog
+ * @package app\middleware
+ */
+class Auth
+{
+    protected static int $CODE_SUCCESS = 0; //成功
+    protected static int $CODE_ERR = 999; //成功
+
+    /**
+     * @param Request $request
+     * @param Closure $next
+     * @return mixed
+     */
+    public function handle(Request $request, Closure $next): mixed
+    {
+        /*if ($request->admin->is_pass != 1 && $request->admin->is_root != 1 || $request->admin->valid != 1 && $request->admin->is_root != 1) {
+            $res = returnFormatError('无权限', 401);
+            throw new HttpResponseException(Response::create($res, "json"));
+        }*/
+       /* $isPass = false;
+        $role_id = $request->admin->role_id;
+        $test = Db::table('role')->where('id', $role_id)->find();
+        $codes = explode(',', $test['codes']);
+        $list = config('permission_action');
+        $ctrl = $request->controller();
+        $fun = $request->action();
+//        dump($ctrl.'_'.$fun);
+        foreach ($list as $k => $v) {
+            if ($ctrl . '_' . $fun == $k) {
+//                dump('a=>'.$v);
+                foreach ($codes as $kk => $vv) {
+                    if ($v == $vv) {
+//                        dump('b=>'.$vv);
+                        $isPass = true;
+                        break;
+                    }
+                }
+            }
+        }
+        // || $fun=='import' || $fun=='export' || $fun=='pass' || $fun=='rePass'
+        if ($request->admin->is_root == 1 || $fun == 'init' || $fun == 'initDetail') {
+            $isPass = true;
+        }
+        // 添加中间件执行代码
+        $admin = $request->admin;
+        if (!$isPass) {
+            $res = returnFormatError('无权限', 555);
+            throw new HttpResponseException(Response::create($res, "json"));
+        }*/
+        return $next($request);
+    }
+
+
+    /**
+     * 返回TOKEN错误代码内容
+     * @param $code
+     * @return string
+     */
+    private static function getError($code): string
+    {
+        $errArr = self::getErrorArr();
+        if (!key_exists($code, $errArr)) {
+            return "未知错误";
+        }
+        return $errArr[$code];
+    }
+
+    /**
+     * 获取TOKEN错误码数组
+     * @return array
+     */
+    private static function getErrorArr(): array
+    {
+        return [
+            self::$CODE_SUCCESS => "成功",
+            self::$CODE_ERR => "系统异常",
+        ];
+    }
+
+}

+ 41 - 0
api/app/admin/middleware/CheckPermissionAttr.php

@@ -0,0 +1,41 @@
+<?php
+
+namespace app\admin\middleware;
+
+use app\admin\attr\Permission;
+use app\common\exception\CatchException;
+use app\common\model\Admin;
+use think\Request;
+
+class CheckPermissionAttr
+{
+    public function handle(Request $request, \Closure $next)
+    {
+        // 通过依赖注入获取admin
+        $admin = app(Admin::class);
+        $role = $admin->role;
+        $codes = $role->codes;
+
+        // 获取权限注解
+        $controller = 'app\\admin\\controller\\'. $request->controller();
+        $ref = new \ReflectionClass($controller);
+        $attrs = $ref->getAttributes(Permission::class);
+
+        // 检查权限
+        /**
+         * @var \ReflectionAttribute $attrRaw 
+         */
+        foreach($attrs as $attrRaw) {
+            /**
+             * @var Permission
+             */
+            $attr = $attrRaw->newInstance();
+            $permission = $attr->value;
+            if (false && !in_array($permission, $codes)) {
+                throw new CatchException("未具有权限$permission, 禁止访问", 403);
+            }
+        }
+
+        return $next($request);
+    }
+}

+ 145 - 0
api/app/admin/middleware/Login.php

@@ -0,0 +1,145 @@
+<?php
+declare (strict_types=1);
+
+namespace app\admin\middleware;
+
+use app\common\model\Admin;
+use app\facade\Encryption;
+use app\Request;
+use Closure;
+use think\db\exception\DataNotFoundException;
+use think\db\exception\DbException;
+use think\db\exception\ModelNotFoundException;
+use think\exception\HttpResponseException;
+use think\Response;
+
+/**
+ * 登录认证
+ * Class CheckAdmin
+ * @package app\middleware
+ */
+class Login
+{
+    protected static int $CODE_SUCCESS = 0; //成功
+    protected static int $CODE_TOKEN_EXPIRE = 11; //token过期
+    protected static int $CODE_TOKEN_ERR = 12; //token不正确或已失效
+    protected static int $CODE_TOKEN_NONE = 13; //缺少token
+    protected static int $CODE_TOKEN_FORMAT_ERR = 14;//token格式不正确
+    protected static int $SYSTEM_ERR = 999;//系统异常
+
+    /**
+     * 处理请求
+     * @param Request $request
+     * @param Closure $next
+     * @return mixed
+     */
+    public function handle(Request $request, Closure $next): mixed
+    {
+        $admin = $this->checkToken();
+       /* $department_ids = \app\common\model\DepartmentAdmin::where('admin_id', $admin->id)->column('department_id');
+        $result =[];
+        foreach ($department_ids as $k => $v) {
+            $this->getDept($v, $result);
+        }
+        $n_array = array_merge($department_ids,$result);
+        $relate_admin_ids = \app\common\model\Admin::where('department_id', 'in', $n_array)->column('id');
+        $request->admin = $admin;
+        $request->company_id = $admin->company_id;
+        $request->relate_admin_ids = $relate_admin_ids;*/
+        return $next($request);
+
+    }
+
+    public function getDept($parent_id, &$result): void
+    {
+        $dept = \app\common\model\Department::where('parent_id', $parent_id)->column('id');
+        foreach ($dept as $k => $v) {
+            array_push($result, $v);
+            $this->getDept($v, $result);
+        }
+    }
+
+    /**
+     * 结束调度
+     * @param Response $response
+     */
+    public function end(Response $response): void
+    {
+        // 回调行为
+//        $response->send();
+    }
+
+    /**
+     * 检测token
+     * token规则
+     * token由base64编码,解码后分为密文、主键、过期时间(时间戳)三部分,用竖线|隔开
+     */
+    public function checkToken()
+    {
+        $token = Encryption::getToken();
+        if ($token == 1) {
+            return (new Admin)->detail(['id' => 27]);
+        }
+
+        if (!$token) {
+            $res = returnFormat(self::$CODE_TOKEN_NONE, self::getError(self::$CODE_TOKEN_NONE));
+            throw new HttpResponseException(Response::create($res, "json"));
+        }
+        $tokenReal = base64_decode($token);
+        $tokenArr = explode("|", $tokenReal);//拆分token
+
+        if (count($tokenArr) != 3) {
+            $res = returnFormat(self::$CODE_TOKEN_FORMAT_ERR, self::getError(self::$CODE_TOKEN_FORMAT_ERR));
+            throw new HttpResponseException(Response::create($res, "json"));
+        }
+        //判断token有没有超时
+        if (time() > $tokenArr[2]) {
+            $res = returnFormat(self::$CODE_TOKEN_EXPIRE, self::getError(self::$CODE_TOKEN_EXPIRE));
+            throw new HttpResponseException(Response::create($res, "json"));
+        }
+
+        $adminId = $tokenArr[1];
+        try {
+            $admin = (new Admin)->where("id", "=", $adminId)->find();
+        } catch (DataNotFoundException|ModelNotFoundException|DbException $e) {
+            $res = returnFormat(self::$SYSTEM_ERR, $e->getMessage());
+            throw new HttpResponseException(Response::create($res, "json"));
+        }//找到token
+        if (!$admin) {
+            $res = returnFormat(self::$CODE_TOKEN_ERR, self::getError(self::$CODE_TOKEN_ERR));
+            throw new HttpResponseException(Response::create($res, "json"));
+        }
+        return $admin;
+    }
+
+
+    /**
+     * 返回TOKEN错误代内容
+     * @param $code
+     * @return string
+     */
+    private static function getError($code): string
+    {
+        $errArr = self::getErrorArr();
+        if (!key_exists($code, $errArr)) {
+            return "未知错误";
+        }
+        return $errArr[$code];
+    }
+
+    /**
+     * 获取TOKEN错误码数组
+     * @return array
+     */
+    private static function getErrorArr(): array
+    {
+        return [
+            self::$CODE_SUCCESS => "成功",
+            self::$CODE_TOKEN_EXPIRE => "token过期",
+            self::$CODE_TOKEN_ERR => "token不正确或已失效",
+            self::$CODE_TOKEN_NONE => "缺少token",
+            self::$CODE_TOKEN_FORMAT_ERR => "token格式不正确",
+            self::$SYSTEM_ERR => "系统异常",
+        ];
+    }
+}

+ 224 - 0
api/app/common.php

@@ -0,0 +1,224 @@
+<?php
+// 应用公共文件
+
+
+if (!function_exists('returnFormat')) {
+    /**
+     * 格式化输出返回值
+     * @param int $code
+     * @param string $msg
+     * @param mixed $data
+     * @return mixed
+     */
+    function returnFormat($code = 0, $msg = "", $data = [])
+    {
+        $res['code'] = $code;
+        $res['data'] = $data;
+        $res['msg'] = $msg;
+        return $res;
+    }
+}
+
+
+if (!function_exists('each_item')) {
+    function each_item(&$array)
+    {
+        $res = array();
+        $key = key($array);
+        if ($key !== null) {
+            next($array);
+            $res[1] = $res['value'] = $array[$key];
+            $res[0] = $res['key'] = $key;
+        } else {
+            $res = false;
+        }
+        return $res;
+    }
+}
+
+if (!function_exists('getVirRootDir')) {
+    /**
+     * 获取虚拟目录路径
+     * @return bool|string
+     */
+    function getVirRootDir()
+    {
+        $url = $_SERVER['SCRIPT_NAME'];
+        $url = substr($url, 0, strripos($url, "/"));
+        return $url;
+    }
+}
+
+
+if (!function_exists('getNow')) {
+    /**
+     * 获取当时时间
+     * @param string $fmt 格式化
+     * @return false|string
+     */
+    function getNow($fmt = "Y-m-d H:i:s")
+    {
+        return date($fmt);
+    }
+}
+
+if (!function_exists('getUrl')) {
+    /**
+     * 获取当前的访问路径
+     * @return [type] [description]
+     */
+    function getUrl()
+    {
+        $sys_protocal = isset($_SERVER['SERVER_PORT']) && $_SERVER['SERVER_PORT'] == '443' ? 'https://' : 'http://';
+        $php_self = $_SERVER['PHP_SELF'] ? $_SERVER['PHP_SELF'] : $_SERVER['SCRIPT_NAME'];
+        $path_info = isset($_SERVER['PATH_INFO']) ? $_SERVER['PATH_INFO'] : '';
+        $relate_url = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : $php_self . (isset($_SERVER['QUERY_STRING']) ? '?' . $_SERVER['QUERY_STRING'] : $path_info);
+        return $sys_protocal . (isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : '') . $relate_url;
+    }
+}
+
+if (!function_exists('number2chinese')) {
+
+    function number2chinese($num)
+    {
+        $arr = array('零', '壹', '贰', '叁', '肆', '伍', '陆', '柒', '捌', '玖');
+        $cny = array('', '拾', '佰', '仟', '', '萬', '亿', '兆');
+        //小数部分
+        $retval = '';
+        if (strpos($num, '.') !== false) {
+            list($num, $dec) = explode('.', $num);
+            $retval .= $arr[$dec[0]] . '角' . $arr[$dec[1]] . '分';
+        }
+        //整数部分
+        $str = $num != '0' ? strrev($num) : '';
+        $out = array();
+        for ($i = 0; $i < strlen($str); $i++) {
+            $out[$i] = $arr[$str[$i]];
+            $out[$i] .= $str[$i] != '0' ? $cny[$i % 4] : '';
+            if ($i > 1 && $str[$i] + $str[$i - 1] == 0) {
+                $out[$i] = '';
+            }
+            if ($i % 4 == 0) {
+                $out[$i] .= $cny[4 + floor($i / 4)];
+            }
+            //echo $out[$i].'<br>';
+        }
+        $retval = implode('', array_reverse($out)) . '元' . $retval;
+        return $retval;
+    }
+}
+
+
+if (!function_exists('toUnderScore')) {
+    /**
+     * 驼峰命名转下划线命名
+     * 思路:
+     * 小写和大写紧挨一起的地方,加上分隔符,然后全部转小写
+     * @param $camelCaps
+     * @param string $separator
+     * @return string
+     * @author web
+     */
+    function toUnderScore($camelCaps, $separator = '_')
+    {
+        return strtolower(preg_replace('/([a-z])([A-Z])/', "$1" . $separator . "$2", $camelCaps));
+    }
+}
+
+
+if (!function_exists('convertUTF8')) {
+    /**
+     * 解决中文乱码的问题
+     * @param $str
+     * @return string
+     */
+    function convertUTF8($str)
+    {
+        if (empty($str)) return '';
+        return iconv('utf-8', 'gb2312', $str);
+    }
+}
+if (!function_exists('isIdCardNo')) {
+    /**
+     * 判断是否为合法的身份证号码
+     * @param $mobile
+     * @return int
+     */
+    function isIdCardNo($vStr)
+    {
+        $vCity = array(
+            '11', '12', '13', '14', '15', '21', '22',
+            '23', '31', '32', '33', '34', '35', '36',
+            '37', '41', '42', '43', '44', '45', '46',
+            '50', '51', '52', '53', '54', '61', '62',
+            '63', '64', '65', '71', '81', '82', '91'
+        );
+
+        if (!preg_match('/^([\d]{17}[xX\d]|[\d]{15})$/', $vStr)) return false;
+        if (!in_array(substr($vStr, 0, 2), $vCity)) return false;
+        $vStr = preg_replace('/[xX]$/i', 'a', $vStr);
+        $vLength = strlen($vStr);
+        if ($vLength == 18) {
+            $vBirthday = substr($vStr, 6, 4) . '-' . substr($vStr, 10, 2) . '-' . substr($vStr, 12, 2);
+        } else {
+            $vBirthday = '19' . substr($vStr, 6, 2) . '-' . substr($vStr, 8, 2) . '-' . substr($vStr, 10, 2);
+            return false;//不考虑一代身份证了
+        }
+        if (date('Y-m-d', strtotime($vBirthday)) != $vBirthday) return false;
+        if ($vLength == 18) {
+            $vSum = 0;
+            for ($i = 17; $i >= 0; $i--) {
+                $vSubStr = substr($vStr, 17 - $i, 1);
+                $vSum += (pow(2, $i) % 11) * (($vSubStr == 'a') ? 10 : intval($vSubStr, 11));
+            }
+            if ($vSum % 11 != 1) return false;
+        }
+        return true;
+    }
+}
+if (!function_exists('cleanEnter')) {
+    /**
+     * 清除回车换行和前后空格
+     * @param $str
+     * @return array|string|string[]
+     */
+    function cleanEnter($str)
+    {
+        $str = trim($str);
+        $str = str_replace("\n", "", $str);
+        $str = str_replace("\r", "", $str);
+        return $str;
+    }
+}
+
+if (!function_exists('randNum')) {
+    /**
+     * 获取数字随机数
+     * @param $length 数字长度
+     * @return int
+     */
+    function randNum($length = 8)
+    {
+        $min = pow(10, $length - 1) + 1;
+        $max = pow(10, $length) - 1;
+        $rand = rand($min, $max);
+//        echo "length: $length, min: $min ,max: $max ,rand: $rand \r\n <br/>";
+        return $rand;
+    }
+}
+if (!function_exists('getParamValue')) {
+    /**
+     * 获取数组结构的参数值
+     * @param $key 键名
+     * @param $array 参数数组
+     * @param $default 默认值,如果键不存在就返回此值,默认为NULL
+     * @return mixed|null 返回值
+     */
+    function getParamValue($key, $array, $default = null)
+    {
+        if (array_key_exists($key, $array)) {
+            return $array[$key];
+        }
+        return $default;
+    }
+}

+ 52 - 0
api/app/common/ErrorCode.php

@@ -0,0 +1,52 @@
+<?php
+
+
+namespace app\common;
+
+
+class ErrorCode
+{
+    const CODE_SUCC = "0";//成功代码
+    const CODE_DB_ERROR = "9005";//数据库写入失败
+    const CODE_RECORD_NOT_FOUND = "9404";//记录未找到或已被删除0
+    const CODE_TOKEN_ERR = "12";
+    const CODE_TOKEN_EXPIRE = "11";
+    const CODE_TOKEN_FORMAT_ERR = "14";
+    const CODE_TOKEN_NONE = "13";
+
+    /**
+     * 返回错误代内容
+     * @param $code
+     * @return mixed
+     */
+    public static function getError($code)
+    {
+        $errArr = self::getErrorArr();
+        if (!key_exists($code, $errArr)) {
+            return "未知错误";
+        }
+        return $errArr[$code];
+    }
+
+    /**
+     * 获取错误码数组
+     * @return array
+     */
+    protected static function getErrorArr()
+    {
+        return [
+            self::CODE_SUCC => "成功",
+            //100以内,需要重新登录
+            self::CODE_TOKEN_EXPIRE => "token过期",
+            self::CODE_TOKEN_ERR => "token不正确或已失效",
+            self::CODE_TOKEN_NONE => "缺少token",
+            self::CODE_TOKEN_FORMAT_ERR => "token格式不正确",
+            "9001" => "缺少签名",
+            "9002" => "签名不正确",
+            "9004" => "请求已过期",
+            self::CODE_DB_ERROR => "数据写入失败,请稍后再试",
+            self::CODE_RECORD_NOT_FOUND => "记录未找到或已被删除",
+            "9999" => "系统错误",
+        ];
+    }
+}

+ 2 - 0
api/app/common/common.php

@@ -0,0 +1,2 @@
+<?php
+// 这是系统自动生成的公共文件

+ 37 - 0
api/app/common/controller/JwtAuthorizedTrait.php

@@ -0,0 +1,37 @@
+<?php
+
+namespace app\common\controller;
+
+use app\common\exception\CatchException;
+use think\facade\Log;
+
+trait JwtAuthorizedTrait
+{
+    /**
+     * $payload Jwt 载荷,stdClass
+     *
+     * @var \stdClass
+     */
+    protected $payload;
+
+    public function initialize()
+    {
+        parent::initialize();
+        // 允许cros通过jwt所在的header
+        $jwt = $this->request->header('Authorization');
+        if (!$jwt) {
+            throw new CatchException("请登录", 600);
+        }
+
+        // 解析jwt
+        $jwt = str_replace('Bearer ', '', $jwt);
+        Log::debug("尝试解析Jwt: " . var_export($jwt, true));
+
+        $this->payload = $this->decodeJwt($jwt);
+
+        // 调用用户自定义的初始化语句
+        if (method_exists($this, 'init')) {
+            $this->init();
+        }
+    }
+}

+ 120 - 0
api/app/common/controller/JwtBaseController.php

@@ -0,0 +1,120 @@
+<?php
+
+
+namespace app\common\controller;
+
+use Firebase\JWT\JWT;
+use Firebase\JWT\Key;
+use app\BaseController;
+use app\common\util\Result;
+use EasyWeChatComposer\EasyWeChat;
+use app\common\exception\CatchException;
+
+class JwtBaseController extends BaseController
+{
+
+    protected $failException = true;
+
+    /**
+     * $key jwt HS256 key
+     *
+     * @var string
+     */
+    protected $key;
+
+    /**
+     * $table 当前使用的表
+     *
+     * @var mixed
+     */
+    protected $table;
+
+    /**
+     * $model 当前使用的模型
+     *
+     * @var \think\Model
+     */
+    protected $model;
+
+    /**
+     * $wechat 微信小程序api实例
+     *
+     * @var \EasyWeChat\MiniProgram\Application
+     */
+    private static $_wechat;
+
+    /**
+     * 设置jwt密钥
+     *
+     * @param mixed $key
+     * 
+     * @return void
+     */
+    protected function setKey($key) {
+        $this->key = $key;
+    }
+
+    /**
+     * 验证token
+     *
+     * @return void
+     */
+    public function valid()
+    {
+        // 允许 Authorization 头部通过 cors
+        $jwt = $this->request->header('Authorization');
+        if (empty($jwt)) {
+            throw new CatchException("未授权用户", 600);
+        }
+
+        $jwt = str_replace('Bearer ', '', $jwt);
+        // try decode
+        $this->decodeJwt($jwt);
+        return Result::rest(true);
+    }
+
+    /**
+     * 默认参数
+     *
+     * @return mixed
+     */
+    protected function params($validator = null, $name = '')
+    {
+        $params = $this->request->param($name, null, 'trim');
+        // 尝试校验
+        if (!is_null($validator)) {
+            $this->validate($params, $validator);
+        }
+        return $params;
+    }
+
+    protected function only(array $names)
+    {
+        return $this->request->only($names);
+    }
+
+    /**
+     * encodeJwt
+     *
+     * @param array|\stdClass $payload
+     *
+     * @return string
+     */
+    protected function encodeJwt($payload)
+    {
+        return JWT::encode($payload, $this->key, 'HS256');
+    }
+
+    /**
+     * decodeJwt
+     *
+     * @param string $jwt
+     *
+     * @return \stdClass
+     */
+    protected function decodeJwt($jwt)
+    {
+        return JWT::decode($jwt, new Key($this->key, 'HS256'));
+    }
+
+}

+ 5 - 0
api/app/common/event.php

@@ -0,0 +1,5 @@
+<?php
+// 这是系统自动生成的event定义文件
+return [
+
+];

+ 12 - 0
api/app/common/exception/CatchException.php

@@ -0,0 +1,12 @@
+<?php
+
+namespace app\common\exception;
+
+use Exception;
+
+/**
+ * 应该被捕获器捕获的异常
+ */
+class CatchException extends Exception
+{
+}

+ 5 - 0
api/app/common/middleware.php

@@ -0,0 +1,5 @@
+<?php
+// 这是系统自动生成的middleware定义文件
+return [
+
+];

+ 63 - 0
api/app/common/middleware/AllowCrossDomain.php

@@ -0,0 +1,63 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006~2021 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: liu21st <liu21st@gmail.com>
+// +----------------------------------------------------------------------
+declare (strict_types=1);
+
+namespace app\common\middleware;
+
+use Closure;
+use think\Config;
+use think\Request;
+use think\Response;
+
+/**
+ * 跨域请求支持
+ */
+class AllowCrossDomain
+{
+    protected mixed $cookieDomain;
+
+    protected array $header = [
+        'Access-Control-Allow-Credentials' => 'true',
+        'Access-Control-Max-Age' => 1800,
+        'Access-Control-Allow-Methods' => 'GET, POST, PATCH, PUT, DELETE, OPTIONS',
+        'Access-Control-Allow-Headers' => 'Authorization, Content-Type, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, X-CSRF-TOKEN, X-Requested-With',
+    ];
+
+    public function __construct(Config $config)
+    {
+        $this->cookieDomain = $config->get('cookie.domain', '');
+    }
+
+    /**
+     * 允许跨域请求
+     * @access public
+     * @param Request $request
+     * @param Closure $next
+     * @param array|null $header
+     * @return Response
+     */
+    public function handle(Request $request, Closure $next, ?array $header = []): Response
+    {
+        $header = !empty($header) ? array_merge($this->header, $header) : $this->header;
+
+        if (!isset($header['Access-Control-Allow-Origin'])) {
+            $origin = $request->header('origin');
+
+            if ($origin && ('' == $this->cookieDomain || strpos($origin, $this->cookieDomain))) {
+                $header['Access-Control-Allow-Origin'] = $origin;
+            } else {
+                $header['Access-Control-Allow-Origin'] = '*';
+            }
+        }
+
+        return $next($request)->header($header);
+    }
+}

+ 40 - 0
api/app/common/middleware/WriteLog.php

@@ -0,0 +1,40 @@
+<?php
+declare (strict_types=1);
+
+namespace app\common\middleware;
+
+use app\common\model\AdminLog;
+use app\Request;
+use Closure;
+use think\facade\Log;
+
+/**
+ * 全局日志记录
+ * Class WriteLog
+ * @package app\middleware
+ */
+class WriteLog
+{
+    /**
+     * @param Request $request
+     * @param Closure $next
+     * @return mixed
+     */
+    public function handle(Request $request, Closure $next): mixed
+    {
+        $response = $next($request);
+        // 添加中间件执行代码
+        Log::record("===============" . getNow() . "全局日志记录===============", "debug");
+        Log::record("request:" . $request->url(true), "debug");
+        Log::record('REFERER  ' . (array_key_exists('HTTP_REFERER', $_SERVER) ? $_SERVER['HTTP_REFERER'] : ""));
+        Log::record('GET  ' . json_encode($_GET, JSON_UNESCAPED_UNICODE));
+        Log::record('POST  ' . json_encode($_POST, JSON_UNESCAPED_UNICODE));
+        Log::record('cookie  ' . json_encode($_COOKIE, JSON_UNESCAPED_UNICODE));
+        Log::record('param  ' . json_encode(input('param.'), JSON_UNESCAPED_UNICODE));
+
+
+
+
+        return $response;
+    }
+}

+ 200 - 0
api/app/common/model/Admin.php

@@ -0,0 +1,200 @@
+<?php
+
+namespace app\common\model;
+
+use think\db\Query;
+
+/**
+ * @property Role $role 关联角色
+ */
+class Admin extends Base
+{
+    /**
+     * 关联角色
+     * @return \think\model\relation\HasOne
+     */
+    public function role()
+    {
+        return $this->hasOne(Role::class, "id", "role_id");
+    }
+
+    /**
+     * 修改管理员信息
+     * @param $id
+     * @param $phone
+     * @param $roleId
+     * @param $valid
+     * @return array|mixed
+     * @throws \think\db\exception\DataNotFoundException
+     * @throws \think\db\exception\DbException
+     * @throws \think\db\exception\ModelNotFoundException
+     */
+    public static function edit($id, $phone, $roleId, $valid)
+    {
+        $admin = Admin::find($id);
+        if (!$admin) {
+            return returnFormat(999, "记录未找到");
+        }
+        $wherePhone = [];
+        $wherePhone[] = ["phone", "=", $phone];
+        $wherePhone[] = ["id", "<>", $id];
+        $exit = Admin::where($wherePhone)->find();
+        if ($exit) {
+            return returnFormat(999, "该手机号已被其它管理员绑定,请更换手机号");
+        }
+        $admin->phone = $phone;
+        $admin->role_id = $roleId;
+        $admin->valid = $valid;
+        $res = $admin->save();
+        if ($res === false) {
+            return returnFormat(999, "提交失败:数据库写入失败");
+        }
+        return returnFormat(0, "", $admin);
+    }
+
+    public static function resetPwd($id,$password){
+        $admin = Admin::find($id);
+        if (!$admin) {
+            return returnFormat(999, "记录未找到");
+        }
+        $admin->password = self::md5($admin->salt, $password);
+        $res = $admin->save();
+        if ($res === false) {
+            return returnFormat(999, "提交失败:数据库写入失败");
+        }
+        return returnFormat(0, "", $admin);
+    }
+    /**
+     * 删除数据
+     * @param $ids
+     * @return array|mixed
+     */
+    public static function del($ids)
+    {
+        $whereDelete = [];
+        $whereDelete[] = ["id", "in", $ids];
+        $updateData = [
+            "delete_time" => getNow(),
+        ];
+//        Log::record("whereDelete".print_r($whereDelete,true),"debug");
+//        Log::record("updateData".print_r($updateData,true),"debug");
+        $res = (new Admin())->where($whereDelete)->update($updateData);
+        if ($res === false) {
+            return returnFormat(999, "删除失败:数据库写入失败");
+        }
+        return returnFormat(0, "", $res);
+    }
+
+    /**
+     * 添加管理员
+     * @param $name
+     * @param $password
+     * @param $phone
+     * @param $roleId
+     * @param $valid
+     * @return array|mixed
+     * @throws \think\db\exception\DataNotFoundException
+     * @throws \think\db\exception\DbException
+     * @throws \think\db\exception\ModelNotFoundException
+     */
+    public static function add($name, $password, $phone, $roleId, $valid)
+    {
+        $admin = Admin::where("name", $name)->find();
+        if ($admin) {
+            return returnFormat(999, "账号已存在,请修改账号");
+        }
+        $admin = Admin::where("phone", $phone)->find();
+        if ($admin) {
+            return returnFormat(999, "手机号已存在,请修改手机号");
+        }
+        $admin = new Admin([
+            "name" => $name,
+            "phone" => $phone,
+            "role_id" => $roleId,
+            "valid" => $valid,
+        ]);
+        $salt = rand(1000, 9999);
+        $admin->salt = $salt;
+        $admin->password = self::md5($salt, $password);
+        $res = $admin->save();
+        if ($res === false) {
+            return returnFormat(999, "提交失败:数据库写入失败");
+        }
+        return returnFormat(0, "", $admin);
+    }
+
+
+
+    /**
+     * 获取管理员列表
+     * @param $keyword
+     * @param $listRow
+     * @return void
+     * @throws \think\db\exception\DbException
+     */
+    public static function getList($keyword = "", $listRow = 20)
+    {
+        $where = [];
+        if ($keyword) {
+            $where[] = ["name|phone", "like", "%" . $keyword . "%"];
+        }
+        $list = Admin::with(['role' => function (Query $query) {
+            $query->field("id,name,valid");
+        }])->where($where)->order("id desc")->paginate($listRow);
+        return returnFormat(0, '', $list);
+    }
+
+    /**
+     * 加密密码
+     * @param $pwd
+     * @param $salt
+     * @return string
+     */
+    private static function md5($salt, $pwd)
+    {
+        $str = md5($salt . $pwd);
+        return $str;
+    }
+
+    /**
+     * 登录接口
+     * @param string $name
+     * @param string $password
+     * @return array|mixed
+     * @throws \think\db\exception\DataNotFoundException
+     * @throws \think\db\exception\DbException
+     * @throws \think\db\exception\ModelNotFoundException
+     */
+    public static function login(string $name, string $password)
+    {
+        $admin = self::where('name|phone', '=', $name)->find();
+        if (!$admin) {
+            return returnFormat(999, "用户未找到");
+        }
+        if ($admin->getAttr('password') != self::md5($admin->getAttr('salt'), $password)) {
+            return returnFormat(999, '登录密码不正确' . $admin->getAttr('salt') . $password);
+        }
+        if (!$admin->valid) {
+            return returnFormat(999, "账号被禁用,请联系管理员");
+        }
+        //更新登录信息
+        $admin->login_count = $admin->login_count + 1;
+        $admin->login_last_time = getNow();
+        $admin->token = $admin->getToken();//更新token
+        $admin->save();
+        return returnFormat(0, "", $admin);
+    }
+
+
+    /**
+     * 获取token
+     * @return string
+     */
+    public function getToken()
+    {
+        $expireDays = 7;//过期时间,单位天
+        //token:  md5([用户名][当前时间])|[用户id]|[过期时间]
+        $token = base64_encode(md5($this->login_name . getNow()) . "|" . $this->id . "|" . (time() + 86400 * $expireDays));
+        return $token;
+    }
+}

+ 66 - 0
api/app/common/model/Allocation.php

@@ -0,0 +1,66 @@
+<?php
+
+namespace app\common\model;
+
+use app\common\model\Base;
+use app\common\model\AllocationDetail;
+
+/**
+ * 物品调拨单
+ * 
+ * @property string $date 出入库日期
+ * @property int $from_repo_id 调出仓库id
+ * @property int $to_repo_id 调入仓库id
+ * @property int $revert_id 回滚id
+ * @property int $out_io_id 调出单ids
+ * @property int $in_io_id 调入单id
+ * @property array<AllocationDetail> $details 明细
+ * @property Repo $from 调出仓库
+ * @property Repo $to 调入仓库
+ * @property Io $out_id 调出单
+ * @property Io $in_io 调入单
+ */
+class Allocation extends Base
+{
+    protected $schema = [
+        'id'         => 'int',       // id
+        'from_repo_id'       => 'int',       // 调出仓库ID
+        'to_repo_id'         => 'int',       // 调入仓库ID
+        'create_time'        => 'datetime',  // 创建时间
+        'update_time'        => 'datetime',  // 更新时间
+        'delete_time'        => 'datetime',  // 删除时间
+        'valid'      => 'tinyint',   // 状态,1启用,0禁用
+        'date'       => 'date',      // 出入库日期
+        'remark'     => 'varchar',   // 备注
+        'admin_id'   => 'int',       // 操作人ID
+        'revert_id'      => 'int',       // 回滚id
+        'out_io_id'      => 'int',       // 调出单id
+        'in_io_id'       => 'int',       // 调入单id
+    ];
+
+    public function details()
+    {
+        return $this->hasMany(AllocationDetail::class);
+    }
+
+    public function from()
+    {
+        return $this->belongsTo(Repo::class, 'from_repo_id', 'id');
+    }
+
+    public function to()
+    {
+        return $this->belongsTo(Repo::class, 'to_repo_id', 'id');
+    }
+
+    public function out_io()
+    {
+        return $this->belongsTo(Io::class, 'out_io_id', 'id');
+    }
+
+    public function in_io()
+    {
+        return $this->belongsTo(Io::class, 'in_io_id', 'id');
+    }
+
+}

+ 37 - 0
api/app/common/model/AllocationDetail.php

@@ -0,0 +1,37 @@
+<?php
+
+namespace app\common\model;
+
+use app\common\model\Allocation;
+use app\common\model\Base;
+
+/**
+ * 
+ * @property int $good_id 物品id
+ * @property number $num 数量
+ */
+class AllocationDetail extends Base
+{
+    protected $schema = [
+        'id'     => 'int',       // id
+        'good_id'        => 'int',       // 物品ID
+        'create_time'    => 'datetime',  // 创建时间
+        'update_time'    => 'datetime',  // 更新时间
+        'delete_time'    => 'datetime',  // 删除时间
+        'valid'  => 'tinyint',   // 状态,1启用,0禁用
+        'num'    => 'float',     // 数量
+        'date'   => 'date',      // 出入库日期
+        'remark'         => 'varchar',   // 备注
+        'allocation_id'  => 'int',       // 调拔单订单ID
+    ];
+
+    public function allocation()
+    {
+        return $this->belongsTo(Allocation::class);
+    }
+
+    public function good()
+    {
+        return $this->belongsTo(Good::class);
+    }
+}

+ 27 - 0
api/app/common/model/Base.php

@@ -0,0 +1,27 @@
+<?php
+/**
+ * Created by PhpStorm.
+ * User: dengjq
+ * Date: 2019/1/24
+ * Time: 14:59
+ */
+
+namespace app\common\model;
+
+
+use think\Model;
+use think\model\concern\SoftDelete;
+
+/**
+ * 模型基类
+ * 
+ * @property int $id 模型主键
+ * @property string $create_time 创建时间
+ * @property string $udpate_time 更新时间
+ * @property string $delete_time 删除时间
+ * @property int|bool $valid 状态,1启用,0禁用,可以直接当布尔使用
+ */
+abstract class Base extends Model
+{
+    use SoftDelete;
+}

+ 24 - 0
api/app/common/model/Check.php

@@ -0,0 +1,24 @@
+<?php
+
+namespace app\common\model;
+
+class Check extends Base
+{
+        protected $schema = [
+        'id'     => 'int',       // id
+        'repertory_id'   => 'int',       // 仓库ID
+        'create_time'    => 'datetime',  // 创建时间
+        'update_time'    => 'datetime',  // 更新时间
+        'delete_time'    => 'datetime',  // 删除时间
+        'valid'  => 'tinyint',   // 状态,1启用,0禁用
+        'date'   => 'date',      // 出入库日期
+        'remark'         => 'varchar',   // 备注
+        'sn'     => 'varchar',   // 订单号
+        'admin_id'       => 'int',       // 操作人ID
+    ];
+
+    public function details()
+    {
+        return $this->hasMany(CheckDetail::class);
+    }
+}

+ 25 - 0
api/app/common/model/CheckDetail.php

@@ -0,0 +1,25 @@
+<?php
+
+namespace app\common\model;
+
+class CheckDetail extends Base
+{
+    protected $schema = [
+        'id'     => 'int',       // id
+        'good_id'        => 'int',       // 物品ID
+        'create_time'    => 'datetime',  // 创建时间
+        'update_time'    => 'datetime',  // 更新时间
+        'delete_time'    => 'datetime',  // 删除时间
+        'valid'  => 'tinyint',   // 状态,1启用,0禁用
+        'origin_num'     => 'float',     // 原数量
+        'num'    => 'float',     // 数量
+        'date'   => 'date',      // 出入库日期
+        'remark'         => 'varchar',   // 备注
+        'check_id'       => 'int',       // 盘点单ID
+    ];
+
+    public function check()
+    {
+        return $this->belongsTo(Check::class);
+    }
+}

+ 68 - 0
api/app/common/model/Config.php

@@ -0,0 +1,68 @@
+<?php
+
+
+namespace app\common\model;
+
+/**
+ * 配置模型类
+ * @package app\common\model
+ */
+class Config extends Base
+{
+    const CODE_WECHAT = 'wechat';
+
+    // 设置json类型字段
+    protected $json = ['content'];
+
+    /**
+     * 获取配置对象
+     * @param string $code
+     * @return array|mixed|\think\Model|null
+     * @throws \think\db\exception\DataNotFoundException
+     * @throws \think\db\exception\DbException
+     * @throws \think\db\exception\ModelNotFoundException
+     */
+    public static function getConfig($code)
+    {
+        $config = (new self)->where('code', $code)->cache(true, 60)->find();
+        if (empty($config)) {
+            $configData = [
+                'code' => $code,
+                'create_time' => getNow(),
+                'update_time' => getNow(),
+            ];
+
+            $config = self::create($configData);
+        }
+        return $config;
+    }
+
+    /**
+     * 获取配置数据内容
+     * @param $code
+     * @return mixed
+     * @throws \think\db\exception\DataNotFoundException
+     * @throws \think\db\exception\DbException
+     * @throws \think\db\exception\ModelNotFoundException
+     */
+    public static function getConfigContent($code)
+    {
+        $config = self::getConfig($code);
+        return $config->content;
+    }
+
+    /**
+     * 获取配置数据中指定的值
+     * @param $code
+     * @param $content_code 如果读取的为json值才有效
+     * @return bool|mixed
+     * @throws \think\db\exception\DataNotFoundException
+     * @throws \think\db\exception\DbException
+     * @throws \think\db\exception\ModelNotFoundException
+     */
+    public static function getConfigValue($code, $content_code = '')
+    {
+        $content = self::getConfigContent($code);
+        return $content->$content_code;
+    }
+}

+ 26 - 0
api/app/common/model/Good.php

@@ -0,0 +1,26 @@
+<?php
+
+namespace app\common\model;
+
+class Good extends Base
+{
+    protected $schema = [
+        'id'     => 'int',       // id
+        'no'     => 'varchar',   // 编号
+        'name'   => 'varchar',   // 物品名称
+        'desc'   => 'varchar',   // 物品介绍
+        'unit'   => 'varchar',   // 单位
+        'img'    => 'varchar',   // 图片
+        'create_time'    => 'datetime',  // 创建时间
+        'update_time'    => 'datetime',  // 更新时间
+        'delete_time'    => 'datetime',  // 删除时间
+        'valid'  => 'tinyint',   // 状态,1启用,0禁用
+        'spec'   => 'varchar',   // 规格
+        'good_class_id'  => 'int',       // 类别
+    ];
+
+    public function goodClass()
+    {
+        return $this->belongsTo(GoodClass::class);
+    }
+}

+ 16 - 0
api/app/common/model/GoodClass.php

@@ -0,0 +1,16 @@
+<?php
+
+namespace app\common\model;
+
+class GoodClass extends Base
+{
+    protected $schema = [
+        'id'     => 'int',       // id
+        'name'   => 'varchar',   // 物品类别名称
+        'desc'   => 'varchar',   // 类别说明
+        'create_time'    => 'datetime',  // 创建时间
+        'update_time'    => 'datetime',  // 更新时间
+        'delete_time'    => 'datetime',  // 删除时间
+        'valid'  => 'tinyint',   // 状态,1启用,0禁用
+    ];
+}

+ 84 - 0
api/app/common/model/Io.php

@@ -0,0 +1,84 @@
+<?php
+
+namespace app\common\model;
+
+/**
+ * 出入库
+ * 
+ * @property string $sn 编号
+ * @property string $date 出入库日期
+ * @property int $type 类型,1入库,2出库
+ * @property int $repo_id 仓库id
+ * @property int $change_type 变更原因,1出入库,2调拔,3.盘点
+ * @property int $revert_id 回滚id
+ * @property array<IoDetail>|\think\Collection<IoDetail> $details 出入库明细
+ */
+class Io extends Base
+{
+    protected $schema = [
+        'id'     => 'int',       // id
+        'repo_id'        => 'int',       // 仓库ID
+        'create_time'    => 'datetime',  // 创建时间
+        'update_time'    => 'datetime',  // 更新时间
+        'delete_time'    => 'datetime',  // 删除时间
+        'date'   => 'date',      // 出入库日期
+        'type'   => 'tinyint',   // 类型,1入库,2出库
+        'remark'         => 'varchar',   // 备注
+        'sn'     => 'varchar',   // 订单号
+        'admin_id'       => 'int',       // 操作人ID
+        'change_type'    => 'tinyint',  // 变更原因,1出入库,2调拔,3.盘点
+        'source' => 'varchar',
+        'revert_id'      => 'int'       // 回滚id
+    ];
+
+    /**
+     * 入库
+     */
+    const TYPE_IN = 1;
+    /**
+     * 出库
+     */
+    const TYPE_OUT = 2;
+
+    
+    /**
+     * 出入库
+     */
+    const CHANGE_TYPE_IO = 1;
+    /**
+     * 调拨
+     */
+    const CHANGE_TYPE_ALLOCATION = 2;
+    /**
+     * 盘点
+     */
+    const CHANGE_TYPE_CHECK = 3;
+
+    const CHANGE_TYPE_MAP = [
+        self::CHANGE_TYPE_IO => ['text' => '出入库'],
+        self::CHANGE_TYPE_ALLOCATION => ['text' => '调拨'],
+        self::CHANGE_TYPE_CHECK => ['text' => '盘点']
+    ];
+
+
+    public function getChangeTypeTextAttr($value, $data)
+    {
+        $index = $data['change_type'];
+        return isset(self::CHANGE_TYPE_MAP[$index]) ? self::CHANGE_TYPE_MAP[$index]['text'] : '未知';
+    }
+
+    public function details()
+    {
+        return $this->hasMany(IoDetail::class);
+    }
+
+    public function repo()
+    {
+        return $this->belongsTo(Repo::class);
+    }
+
+    public function revert()
+    {
+        return $this->belongsTo(Io::class, 'revert_id', 'id');
+    }
+}

+ 94 - 0
api/app/common/model/IoDetail.php

@@ -0,0 +1,94 @@
+<?php
+
+namespace app\common\model;
+use app\common\exception\CatchException;
+use app\common\model\Good;
+use app\common\service\StockService;
+
+/**
+ * 
+ * @property int $good_id 物品ID
+ * @property int $repo_id 仓库ID
+ * @property number $num 数量,正数增加,负数减少
+ * @property int $type 类型,1入库,2出库
+ * @property string $transit_status 在途状态:"TRANSIT" 在途/借出 “COMPLETED” 完成/结束 ""/"NONE" 非在途/借出
+ * @property number $transit_received 在途到达/归还数量
+ * @property number $transit_lost 在途遗失数
+ */
+class IoDetail extends Base
+{
+    protected $schema = [
+        'id'     => 'int',       // id
+        'good_id'        => 'int',       // 物品ID
+        'repo_id'        => 'int',       // 仓库ID
+        'create_time'    => 'datetime',  // 创建时间
+        'update_time'    => 'datetime',  // 更新时间
+        'delete_time'    => 'datetime',  // 删除时间
+        'num'    => 'float',     // 数量,正数增加,负数减少
+        'date'   => 'date',      // 出入库日期
+        'type'   => 'tinyint',   // 类型,1入库,2出库
+        'remark'         => 'varchar',   // 备注
+        'io_id'  => 'int',       // 出入库订单ID
+        'transit_status'         => 'varchar',   // 在途状态:"TRANSIT" 在途/借出 “COMPLETED” 完成/结束 ""/"NONE" 非在途/借出
+        'transit_received'       => 'float',     // 在途到达/归还数量
+        'transit_lost'   => 'float',     // 在途遗失数
+    ];
+
+    /**
+     * 入库
+     */
+    const TYPE_IN = 1;
+    /**
+     * 出库
+     */
+    const TYPE_OUT = 2;
+
+    /**
+     * 非在途/借出
+     */
+    const TRANSIT_STATUS_NONE = "NONE";
+    /**
+     * 在途/借出
+     */
+    const TRANSIT_STATUS_TRANSIT = 'TRANSIT';
+
+    /**
+     * 完成/结束
+     */
+    const TRANSIT_STATUS_COMPLETED = 'COMPLETED';
+
+    const TRANSIT_STATUS_MAP = [
+        self::TRANSIT_STATUS_NONE => ['text' => '非在途/借出'],
+        self::TRANSIT_STATUS_TRANSIT => ['text' => '在途/借出'],
+        self::TRANSIT_STATUS_COMPLETED => ['text' => '非在途/借出']
+    ];
+
+    public function io()
+    {
+        return $this->belongsTo(Io::class);
+    }
+
+    public function repo()
+    {
+        return $this->belongsTo(Repo::class);
+    }
+
+    public function good()
+    {
+        return $this->belongsTo(Good::class);
+    }
+
+    public function getTransitStatusTextAttr($value, $data)
+    {
+        return self::TRANSIT_STATUS_MAP[$data['transit_status']]['text'];
+    }
+
+    public static function onAfterInsert($detail)
+    {
+        static $service;
+        if (!$service) {
+            $service = (new StockService(app()))->exceptionClass(CatchException::class);
+        }
+        $service->chengeStockTrans($detail->repo, $detail->good, $detail->num);
+    }
+}

+ 33 - 0
api/app/common/model/Repo.php

@@ -0,0 +1,33 @@
+<?php
+
+namespace app\common\model;
+
+class Repo extends Base
+{
+    protected $schema = [
+        'id'     => 'int',       // id
+        'name'   => 'varchar',   // 仓库名称
+        'desc'   => 'varchar',   // 仓库说明
+        'address'        => 'varchar',   // 仓库地址
+        'create_time'    => 'datetime',  // 创建时间
+        'update_time'    => 'datetime',  // 更新时间
+        'delete_time'    => 'datetime',  // 删除时间
+        'valid'  => 'tinyint',   // 状态,1启用,0禁用
+        'parent' => 'int',  // 父仓库id
+    ];
+
+    public function logs()
+    {
+        return $this->hasMany(Repo::class);
+    }
+
+    public function parent()
+    {
+        return $this->belongsTo(Repo::class, 'id', 'parent');
+    }
+
+    public function children()
+    {
+        return $this->hasMany(Repo::class, 'parent', 'id');
+    }
+}

+ 38 - 0
api/app/common/model/Role.php

@@ -0,0 +1,38 @@
+<?php
+
+namespace app\common\model;
+
+/**
+ * @property array $codes
+ */
+class Role extends Base
+{
+    /**
+     * 获取分页列表
+     * @param $keyword
+     * @param $valid
+     * @param $listRow
+     * @param $field
+     * @return array|mixed
+     * @throws \think\db\exception\DbException
+     */
+    public static function getList($keyword = "", $valid = -1, $listRow = 20, $field = "*")
+    {
+        $where = [];
+        if ($keyword) {
+            $where[] = ["name", "like", "%" . $keyword . "%"];
+        }
+        if ($valid != -1) {
+            $where[] = ["valid", "=", $valid];
+        }
+
+        $list = Role::field($field)->where($where)->paginate($listRow);
+        return returnFormat(0, '', $list);
+    }
+
+    public function getCodesAttr($value, $data)
+    {
+        return explode(',', $data['codes']);
+    }
+
+}

+ 30 - 0
api/app/common/model/Stock.php

@@ -0,0 +1,30 @@
+<?php
+
+namespace app\common\model;
+
+/**
+ * @property number $num
+ */
+class Stock extends Base
+{
+    protected $schema = [
+        'id'     => 'int',       // id
+        'repo_id'        => 'int',       // 仓库ID
+        'good_id'        => 'int',       // 物品ID
+        'create_time'    => 'datetime',  // 创建时间
+        'update_time'    => 'datetime',  // 更新时间
+        'delete_time'    => 'datetime',  // 删除时间
+        'valid'  => 'tinyint',   // 状态,1启用,0禁用
+        'num'    => 'float',     // 数量
+    ];
+
+    public function repo()
+    {
+        return $this->belongsTo(Repo::class);
+    }
+
+    public function good()
+    {
+        return $this->belongsTo(Good::class);
+    }
+}

+ 30 - 0
api/app/common/model/Transit.php

@@ -0,0 +1,30 @@
+<?php
+
+namespace app\common\model;
+
+class Transit extends Base
+{
+    protected $schema = [
+        'id'     => 'int',       // id
+        'create_time'    => 'datetime',  // 创建时间
+        'update_time'    => 'datetime',  // 更新时间
+        'delete_time'    => 'datetime',  // 删除时间
+        'valid'  => 'tinyint',   // 状态,1启用,0禁用
+        'date'   => 'date',      // 出入库日期
+        'type'   => 'tinyint',   // 类型,1入库,2出库
+        'remark'         => 'varchar',   // 备注
+        'sn'     => 'varchar',   // 订单号
+        'admin_id'       => 'int',       // 操作人ID
+        'status'         => 'varchar',   // 状态值:IN_TRANSIT 在途(入库) ARRIVED 到达 LEND 借出 LOST 遗失 COMPENSATED 赔偿
+        'repo_id'        => 'int',       // 仓库id
+        'from'   => 'varchar',   // 来自?
+        'to'     => 'varchar',   // 前往?
+        'contact_name'   => 'varchar',   // 联系人
+        'contact_phone'  => 'varchar',   // 联系电话
+    ];
+
+    public function details()
+    {
+        return $this->hasMany(TransitDetail::class);
+    }
+}

+ 26 - 0
api/app/common/model/TransitDetail.php

@@ -0,0 +1,26 @@
+<?php
+
+namespace app\common\model;
+
+class TransitDetail extends Base
+{
+    protected $schema = [
+        'id'     => 'int',       // id
+        'good_id'        => 'int',       // 物品ID
+        'repo_id'        => 'int',       // 仓库ID
+        'create_time'    => 'datetime',  // 创建时间
+        'update_time'    => 'datetime',  // 更新时间
+        'delete_time'    => 'datetime',  // 删除时间
+        'valid'  => 'tinyint',   // 状态,1启用,0禁用
+        'num'    => 'float',     // 数量,正数增加,负数减少
+        'date'   => 'date',      // 出入库日期
+        'type'   => 'tinyint',   // 类型,1入库,2出库
+        'remark'         => 'varchar',   // 备注
+        'transit_id'     => 'int',       // 出入库订单ID
+    ];
+
+    public function transit()
+    {
+        return $this->belongsTo(Transit::class);
+    }
+}

+ 8 - 0
api/app/common/model/User.php

@@ -0,0 +1,8 @@
+<?php
+
+namespace app\common\model;
+
+class User extends Base
+{
+    
+}

+ 245 - 0
api/app/common/service/AllocationService.php

@@ -0,0 +1,245 @@
+<?php
+
+namespace app\common\service;
+
+use Carbon\Carbon;
+use think\Validate;
+use think\facade\Db;
+use app\common\model\Io;
+use app\common\model\Good;
+use app\common\model\Admin;
+use app\common\model\IoDetail;
+use app\common\model\Allocation;
+use app\common\util\WhereBuilder;
+use app\common\model\AllocationDetail;
+
+class AllocationService extends Service
+{
+    public function page($params = [])
+    {
+        // 自动处理传入的参数
+        $this->autoParams($params);
+
+        // 获取关键词
+        $keyword = $this->pg('keyword');
+
+        // 获取来源仓库ID
+        $from_repo_id = $this->pg('from_repo_id');
+
+        // 获取目标仓库ID
+        $to_repo_id = $this->pg('to_repo_id');
+
+        // 获取开始日期
+        $begin_date = $this->pg('begin_date');
+
+        // 获取结束日期
+        $end_date = $this->pg('end_date');
+
+        // 如果结束日期存在,则将其解析为 Carbon 对象并增加一天,并转换为日期字符串
+        if ($end_date) {
+            $end_date = Carbon::parse($end_date)->addDay()->toDateString();
+        }
+
+        // 构建查询条件
+        $where = WhereBuilder::builder()
+            ->like('a.id|from.name|to.name|a.remark', $keyword)
+            ->in('a.from_repo_id', $from_repo_id)
+            ->in('a.to_repo_id', $to_repo_id)
+            ->between('a.date', $begin_date, $end_date)
+            ->build();
+
+        // 执行查询,并返回分页结果
+        return (new Allocation)->alias('a')
+            ->field('a.*, from.name as from_repo_name, to.name as to_repo_name')
+            ->join('repo from', 'from.id = a.from_repo_id', 'LEFT')
+            ->join('repo to', 'to.id = a.to_repo_id', 'LEFT')
+            ->where($where)
+            ->order('a.create_time desc')
+            ->paginate($this->tp6Page());
+    }
+
+
+    public function info($params = [])
+    {
+        $this->autoParams($params);
+
+        $alloc = $this->one(Allocation::class);
+        $alloc->append(['from', 'to', 'details', 'details.good', 'details.good.goodClass']);
+
+        return $alloc;
+    }
+
+    /**
+     * 创建调拨单(事务处理)
+     *
+     * @param Admin $admin 管理员对象
+     * @param array $params 参数数组
+     * @return Allocation 创建的调拨单对象
+     */
+    public function createTrans(Admin $admin, $params = [])
+    {
+        // 使用事务处理创建调拨单
+        return Db::transaction(fn() => $this->create($admin, $params));
+    }
+
+    /**
+     * 创建调拨单
+     *
+     * @param Admin $admin 管理员对象
+     * @param array $params 参数数组
+     * @return Allocation 创建的调拨单对象
+     */
+    public function create(Admin $admin, $params = [])
+    {
+        // 自动处理参数
+        $params = $this->autoParams($params);
+
+        // 验证参数
+        $this->validate($params, new AllocateValidate);
+
+        // 获取调拨明细
+        $details = $this->pg('details');
+
+        // 设置管理员ID
+        $params['admin_id'] = $admin->id;
+
+        // 创建调拨单
+        $allocation = Allocation::create($params);
+
+        // 遍历调拨明细进行赋值和检查
+        foreach ($details as &$detail) {
+            // 设置调拨单ID和日期
+            $detail['allocation_id'] = $allocation->id;
+            $detail['date'] = isset($detail['date']) && $detail['date'] ? $detail['date'] : $allocation->date;
+
+            // 检查出入库数量
+            if ($detail['num'] == 0) {
+                $good = Good::find($detail['good_id']);
+                throw $this->exception("物品--{$good?->name}的出入库数量不能为0");
+            }
+            if ($detail['num'] > 0 && $allocation->type == Io::TYPE_OUT) {
+                $good = Good::find($detail['good_id']);
+                throw $this->exception("物品--{$good?->name}的出入库数量大于0但是类型为出库");
+            }
+            if ($detail['num'] < 0 && $allocation->type == Io::TYPE_IN) {
+                $good = Good::find($detail['good_id']);
+                throw $this->exception("物品--{$good?->name}的出入库数量小于0但是类型为入库");
+            }
+        }
+
+        // 保存调拨明细
+        $allocationDetails = (new AllocationDetail)->saveAll($details);
+
+        // 出库
+        $outIoData = [
+            'repo_id' => $allocation->from_repo_id,
+            'type' => Io::TYPE_OUT,
+            'change_type' => Io::CHANGE_TYPE_ALLOCATION,
+            'date' => $allocation->date,
+            'remark' => "自动生成自调拨单{$allocation->id}",
+            'sn' => "SYS_ALC_OUT_T" . hrtime(true) . '_SN' . rand(100000, 999999),
+            'admin_id' => $admin->id,
+            'source' => "调拨单{$allocation->id}"
+        ];
+        $outIo = Io::create($outIoData);
+
+        // 出库详情
+        foreach ($details as &$detail) {
+            $detail['type'] = Io::TYPE_OUT;
+            $detail['io_id'] = $outIo->id;
+            $detail['repo_id'] = $allocation->from_repo_id;
+            $detail['num'] = -$detail['num'];
+        }
+        $outDetails = (new IoDetail)->saveAll($details);
+
+        // 入库
+        $inIoData = [
+            'repo_id' => $allocation->to_repo_id,
+            'type' => Io::TYPE_IN,
+            'change_type' => Io::CHANGE_TYPE_ALLOCATION,
+            'date' => $allocation->date,
+            'remark' => "自动生成自调拨单{$allocation->id}",
+            'sn' => "SYS_ALC_IN_T" . hrtime(true) . '_SN' . rand(100000, 999999),
+            'admin_id' => $admin->id,
+            'source' => "调拨单{$allocation->id}"
+        ];
+        $inIo = Io::create($inIoData);
+
+        // 入库详情
+        foreach ($details as &$detail) {
+            $detail['type'] = Io::TYPE_IN;
+            $detail['io_id'] = $inIo->id;
+            $detail['repo_id'] = $allocation->to_repo_id;
+            $detail['num'] = -$detail['num'];
+        }
+        $inDetails = (new IoDetail)->saveAll($details);
+
+        $allocation->out_io_id = $outIo->id;
+        $allocation->in_io_id = $inIo->id;
+        $allocation->save();
+
+        // 返回创建的调拨单对象
+        return $allocation;
+    }
+
+    public function revertTrans(Admin $admin, $params = [])
+    {
+        return Db::transaction(fn() => $this->revert($admin, $params));
+    }
+    
+    public function revert(Admin $admin, $params = [])
+    {
+        $this->autoParams($params);
+        $allc = $this->one(Allocation::class);
+        if ($allc->revert_id) {
+            return true;
+        }
+        $service = (new IoService($this->app))->exceptionClass($this->exceptionClass);
+        $out_revert_to_in_io = $service->revertTrans($admin, ['id' => $allc->out_io_id]);
+        $in_revert_to_out_io = $service->revertTrans($admin, ['id' => $allc->in_io_id]);
+        // 创建调拨单
+        $revertData = [
+            'from_repo_id' => $allc->to_repo_id,
+            'to_repo_id' => $allc->from_repo_id,
+            'date' => date('Ymd'),
+            'remark' => "回滚自$allc->id",
+            'admin_id' => $admin->id,
+            'out_io_id' => $in_revert_to_out_io->id,
+            'in_io_id' => $out_revert_to_in_io->id
+        ];
+        $revert = Allocation::create($revertData);
+
+        $revertDetails = [];
+        // 遍历调拨明细进行赋值和检查
+        foreach ($allc->details as $detail) {
+            // 设置调拨单ID和日期
+            $revertDetail = [
+                'good_id' => $detail->good_id,
+                'num' => $detail->num,
+                'date' => date('Ymd'),
+                'remark' => '回滚',
+                'allocation_id' => $revert->id
+            ];
+            $revertDetails[] = $revertDetail;
+        }
+
+        // 保存调拨明细
+        $allocationDetails = (new AllocationDetail)->saveAll($revertDetails);
+
+        $allc->revert_id = $revert->id;
+        $allc->save();
+        return $revert;
+    }
+}
+
+
+class AllocateValidate extends Validate
+{
+    protected $rule = [
+        'from_repo_id' => 'require',
+        'to_repo_id' => 'require',
+        'date' => 'require',
+        'remark' => 'max:255',
+        'details' => 'require|array'
+    ];
+}

+ 15 - 0
api/app/common/service/CheckService.php

@@ -0,0 +1,15 @@
+<?php
+
+namespace app\common\service;
+use app\common\model\Check;
+
+class CheckService extends Service
+{
+    public function page($params = [])
+    {
+        $this->autoParams($params);
+
+        return (new Check)
+            ->paginate($this->tp6Page());
+    }
+}

+ 52 - 0
api/app/common/service/FileService.php

@@ -0,0 +1,52 @@
+<?php
+
+namespace app\common\service;
+
+use app\common\service\Service;
+use think\facade\Filesystem;
+use think\facade\Request;
+use think\facade\Validate;
+
+class FileService extends Service
+{
+    /**
+     * 上传图片
+     * 本地运行如果无法写入图片可能是没有给public目录写权限
+     * 
+     * @param string $path 上传路径,默认为'default'
+     * @return array 上传后的图片路径
+     */
+    public function uploadImage($path = 'default')
+    {
+        // 获取表单上传文件
+        $files = request()->file();
+        // 验证上传文件格式和大小
+        $this->validate($files, Validate::rule(['file' => 'fileSize:2048000|fileExt:jpeg,jpg,png,webp']));
+        $file = request()->file('file');
+        try {
+            // 保存上传文件到public目录下的$path目录中
+            $path = Filesystem::disk('public')->putFile($path, $file);
+        } catch (\Exception $e) {
+            throw $this->warpException($e, '上传图片失败:\n');
+        }
+
+        // 获取图片的url地址
+        $path = Request::domain() . getVirRootDir() . '/storage/' . $path;
+
+        return [
+            'path' => $path
+        ];
+    }
+
+    /**
+     * 获取当前网站的域名地址
+     * 
+     * @return string 域名地址
+     */
+    protected static function get_domain()
+    {
+        $sys_protocal = isset($_SERVER['SERVER_PORT']) && $_SERVER['SERVER_PORT'] == '443' ? 'https://' : 'http://';
+        return $sys_protocal . (isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : '');
+    }
+
+}

+ 79 - 0
api/app/common/service/GoodClassService.php

@@ -0,0 +1,79 @@
+<?php
+
+namespace app\common\service;
+
+use app\common\model\GoodClass;
+use app\common\util\WhereBuilder;
+
+class GoodClassService extends Service
+{
+    /**
+     * 分页
+     *
+     * @param array $params
+     */
+    public function page($params = [])
+    {
+        $this->autoParams($params);
+        $keyword = $this->pg('keyword');
+
+        $where = WhereBuilder::builder()
+            ->like('name', $keyword)
+            ->build();
+
+        return (new GoodClass)
+            ->where($where)
+            ->paginate($this->tp6Page());
+    }
+
+    public function all()
+    {
+        return (new GoodClass)->select();
+    }
+
+    /**
+     * 创建
+     *
+     * @param array $params
+     */
+    public function create($params = [])
+    {
+        $this->autoParams($params);
+        $name = $this->pg('name');
+        $desc = $this->pg('desc');
+        $class = (new GoodClass)->where('name', '=', $name)->find();
+        if ($class) {
+            throw $this->exception("名称为 $name 的类别已存在,id={$class->id}");
+        }
+        $classData = [
+            'name' => $name,
+            'desc' => $desc,
+        ];
+        $class = GoodClass::create($classData);
+        return $class;
+    }
+
+    /**
+     * 更新
+     *
+     * @param array $params
+     */
+    public function update($params = [])
+    {
+        $params = $this->autoParams($params);
+        return (new GoodClass)->allowField(['name', 'desc'])->update($params);
+    }
+
+    /**
+     * 删除
+     *
+     * @param array $params
+     */
+    public function delete($params = [])
+    {
+        $this->autoParams($params);
+        $class = $this->one(GoodClass::class);
+        return $class->delete();
+    }
+
+}

+ 286 - 0
api/app/common/service/GoodService.php

@@ -0,0 +1,286 @@
+<?php
+
+namespace app\common\service;
+
+use think\facade\Db;
+use app\common\model\Good;
+use think\facade\Validate;
+use app\common\model\Stock;
+use app\common\util\Result;
+use think\facade\Filesystem;
+use app\common\model\GoodClass;
+use app\common\service\Service;
+use app\common\util\WhereBuilder;
+use app\common\util\PhpSpreadsheetImport;
+use app\common\util\PhpSpreadsheetExportV2;
+
+class GoodService extends Service
+{
+    /**
+     * 分页查询商品列表
+     *
+     * @param array $params 参数数组
+     * @return \think\Paginator 分页对象,包含商品列表
+     */
+    public function page($params = [])
+    {
+        // 自动处理参数
+        $this->autoParams($params);
+
+        // 获取关键字和商品分类ID
+        $keyword = $this->pg('keyword');
+        $valid = $this->pgd([], 'valid');
+        $good_class_id = $this->pg('good_class_id');
+
+        // 构建查询条件
+        $where = WhereBuilder::builder()
+            ->like('no|name|desc|unit|spec', $keyword)
+            ->eq('good_class_id', $good_class_id)
+            ->in('valid', $valid)
+            ->build();
+
+        // 执行分页查询
+        $page = (new Good)
+            ->where($where)
+            ->paginate($this->tp6Page());
+
+        // 关联加载商品分类信息
+        $page->getCollection()->append(['goodClass']);
+
+        // 返回分页对象
+        return $page;
+    }
+
+
+    /**
+     * 创建商品(事务处理)
+     *
+     * @param array $params 参数数组
+     * @return Good 创建的商品对象
+     */
+    public function createTrans($params = [])
+    {
+        // 使用事务处理创建商品
+        return Db::transaction(fn() => $this->create($params));
+    }
+
+    /**
+     * 创建商品
+     *
+     * @param array $params 参数数组
+     * @return Good 创建的商品对象
+     * @throws \Exception 如果商品编号已存在,则抛出异常
+     */
+    public function create($params = [])
+    {
+        // 自动处理参数
+        $params = $this->autoParams($params);
+
+        // 获取商品编号
+        $no = $this->pg('no');
+
+        // 获取商品分类对象
+        $goodClass = $this->one(GoodClass::class, 'good_class_id');
+
+        // 根据编号查找商品
+        $good = Good::getByNo($no);
+        if ($good) {
+            throw $this->exception("编号 $no 已存在");
+        }
+
+        // 创建商品
+        return Good::create($params);
+    }
+
+
+    /**
+     * 更新商品信息
+     *
+     * @param array $params 参数数组
+     * @return array 更新成功的商品对象数组
+     * @throws \Exception 如果未选中任何商品进行更新,则抛出异常
+     */
+    public function update($params = [])
+    {
+        // 自动处理参数
+        $params = $this->autoParams($params);
+
+        if (!$params) {
+            throw $this->exception('未选中任何商品进行更新');
+        }
+
+        // 更新商品信息并返回更新成功的商品对象数组
+        return (new Good)->allowField(['name', 'unit', 'img', 'valid', 'spec', 'good_class_id'])->saveAll($params);
+    }
+
+    /**
+     * 删除商品
+     *
+     * @param array $params 参数数组
+     * @return int 删除的商品数量
+     * @throws \Exception 如果非强制删除商品时,存在库存未处理,则抛出异常
+     */
+    public function delete($params = [])
+    {
+        // 自动处理参数
+        $this->autoParams($params);
+
+        // 获取商品ID和是否强制删除标志
+        $id = $this->req('id');
+        $force = $this->pgd(false, 'force');
+
+        if (is_int($id)) {
+            $id = [$id];
+        }
+
+        // 非强制删除时,检查仓库库存
+        if (!$force) {
+            foreach ($id as $i) {
+                $num = (new Stock)->where('good_id', '=', $i)->sum('num');
+                if ($num > 0) {
+                    $good = (new Good)->find($id);
+                    throw $this->exception("商品{$good->name}仍有库存未处理,请使用调拨/出库清理库存", 1);
+                }
+            }
+        }
+
+        // 删除商品并返回删除的商品数量
+        return Good::destroy($id);
+    }
+
+
+    /**
+     * 导入商品数据
+     *
+     * @return \think\Response 导入结果的响应对象
+     * @throws \think\Exception\ValidateException 如果上传文件格式或大小不符合要求,则抛出验证异常
+     * @throws \think\Exception 如果上传图片失败,则抛出异常
+     */
+    public function import()
+    {
+        // 获取表单上传文件
+        $files = request()->file();
+
+        // 验证上传文件格式和大小
+        $this->validate($files, Validate::rule(['file' => 'fileSize:20480000|fileExt:csv,xlsx']));
+        $file = request()->file('file');
+
+        try {
+            $path = Filesystem::putFile('execl', $file);
+        } catch (\Exception $e) {
+            throw $this->warpException($e, '上传图片失败:\n');
+        }
+
+        // 读取导入的数据
+        $data = PhpSpreadsheetImport::readData(runtime_path('../storage') . $path);
+
+        static $MAP = [
+        'A' => 'no',
+        'B' => 'name',
+        'C' => 'desc',
+        'D' => 'unit',
+        'E' => 'img',
+        'F' => 'spec',
+        'G' => 'good_class_id'
+        ];
+
+        /**
+         * @var GoodClassService $service 
+         */
+        static $service;
+
+        if (!$service) {
+            $service = (new GoodClassService($this->app))->exceptionClass($this->exceptionClass);
+        }
+
+        $goods = [];
+
+        // 处理导入的每一行数据
+        foreach ($data as $row) {
+            $good = [];
+            foreach ($row as $key => $value) {
+                $newKey = $MAP[$key];
+
+                // 如果是分类字段,则替换为对应的ID,如果分类不存在则创建
+                if ($newKey == 'good_class_id') {
+                    $class = (new GoodClass)->cache()->where('name', '=', $value)->find();
+                    if (!$class) {
+                        $class = $service->create(['name' => $value]);
+                    }
+                    $value = $class->id;
+                }
+
+                $good[$newKey] = $value;
+            }
+            $goods[] = $good;
+        }
+
+        // 批量保存商品数据
+        $goods = (new Good)->saveAll($goods);
+
+        return Result::rest(true, 0, "成功{$goods->count()}个");
+    }
+
+
+
+    /**
+     * 导出商品数据
+     *
+     * @param array $params 导出参数
+     * @return mixed 导出结果
+     */
+    public function export($params = [])
+    {
+        $this->autoParams($params);
+        $keyword = $this->pg('name');
+        $repo_id = $this->pg('repo_id');
+
+        // 构建查询条件
+        $where = WhereBuilder::builder()
+            ->like('name', $keyword)
+            ->in('repo_id', $repo_id)
+            ->build();
+
+        $header = [
+            [
+                'id',
+                '序号',
+                '名称',
+                '介绍',
+                '单位',
+                '图片地址',
+                '创建时间',
+                '更新时间',
+                '删除时间',
+                '状态',
+                '规格',
+                '类别id',
+                '类别名称'
+            ]
+        ];
+
+        // 查询商品数据,并关联查询商品类别信息
+        $goods = (new Good)
+            ->field('g.*, c.name as class_name')
+            ->alias('g')
+            ->join('good_class c', 'c.id = g.good_class_id', 'LEFT')
+            ->where($where)
+            ->select()
+            ->map(fn(Good $good) => $good->getData())
+            ->each(function (array $good) {
+                // 格式化商品状态字段
+                $good['valid'] = $good['valid'] ? '启用' : '禁用';
+                return $good;
+            });
+
+        // 导出商品数据
+        $res = PhpSpreadsheetExportV2::outputFile(
+            $goods,
+            $header,
+            'good' . date('Ymdis'),
+            []
+        );
+
+        return $res;
+    }
+}

+ 51 - 0
api/app/common/service/IoDetailService.php

@@ -0,0 +1,51 @@
+<?php
+
+namespace app\common\service;
+
+use app\admin\attr\Permission;
+use app\common\model\IoDetail;
+use app\common\util\WhereBuilder;
+
+#[Permission('io_detail')]
+class IoDetailService extends Service
+{
+    public function page($params = [])
+    {
+        $params = $this->autoParams($params);
+        $repo_id = $this->pg('repo_id');
+
+        $where = WhereBuilder::builder()
+            ->eq('d.repo_id', $repo_id)
+            ->build();
+
+        return (new IoDetail)->alias('d')
+            ->field('d.*, r.name as repo_name, g.name as good_name')
+            ->join('repo r', 'r.id = d.repo_id', 'LEFT')
+            ->join('good g', 'g.id = d.good_id', 'LEFT')
+            ->where($where)
+            ->order('d.create_time desc')
+            ->paginate($this->tp6Page());
+    }
+
+    public function transits($params = [])
+    {
+        $params = $this->autoParams($params);
+
+        $transit_status = $this->pg('transit_status');
+        $repo_id = $this->pg('repo_id');
+        $io_id = $this->pg('io_id');
+
+        $where = WhereBuilder::builder()
+            ->in('d.transit_status', $transit_status)
+            ->eq('d.repo_id', $repo_id)
+            ->eq('d.io_id', $io_id)
+            ->build();
+
+        return (new IoDetail)->alias('d')
+            ->field('d.*, r.name as repo_name, g.name as good_name, g.no as good_no')
+            ->join('repo r', 'r.id = d.repo_id', 'LEFT')
+            ->join('good g', 'g.id = d.good_id', 'LEFT')
+            ->where($where)
+            ->select();
+    }
+}

+ 420 - 0
api/app/common/service/IoService.php

@@ -0,0 +1,420 @@
+<?php
+
+namespace app\common\service;
+
+use Carbon\Carbon;
+use think\Validate;
+use think\facade\Db;
+use app\common\model\Io;
+use app\common\model\Good;
+use app\common\model\Repo;
+use app\common\model\Admin;
+use app\common\model\Stock;
+use app\common\model\IoDetail;
+use app\common\util\WhereBuilder;
+use app\common\util\PhpSpreadsheetExportV2;
+
+class IoService extends Service
+{
+    /**
+     * 处理分页请求
+     * 
+     * @param array $params 请求参数数组
+     * @return mixed 分页结果
+     */
+    public function page($params = [])
+    {
+        // 自动处理请求参数
+        $this->autoParams($params);
+
+        // 从请求参数中获取相关值
+        $type = $this->pg('type');
+        $change_type = $this->pg('change_type');
+        $keyword = $this->pg('keyword');
+        $repo_id = $this->pg('repo_id');
+        $begin_date = $this->pg('begin_date');
+        $end_date = $this->pg('end_date');
+
+        // 如果存在结束日期,则向后延长一天
+        if ($end_date) {
+            $end_date = Carbon::parse($end_date)->addDay()->toDateString();
+        }
+
+        // 获取是否包含运输状态参数
+        $contain_transit = $this->pgd(false, 'contain_transit');
+
+        // 构建查询条件
+        $where = WhereBuilder::builder()
+            ->like('io.sn|r.name|io.remark', $keyword)
+            ->in('io.type', $type)
+            ->in('io.change_type', $change_type)
+            ->in('io.repo_id', $repo_id)
+            ->between('io.date', $begin_date, $end_date)
+            ->where('d.transit_status', 'in', [IoDetail::TRANSIT_STATUS_TRANSIT, IoDetail::TRANSIT_STATUS_TRANSIT], $contain_transit)
+            ->build();
+
+        // 构建查询对象
+        $query = (new Io)
+            ->alias('io')
+            ->field('io.*, r.name as repo_name')
+            ->join('repo r', 'r.id = io.repo_id');
+
+        // 如果包含运输状态,则进行关联查询
+        if ($contain_transit) {
+            $query = $query->join('io_detail d', 'd.io_id = io.id');
+        }
+
+        // 执行分页查询
+        $page = $query->where($where)
+            ->order('io.create_time desc')
+            ->paginate($this->tp6Page());
+
+        // 添加附加字段到分页结果集
+        $page->getCollection()->append(['change_type_text']);
+
+        // 返回分页结果
+        return $page;
+    }
+
+    /**
+     * 获取Io信息
+     * 
+     * @param array $params 请求参数数组
+     * @return mixed Io对象
+     */
+    public function info($params = [])
+    {
+        // 自动处理请求参数
+        $this->autoParams($params);
+
+        // 查询单个Io对象
+        $io = $this->one(Io::class);
+
+        // 添加附加字段到Io对象
+        $io->append(['change_type_text', 'details', 'repo', 'details.good']);
+
+        // 返回Io对象
+        return $io;
+    }
+
+    /**
+     * 创建运输记录事务
+     * 
+     * @param Admin $admin 管理员对象
+     * @param array $params 请求参数数组
+     * @return mixed 运输记录创建结果
+     */
+    public function createTrans(Admin $admin, $params = [])
+    {
+        // 使用数据库事务执行创建操作
+        return Db::transaction(fn() => $this->create($admin, $params));
+    }
+
+    /**
+     * 创建运输记录
+     * 
+     * @param Admin $admin 管理员对象
+     * @param array $params 请求参数数组
+     * @return mixed 创建的Io对象
+     * @throws \Exception 创建失败时抛出异常
+     */
+    public function create(Admin $admin, $params = [])
+    {
+        // 自动处理请求参数
+        $params = $this->autoParams($params);
+
+        // 如果存在管理员对象,则将管理员ID赋值给参数数组
+        if ($admin) {
+            $params['admin_id'] = $admin->id;
+        }
+
+        // 查询Repo对象
+        $repo = $this->one(Repo::class, 'repo_id');
+
+        // 获取出入库明细
+        $details = $this->pg('details');
+        // 过滤掉数量为0的明细
+        $details = array_filter($details, fn($detail) => $detail['num']);
+
+        // 创建Io对象
+        $io = Io::create($params);
+
+        // 赋值并检查明细
+        foreach ($details as &$detail) {
+            $detail['io_id'] = $io->id;
+            $detail['date'] = isset($detail['date']) && $detail['date'] ? $detail['date'] : $io->date;
+            $detail['type'] = isset($detail['type']) && $detail['type'] ? $detail['type'] : $io->type;
+            $detail['repo_id'] = isset($detail['repo_id']) && $detail['repo_id'] ? $detail['repo_id'] : $io->repo_id;
+
+            // 检查出入库数量
+            if ($detail['num'] == 0) {
+                $good = Good::find($detail['good_id']);
+                throw $this->exception("物品--{$good?->name}的出入库数量不能为0");
+            }
+            if ($detail['num'] > 0 && $detail['type'] == Io::TYPE_OUT) {
+                $good = Good::find($detail['good_id']);
+                throw $this->exception("物品--{$good?->name}的出入库数量大于0但是类型为出库");
+            }
+            if ($detail['num'] < 0 && $detail['type'] == Io::TYPE_IN) {
+                $good = Good::find($detail['good_id']);
+                throw $this->exception("物品--{$good?->name}的出入库数量小于0但是类型为入库");
+            }
+        }
+
+        // 保存出入库明细
+        $details = (new IoDetail)->saveAll($details);
+
+        // 在oninsert事件中进行库存变更
+        $io->details = $details;
+
+        // 返回创建的Io对象
+        return $io;
+    }
+
+
+    /**
+     * 导出运输记录
+     * 
+     * @param array $params 请求参数数组
+     * @return mixed 导出结果
+     */
+    public function export($params = [])
+    {
+        // 自动处理请求参数
+        $this->autoParams($params);
+
+        $type = $this->pg('type');
+        // 获取起始日期和结束日期
+        $begin_date = $this->pg('begin_date');
+        $end_date = $this->pg('end_date');
+
+        // 如果存在结束日期,则向后延长一天
+        if ($end_date) {
+            $end_date = Carbon::parse($end_date)->addDay()->toDateString();
+        }
+
+        // 构建查询条件
+        $where = WhereBuilder::builder()
+            ->in('io.type', $type)
+            ->between('i.date', $begin_date, $end_date)
+            ->build();
+
+        // 导出表格的表头
+        $header = [
+            [
+                '编号',
+                '仓库名',
+                '出入库类型',
+                '变更原因',
+                '备注',
+                '来源',
+                '创建时间',
+                '日期',
+                '物品名',
+                '数量',
+                '明细备注',
+                '在途状态',
+                '归还数量',
+                '遗失数量'
+            ]
+        ];
+
+        // 构建查询字段中的change_type_export
+        $changeTypeField = <<<SQL
+            CASE i.change_type 
+                WHEN 1 THEN "出入库" 
+                WHEN 2 THEN "调拨" 
+                WHEN 3 THEN "盘点" 
+                ELSE "出入库" 
+            END as change_type_export
+        SQL;
+
+        // 构建查询字段中的transit_status_export
+        $transitStatusField = <<<SQL
+            CASE d.transit_status
+                WHEN "TRANSIT" THEN "在途"
+                WHEN "COMPLETED" THEN "完成"
+                ELSE ""
+            END as transit_status_export
+        SQL;
+
+        // 执行查询
+        $details = (new IoDetail)->alias('d')
+            ->field('i.sn, r.name as repo_name, if(i.type = 1, "入库", "出库")')
+            ->field($changeTypeField)
+            ->field('i.remark, i.source, i.create_time, i.date')
+            ->field('g.name as good_name, d.num, d.remark as detail_remark')
+            ->field($transitStatusField)
+            ->field('d.transit_received, d.transit_lost')
+            ->join('io i', 'i.id = d.io_id', 'LEFT')
+            ->join('repo r', 'r.id = i.repo_id', 'LEFT')
+            ->join('good g', 'g.id = d.good_id', 'LEFT')
+            ->where($where)
+            ->select()
+            ->toArray();
+
+        // 导出表格文件
+        $res = PhpSpreadsheetExportV2::outputFile(
+            $details,
+            $header,
+            'io' . date('Ymdis'),
+            []
+        );
+
+        // 返回导出结果
+        return $res;
+    }
+
+
+    public function completeTrans(Admin $admin, $params = [])
+    {
+        return Db::transaction(fn() => $this->complete($admin, $params));
+    }
+
+    public function complete(Admin $admin, $params = [])
+    {
+        $params = $this->autoParams($params);
+        $this->validate($params, new CompleteValidate);
+        $transits = $this->pg('transits');
+        foreach ($transits as &$transit) {
+            /**
+             * @var IoDetail
+             */
+            $detail = IoDetail::find($transit['id']);
+            $stock = (new Stock)
+                ->where('repo_id', '=', $detail->repo_id)
+                ->where('good_id', '=', $detail->good_id)
+                ->find();
+
+            // 检查数量
+            $num = abs($detail->num);
+            if ($transit['transit_received'] + $transit['transit_lost'] == $num) {
+                $transit['transit_status'] = IoDetail::TRANSIT_STATUS_COMPLETED;
+            } elseif ($transit['transit_received'] + $transit['transit_lost'] > $num) {
+                throw $this->exception("到达/归还数量{$transit['transit_received']} 加 在途遗失数{$transit['transit_lost']} 大于该出入库明细的数量:{$num}");
+            } elseif ($transit['transit_received'] < $detail->transit_received) {
+                throw $this->exception("到达/归还数量{$transit['transit_received']} 比原来还少了!(原数量:{$detail->transit_received}) 如发现归还数量错误,请使用盘点功能");
+            } elseif ($transit['transit_lost'] < $detail->transit_lost) {
+                throw $this->exception("在途遗失数量{$transit['transit_lost']} 比原来还少了!(原数量:{$detail->transit_lost}) 如发现遗失后又归还,请使用盘点功能");
+            } else {
+                $transit['transit_status'] = isset($transit['transit_status']) ? $transit['transit_status'] : IoDetail::TRANSIT_STATUS_TRANSIT;
+            }
+
+            $stock->num += self::getTypeMul($detail->type) * ($transit['transit_received'] - $detail->transit_received);
+            $stock->save();
+
+        }
+        (new IoDetail)->allowField(['transit_status', 'transit_received', 'transit_lost'])->saveAll($transits);
+        return true;
+    }
+
+    public function revertTrans(Admin $admin, $params = []) {
+        return Db::transaction(fn() => $this->revert($admin, $params));
+    }
+
+    protected function revert(Admin $admin, $params = [])
+    {
+        $this->autoParams($params);
+        $io = $this->one(Io::class);
+        if ($io->revert_id) {
+            return true;
+        }
+        $revertData = [
+            'repo_id' => $io->repo_id,
+            'date' => date('Ymd'),
+            'type' => self::inverseIoType($io['type']),
+            'sn' => 'REVERT_T' . hrtime(true) . '_SN' . rand(100000, 999999),
+            'remark' => "回滚自$io->sn",
+            'admin_id' => $admin->id,
+            'change_type' => $io->change_type,
+            'source' => "$io->sn"
+        ];
+        $revert = Io::create($revertData);
+
+        $revertDetails = [];
+        foreach ($io->details as $detail) {
+            $revertDetail = [
+                'repo_id' => $detail->repo_id,
+                'good_id' => $detail->good_id,
+                'num' => -$detail['num'],
+                'date' => date('Ymd'),
+                'type' => self::inverseIoType($detail['type']),
+                'remark' => '回滚',
+                'io_id' => $revert->id,
+            ];
+            $revertDetails[] = $revertDetail;
+        }
+        $details = (new IoDetail)->saveAll($revertDetails);
+
+        $io->revert_id = $revert->id;
+        $io->save();
+        return $revert;
+    }
+
+    /**
+     * 翻转出入库类型
+     *
+     * @param int $type 出入库类型
+     * @return int 翻转后的出入库类型
+     */
+    protected static function inverseIoType(int $type)
+    {
+        return $type == Io::TYPE_IN ? Io::TYPE_OUT : Io::TYPE_IN;
+    }
+
+    /**
+     * 获取出入库类型的乘数
+     *
+     * @param int $type 出入库类型
+     * @return int 出入库类型的乘数
+     */
+    protected static function getTypeMul(int $type)
+    {
+        return $type == Io::TYPE_IN ? 1 : -1;
+    }
+
+}
+
+class CreateIoValidate extends Validate
+{
+    protected $rule = [
+        'repo_id|仓库' => 'require',
+        'valid|状态' => 'require',
+        'type|出入库类型' => 'require',
+        'change_type|变更类型' => 'require',
+        'details|出入库内容' => 'checkDetails',
+    ];
+
+    protected function checkDetails($details)
+    {
+        if (!is_array($details)) {
+            return 'details必须为数组';
+        }
+        if (!$details) {
+            return '出入库内容不能为空';
+        }
+        $setAndNotEmpty = fn($item, $index) => isset($item[$index]) && $item[$index];
+        $requires = ['good_id', 'num', 'type', 'remark'];
+        foreach ($details as $index => $detail) {
+            foreach ($requires as $require) {
+                if (!$setAndNotEmpty($detail, $require)) {
+                    return "details[$index].$require 不能为空";
+                }
+            }
+            $good_id = $detail['good_id'];
+            $good = Good::find($good_id);
+            if (!$good) {
+                return "未找到物品对象, id={$good_id}";
+            }
+        }
+
+        return true;
+    }
+}
+
+class CompleteValidate extends Validate
+{
+    protected $rule = [
+        'transits' => 'array'
+    ];
+}

+ 75 - 0
api/app/common/service/RepoService.php

@@ -0,0 +1,75 @@
+<?php
+
+namespace app\common\service;
+use app\common\model\Repo;
+use app\common\model\Stock;
+use app\common\util\WhereBuilder;
+use think\facade\Validate;
+
+class RepoService extends Service
+{
+    public function page($params = [])
+    {
+        $this->autoParams($params);
+
+        $keyword = $this->pg('keyword');
+        $valid = $this->pg('valid');
+
+        $where = WhereBuilder::builder()
+            ->like('name|address', $keyword)
+            ->eq('valid', $valid)
+            ->build();
+
+        $page = (new Repo)
+            ->where($where)
+            ->paginate($this->tp6Page());
+
+        return $page;
+    }
+
+    public function all($params = [])
+    {
+        return (new Repo)->select();
+    }
+
+    public function create($params = [])
+    {
+        $params = $this->autoParams($params);   
+
+        return Repo::create($params);
+    }
+
+    public function update($params = [])
+    {
+        $params = $this->autoParams($params);
+        if (!$params) {
+            throw $this->exception('未选中任何商品进行更新');
+        }
+        return (new Repo)->allowField(['name', 'desc', 'address', 'valid'])->update($params);
+    }
+
+    public function delete($params = [])
+    {
+        $params = $this->autoParams($params);
+        $this->validate($params, Validate::rule(['id' => 'array']));
+
+        $id = $this->req('id');
+        $force = $this->pgd(false, 'force');
+
+        if (is_int($id)) {
+            $id = [$id];
+        }
+        // 非强制删除检查仓库库存
+        if (!$force) {
+            foreach ($id as $i) {
+                $sum = (new Stock)->where('repo_id', '=', $i)->sum('num');
+                if ($sum > 0) {
+                    $repo = (new Repo)->find($id);
+                    throw $this->exception("仓库{$repo->name}仍有库存未处理,请使用调拨/出库清理库存,或使用强制删除", 1);
+                }
+            }
+        }
+
+        return Repo::destroy($id);
+    }
+}

+ 264 - 0
api/app/common/service/Service.php

@@ -0,0 +1,264 @@
+<?php
+
+namespace app\common\service;
+
+use think\exception\ValidateException;
+use think\facade\Request;
+use think\Service as ThinkService;
+use think\Validate;
+
+/**
+ * 服务基类
+ */
+class Service extends ThinkService
+{
+    protected $exceptionClass = \Exception::class;
+    protected $isExceptionClassChanged = false;
+    protected $params = [];
+
+    /**
+     * 默认参数
+     *
+     * @return mixed
+     */
+    protected function params($validator = null)
+    {
+        $params = [];
+        if ($this->params) {
+            $params = $this->params;
+        } else {
+            $params = Request::instance()->param('', null, 'trim');
+        }
+        // 尝试校验
+        if ($validator) {
+            $this->validate($params, $validator);
+        }
+        return $params;
+    }
+
+    /**
+     * 使用该方法需要先调用overrideParams
+     *
+     * @param mixed $default 默认值
+     * @param string|int ...$index 数组下标
+     * 
+     * @return mixed
+     */
+    protected function pgd($default = null, ...$index)
+    {
+        $params = $this->params();
+        foreach ($index as $i) {
+            if (isset($params[$i])) {
+                return $params[$i];
+            }
+        }
+        return $default;
+    }
+
+    /**
+     * 使用该方法需要先调用overrideParams
+     *
+     * @param string|int ...$index 数组下标
+     * 
+     * @return mixed
+     */
+    protected function pg(...$index)
+    {
+        $params = $this->params();
+        foreach ($index as $i) {
+            if (isset($params[$i])) {
+                return $params[$i];
+            }
+        }
+        return null;
+    }
+
+    protected function req(...$index)
+    {
+        $params = $this->params();
+        foreach ($index as $i) {
+            if (isset($params[$i])) {
+                return $params[$i];
+            }
+        }
+        $desc = implode('或', $index);
+        throw $this->exception("缺少参数 $desc");
+    }
+
+    /**
+     * 获取主键
+     *
+     * @param string $name
+     * 
+     * @return mixed
+     */
+    protected function primaryKey($name = 'id')
+    {
+        $params = $this->params(\think\facade\Validate::rule([$name => 'require']));
+        return $params[$name];
+    }
+
+    /**
+     * 通过主键获取对象
+     *
+     * @template T
+     * @param class-string<T> $objClass
+     * @param string $name
+     * 
+     * @return T
+     */
+    protected function one(string $objClass, $name = 'id')
+    {
+        $id = $this->primaryKey($name);
+        $obj = (new $objClass)->find($id);
+        if (!$obj) {
+            throw $this->exception("未找到对象 $objClass $name=$id", 404);
+        }
+        return $obj;
+    }
+
+    /**
+     * 覆盖params
+     * - 优先级 function($params) > overrideParams($params) > autoget
+     *
+     * @param mixed $params
+     * 
+     * @return Service
+     */
+    public function overrideParams($params)
+    {
+        $this->params = $params;
+        return $this;
+    }
+
+    protected function autoParams($params)
+    {
+        $this->params = $params ?: $this->params();
+        return $this->params;
+    }
+
+    /**
+     * 分页参数
+     *
+     * @param int $max
+     * 
+     * @return array
+     */
+    public function pageParams($max = 100)
+    {
+        $this->validate($this->params, \think\facade\Validate::rule([
+            'pageParams.page' => '>=:0',
+            'pageParams.size' => "<=:{$max}"
+        ]));
+        $pageParams = $this->params['pageParams'];
+        $pageParams['size'] = isset($pageParams['size']) ? $pageParams['size'] : 15;
+        $pageParams['page'] = isset($pageParams['page']) ? $pageParams['page'] : 1;
+        return $pageParams;
+    }
+
+    public function tp6Page($max = 100)
+    {
+        ['page' => $page, 'size' => $size] = $this->pageParams($max);
+        return [
+            'page' => $page,
+            'list_rows' => $size,
+        ];
+    }
+
+    /**
+     * 校验
+     *
+     * @param mixed $data
+     * @param Validate $validator
+     * @param array $msg
+     * 
+     * @return bool
+     */
+    public function validate($data, Validate $validator)
+    {
+        if (!$validator->check($data)) {
+            throw new ValidateException($validator->getError());
+        }
+        return true;
+    }
+
+    /**
+     * 设置或获取内置异常类
+     *
+     * @param string $class 异常类
+     * 
+     * @return self|string
+     */
+    public function exceptionClass($class = null)
+    {
+        if (empty($class)) {
+            return $this->exceptionClass;
+        }
+        if ($this->isExceptionClassChanged) {
+            return $this;
+        }
+        $this->exceptionClass = $class;
+        return $this;
+    }
+
+    /**
+     * 锁定异常类
+     *
+     * @return self
+     */
+    public function lockException()
+    {
+        $this->isExceptionClassChanged = true;
+        return $this;
+    }
+
+    /**
+     * 封装异常
+     *
+     * @param \Exception $e 父异常
+     * @param string $prefixMessage 消息前缀
+     * @param  $newCode 新的code
+     * @param string $class 如有需要使用另一个class
+     * 
+     * @return \Exception
+     */
+    public function warpException(\Exception $e, $prefixMessage = "发生异常:\n ", $newCode = null, $class = null)
+    {
+        $class = $class ?: $this->exceptionClass;
+        $newCode = $newCode ?? $e->getCode();
+        return new $class("{$prefixMessage}[{$e->getCode()}]:" . $e->getMessage(), $newCode, $e);
+    }
+
+    /**
+     * 附带新异常
+     *
+     * @param \Closure $func 函数
+     * @param mixed ...$args 参数
+     * 
+     * @return void
+     */
+    public function withException(\Closure $func, ...$args)
+    {
+        try {
+            $func(...$args);
+        } catch (\Exception $e) {
+            throw $this->warpException($e);
+        }
+    }
+
+    /**
+     * 使用内定的异常类
+     *
+     * @param string $message 消息
+     * @param $code code
+     * @param \Exception $parent 父异常
+     * @param string $class 如有需要使用另一个class
+     *  
+     * @return \Exception
+     */
+    public function exception($message = '', $code = -1, $parent = null, $class = null)
+    {
+        $class = $class ?: $this->exceptionClass;
+        return new $class($message, $code, $parent);
+    }
+}

+ 164 - 0
api/app/common/service/StockService.php

@@ -0,0 +1,164 @@
+<?php
+
+namespace app\common\service;
+
+use app\common\util\Result;
+use think\annotation\route\Group;
+use think\facade\Db;
+use app\common\model\Good;
+use app\common\model\Repo;
+use app\common\model\Stock;
+use app\common\util\WhereBuilder;
+use app\common\util\PhpSpreadsheetExportV2;
+
+class StockService extends Service
+{
+    public function page($params = [])
+    {
+        $this->autoParams($params);
+        $keyword = $this->pg('keyword');
+        $repo_id = $this->pg('repo_id');
+        $good_id = $this->pg('good_id');
+        $filter_zero_transit = $this->pgd(false, 'filter_zero_transit');
+        if ($repo_id && !Repo::find($repo_id)) {
+            throw $this->exception('仓库不存在');
+        }
+        if ($good_id && !Good::find($good_id)) {
+            throw $this->exception('物品不存在');
+        }
+
+        $where = WhereBuilder::builder()
+            ->like('g.no|g.name|r.name', $keyword)
+            ->in('s.repo_id', $repo_id)
+            ->build();
+
+        $transit_filter_symbol = '>=';
+        if ($filter_zero_transit) {
+            $transit_filter_symbol = '>';
+        }
+
+        $page = (new Stock)
+            ->alias('s')
+            ->field('s.*, g.name as good_name, g.no as good_no, r.name as repo_name, IFNULL(sum(abs(d.num) - d.transit_received), 0) as sum_transit')
+            ->join('good g', 'g.id = s.good_id', 'LEFT')
+            ->join('repo r', 'r.id = s.repo_id', 'LEFT')
+            ->join('io_detail d', 'd.good_id = s.good_id AND d.repo_id = s.repo_id AND d.transit_status = "TRANSIT"', 'LEFT')
+            ->where($where)
+            ->whereRaw("IFNULL(abs(d.num) - d.transit_received, 0) $transit_filter_symbol 0")
+            ->group('s.id')
+            ->paginate($this->tp6Page(PHP_INT_MAX));
+
+        return $page;
+    }
+
+    public function allByRepo($params = [])
+    {
+        $this->autoParams($params);
+
+        $repo = $this->one(Repo::class);
+
+        return (new Stock)->where('repo_id', '=', $repo->id)->select();
+    }
+
+    public function info($params = [])
+    {
+        $this->autoParams($params);
+
+        $stock = $this->one(Stock::class);
+        $stock->append(['good']);
+
+        return $stock;
+    }
+
+    public function getByGoodAndRepo($params = [])
+    {
+        $this->autoParams($params);
+        $good = $this->one(Good::class, 'good_id');
+        $repo = $this->one(Repo::class, 'repo_id');
+        return (new Stock)
+            ->with(['good'])
+            ->where('good_id', '=', $good->id)
+            ->where('repo_id', '=', $repo->id)
+            ->find()
+            ?? Result::rest(['repo_id' => $repo->id, 'good_id' => $good->id, 'num' => 0]);
+    }
+
+    /**
+     * tp6是支持事务嵌套的,为了保证修改库存时数据不出现问题,请无脑使用该事务方法
+     */
+    public function chengeStockTrans(Repo $repo, Good $good, $change)
+    {
+        // tp6是支持事务嵌套的,所有修改可以直接用事务
+        return Db::transaction(fn() => $this->changeStock($repo, $good, $change));
+    }
+
+    protected function changeStock(Repo $repo, Good $good, $change)
+    {
+        if (!$repo) {
+            throw $this->exception('致命错误,$repo为空');
+        }
+        if (!$good) {
+            throw $this->exception('致命错误,$good为空');
+        }
+        /**
+         * @var Stock
+         */
+        $stock = (new Stock)
+            ->lock(true)
+            ->where('good_id', '=', $good->id)
+            ->where('repo_id', '=', $repo->id)
+            ->find();
+        if (!$stock) {
+            $stock = Stock::create([
+                'repo_id' => $repo->id,
+                'good_id' => $good->id,
+                'num' => 0
+            ]);
+        }
+        if ($change < 0 && $stock->num < abs($change)) {
+            $info = <<<EOF
+            物品{$good->name}库存不足,当前库存为{$stock->num},出库数量为{$change}
+            出现这种情况可能有以下原因:
+            - 在该操作前,该物品已由另一名操作员出库,导致库存不足
+            - 多个相同商品出库相加库存不足
+            - 库存不足
+            - 库存低于0.01,此时系统会认为没有库存,请避免此类情况的发生
+            - 出库数量/库存数量过高导致溢出,如出现此类情况请联系开发人员解决
+            EOF;
+            throw $this->exception($info);
+        }
+        $stock->num += $change;
+        $stock->save();
+    }
+
+    public function export($params = [])
+    {
+        $params = $this->autoParams($params);
+        $params['pageParams']['size'] = PHP_INT_MAX;
+        $stocks = $this->page($params)->getCollection();
+        $stocks = $stocks->map(fn(Stock $stock) => $stock->getData());
+        $header = [
+            [
+                'id',
+                '仓库id',
+                '商品id',
+                '创建时间',
+                '更新时间',
+                '删除时间',
+                '数量',
+                '商品名',
+                '编号',
+                '仓库名',
+                '在途数量'
+            ]
+        ];
+        $res = PhpSpreadsheetExportV2::outputFile(
+            $stocks,
+            $header,
+            '仓储管理系统' . date('Ymdis'),
+            []
+        );
+        return $res;
+    }
+
+}

+ 49 - 0
api/app/common/util/AliSms.php

@@ -0,0 +1,49 @@
+<?php
+namespace app\common\util;
+
+use AlibabaCloud\Client\AlibabaCloud;
+use AlibabaCloud\Client\Exception\ClientException;
+use AlibabaCloud\Client\Exception\ServerException;
+use think\facade\Log;
+
+class AliSms
+{
+    /**
+     * 发送短信验证码
+     * @param $phone
+     * @param $code
+     * @throws ClientException
+     */
+    public function sendMessageCode($phone, $code){
+        Log::record("sendMessageCode begin:".$phone.",code:".$code,"debug");
+        AlibabaCloud::accessKeyClient(config('ali_msm.accessKeyId'), config('ali_msm.accessKeySecret'))
+            ->regionId('cn-hangzhou')
+            ->asDefaultClient();
+
+        try {
+            $result = AlibabaCloud::rpc()
+                ->product('Dysmsapi')
+                // ->scheme('https') // https | http
+                ->version('2017-05-25')
+                ->action('SendSms')
+                ->method('POST')
+                ->host('dysmsapi.aliyuncs.com')
+                ->options([
+                    'query' => [
+                        'RegionId' => config('message.RegionId'),
+                        'PhoneNumbers' => $phone,
+                        'SignName' => config('message.SignName'),
+                        'TemplateCode' => config('message.TemplateCode'),
+                        'TemplateParam' => "{'code': $code}",
+                    ],
+                ])
+                ->request();
+            Log::record("短信验证码发送结果:","debug");
+            Log::record(print_r($result->toArray(),true),"debug");
+        } catch (ClientException|ServerException $e) {
+            Log::record("短信验证码发送结果:","debug");
+            Log::record($e->getErrorMessage(),"debug");
+//        echo $e->getErrorMessage() . PHP_EOL;
+        }
+    }
+}

+ 135 - 0
api/app/common/util/ArithmeticCount.php

@@ -0,0 +1,135 @@
+<?php
+
+namespace app\common\util;
+
+class ArithmeticCount
+{
+//运算符栈,在栈里插入最小优先级的运算符,避免进入不到去运算符比较的循环里
+    protected $opArr = ['#'];
+    //运算数栈;
+    protected $oprandArr = [];
+    //运算符优先级
+    protected $opLevelArr = [
+        ')' => 2,
+        '(' => 3,
+        '+' => 4,
+        '-' => 4,
+        '*' => 5,
+        '/' => 5,
+        '#' => 1
+    ];
+
+    protected $bolanExprArr = [];
+    protected $exprLen=0;
+    protected $inop = false;
+    protected $opNums = "";
+    protected $expr="";
+    protected $bai=false;
+    /**
+     * Notes: 百分号转数字 查找替换
+     * author 何腾骥
+     * Date: 2018/9/27
+     * Time: 11:51
+     * @param $expr
+     * @return array|string|string[]
+     */
+    protected function doReplace($expr)
+    {
+        $expr = str_replace(' ','' ,$expr);
+        $expr = str_replace('(','(' ,$expr);
+        $expr = str_replace(')',')' ,$expr);
+        $expr = str_replace(' ','' ,$expr);
+        preg_match_all("/\d+(\.\d+)?%/",$expr,$arr);
+
+        if(isset($arr[0]) && !empty($arr[0])){
+            foreach ($arr[0] as $val){
+                $expr = str_replace($val,(float) $val /100 ,$expr);
+            }
+            return $expr;
+        }
+        if(preg_match("/100-\(/",$expr,$arr)){
+            $expr = str_replace('100-','' ,$expr);
+            $this->bai = true;
+        }
+        return $expr;
+
+    }
+
+    /**
+     * Notes: 处理 计算
+     * author 何腾骥
+     * Date: 2018/9/27
+     * Time: 11:53
+     */
+    public  function makeCount($expr)
+    {
+        error_reporting(0);
+        // 处理百分号
+        $this->expr = $this->doReplace($expr);
+        // 计算长度
+        $this->exprLen = strlen($expr);
+        //解析表达式
+        for($i = 0;$i <= $this->exprLen;$i++){
+            $char = $this->expr[$i];
+            //获取当前字符的优先级
+            $level = intval($this->opLevelArr[$char]);
+            //如果大于0,表示是运算符,否则是运算数,直接输出
+            if($level > 0){
+                $this->inop = true;
+                //如果碰到左大括号,直接入栈
+                if($level == 3){
+                    array_push($this->opArr,$char);continue;
+                }
+                //与栈顶运算符比较,如果当前运算符优先级小于栈顶运算符,则栈顶运算符弹出,一直到当前运算符优先级不小于栈顶
+                while($op = array_pop($this->opArr)){
+                    if($op){
+                        $currentLevel = intval($this->opLevelArr[$op]);
+                        if($currentLevel == 3 && $level == 2) {
+                            break;
+                        }elseif($currentLevel >= $level && $currentLevel != 3){
+                            array_push($this->bolanExprArr,$op);
+                        }else{
+                            array_push($this->opArr,$op);
+                            array_push($this->opArr,$char);
+                            break;
+                        }
+                    }
+                }
+            }else{
+                //多位数拼接成一位数
+                $this->opNums .= $char;
+                if($this->opLevelArr[$this->expr[$i+1]] > 0){
+                    array_push($this->bolanExprArr, $this->opNums);
+                    $this->opNums = "";
+                }
+            }
+        }
+        array_push($this->bolanExprArr, $this->opNums);
+
+        //输出剩余运算符
+        while($leftOp = array_pop($this->opArr)){
+            if($leftOp != '#'){
+                array_push($this->bolanExprArr,$leftOp);
+            }
+        }
+
+        //计算逆波兰表达式。
+        foreach($this->bolanExprArr as $v){
+            if(!isset($this->opLevelArr[$v])){
+                array_push($this->oprandArr,$v);
+            }else{
+                $op1 = array_pop($this->oprandArr);
+                $op2 = array_pop($this->oprandArr);
+
+                eval("\$result = $op2 $v $op1;");
+
+                array_push($this->oprandArr,$result);
+            }
+        }
+
+        if($this->bai){
+            return 100 - $result;
+        }
+        return $result;
+    }
+}

+ 34 - 0
api/app/common/util/Channel.php

@@ -0,0 +1,34 @@
+<?php
+
+namespace app\common\util;
+
+use Channel\Client;
+use Exception;
+use think\facade\Log;
+
+class Channel
+{
+    public function __construct(){
+
+    }
+    /**
+     * 通知websocket进程
+     * @param string $events 事件
+     * @param string $type 类型
+     * @param int $connection_ids 连接id
+     * @param array $data 传输的数据报文
+     */
+    public  function sendChannel(string $events, string $type, int $connection_ids=0, array $data = []){
+        try {
+            Client::connect(config('channel.ip'),config('channel.port'));
+        }catch (Exception $e) {
+            // 异常捕获
+            Log::write( $e->getMessage(),'notice');
+        }
+        Client::publish($events,[
+            'type'=>$type,
+            'data'=>$data,
+            'connection_ids'=>$connection_ids,
+        ]);
+    }
+}

+ 94 - 0
api/app/common/util/DonationCert.php

@@ -0,0 +1,94 @@
+<?php
+namespace app\common\util;
+use app\common\service\OrderService;
+use Carbon\Carbon;
+class DonationCert
+{
+    public static function certByOrder($order)
+    {
+        $carbon = Carbon::parse($order->pay_time);
+        $date = $carbon->isoFormat('YYYY年MM月DD日');
+        $position = OrderService::orderPositionInDate($order);
+        $certNo = $carbon->isoFormat('YYYYMMDD') . sprintf('%04d', $position);
+        return self::cert($order->user_name, $order->money, $date, $certNo);
+    }
+
+    public static function cert($name, $money, $date, $certNo)
+    {
+        // 创建一张新的图片
+        $image = imagecreatefromjpeg('image.jpg');
+
+        // 设置文字颜色和字体大小
+        $gray = imagecolorallocate($image, 102, 102, 102);
+        $black = imagecolorallocate($image, 46, 46, 46);
+        $green = imagecolorallocate($image, 7, 193, 96);
+
+        $font = 'PingFang.ttf';
+        $semibold = 'PingFangSemiBold.ttf';
+
+        // 姓名
+        $x = 90; // 文本在图片中的x坐标
+        $y = 550; // 文本在图片中的y坐标
+        imagettftext($image, 36, 0, $x, $y, $black, $semibold, $name);
+
+        // 谢谢你
+        $thx = '谢谢你:';
+        $x = $x + 12 + strlen($name) * 15;
+        imagettftext($image, 18, 0, $x, $y, $gray, $font, $thx);
+
+        // 感谢语金额
+        $suffix = 2;
+        if ($money >= 10 * 10000 && $money < 100 * 10000) {
+            $suffix = 1;
+        } elseif ($money >= 100 * 10000) {
+            $suffix = 0;
+        }
+        $money = number_format($money, $suffix, '.', '');
+        $x = 265;
+        $y = 625;
+        imagettftext($image, 28, 0, $x, $y, $green, $semibold, $money);
+
+        // 下方列表
+        $formX = 258;
+        $formY = 870;
+        $gap = 86; //中间间隔
+
+        // 证书编号
+        imagettftext($image, 24, 0, $formX, $formY, $black, $font, $certNo);
+        // 捐赠金额
+        $formY += $gap;
+        imagettftext($image, 28, 0, $formX, $formY, $green, $semibold, $money);
+        // 发放时间
+        $formY += $gap;
+        imagettftext($image, 24, 0, $formX, $formY, $black, $font, $date);
+
+
+        // 输出图片
+        $name = md5(strtotime('now') . random_int(100000, 999999)) . '.png';
+        $dir = public_path() . "storage/";
+        if (!file_exists($dir)) {
+            mkdir($dir, 0777, true);
+        }
+        $path = $dir . $name;
+        imagepng($image, $path);
+        $url = self::get_domain() . dirname($_SERVER['SCRIPT_NAME']) . '/' . $name;
+        $url = str_replace('\\', '/', $url);
+        $url = str_replace('public', 'public/storage', $url);
+
+        // 释放内存
+        imagedestroy($image);
+
+        return ['url' => $url];
+    }
+
+    /**
+     * 获取当前网站的域名地址
+     * 
+     * @return string 域名地址
+     */
+    protected static function get_domain()
+    {
+        $sys_protocal = isset($_SERVER['SERVER_PORT']) && $_SERVER['SERVER_PORT'] == '443' ? 'https://' : 'http://';
+        return $sys_protocal . (isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : '');
+    }
+}

+ 90 - 0
api/app/common/util/Email.php

@@ -0,0 +1,90 @@
+<?php
+
+namespace app\common\util;
+
+use PHPMailer\PHPMailer\Exception;
+use PHPMailer\PHPMailer\PHPMailer;
+
+/**
+ * 动态邮箱类
+ */
+class Email
+{
+    public function sendEmail($toEmail, $tp): int
+    {
+        $mail = new PHPMailer();
+
+        $mail->isSMTP();// 使用SMTP服务
+        $mail->CharSet = "utf8";// 编码格式为utf8,不设置编码的话,中文会出现乱码
+        $mail->Host = "smtp.qq.com";// 发送方的SMTP服务器地址
+        $mail->SMTPAuth = true;// 是否使用身份验证
+        $mail->Username = "845178954@qq.com";// 发送方的qq邮箱用户名,就是你申请qq的SMTP服务使用的qq邮箱
+        $mail->Password = "dptntlmpifkqbfgb";// 发送方的邮箱密码,注意用163邮箱这里填写的是“客户端授权密码”而不是邮箱的登录密码!
+        $mail->SMTPSecure = "ssl";// 使用ssl协议方式
+        $mail->Port = 465;// 163邮箱的ssl协议方式端口号是465/994
+
+        try {
+            $mail->setFrom("845178954@qq.com", "皮编程");
+        } catch (Exception $e) {
+        }// 设置发件人信息,如邮件格式说明中的发件人,这里会显示为Mailer(xxxx@163.com),Mailer是当做名字显示
+        try {
+            $mail->addAddress($toEmail, '教务');
+        } catch (Exception $e) {
+
+        }// 设置收件人信息,如邮件格式说明中的收件人,这里会显示为Liang(yyyy@163.com)
+        try {
+            $mail->addReplyTo("845178954@qq.com", "皮编程");
+        } catch (Exception $e) {
+        }// 设置回复人信息,指的是收件人收到邮件后,如果要回复,回复邮件将发送到的邮箱地址
+        //$mail->addCC("xxx@163.com");// 设置邮件抄送人,可以只写地址,上述的设置也可以只写地址(这个人也能收到邮件)
+        //$mail->addBCC("xxx@163.com");// 设置秘密抄送人(这个人也能收到邮件)
+        //$mail->addAttachment("bug0.jpg");// 添加附件
+
+        $mail->isHTML();
+        $mail->Subject = "【商品发货通知】";// 邮件标题
+        try {
+            $mail->MsgHTML($tp);
+        } catch (Exception $e) {
+        }// 邮件正文
+        //$mail->AltBody = "This is the plain text纯文本";// 这个是设置纯文本方式显示的正文内容,如果不支持Html方式,就会用到这个,基本无用
+
+        if (!$mail->send()) {// 发送邮件
+            // return $this->api("Mailer Error: ".$mail->ErrorInfo, '发送失败~', 400);
+            return 400;
+        } else {
+            return 200;
+        }
+    }
+
+
+    public function getTp($phone, $goodsname, $goodstype, $num, $price, $total_amount, $status_text, $password = ''): string
+    {
+        return '<!DOCTYPE html>
+<html lang="">
+<head>
+  <title></title>
+  <meta charset="utf-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1">
+  <style>
+      table{
+        border: 1px solid red;
+      }
+  </style>
+</head>
+<body>
+
+<div class="container mt-3">
+  <h2>订单信息: </h2>  
+  <h5>账号: ' . $phone . '</h5>       
+  <h5>商品: ' . $goodsname . '</h5>     
+  <h5>类型: ' . $goodstype . '</h5>        
+  <h5>数量: ' . $num . '</h5>       
+  <h5>单价: ' . $price . '</h5>       
+  <h5>总金额: ' . $total_amount . '</h5>       
+  <h5>状态: ' . $status_text . '</h5>    
+  <h5>卡密: ' . $password . '</h5>        
+</div>
+</body>
+</html>';
+    }
+}

+ 37 - 0
api/app/common/util/Encryption.php

@@ -0,0 +1,37 @@
+<?php
+
+namespace app\common\util;
+class Encryption
+{
+    /**
+     * 生成token
+     * @param int $id
+     * @return string
+     */
+    public function createToken(int $id): string
+    {
+        $expireDays = 7;//过期时间,单位天
+        //token:  md5([用户名][当前时间])|[用户id]|[过期时间]
+        $time = (time() + 86400 * $expireDays);
+        $signKey = env('TOKEN_KEY');
+        $sign = md5($signKey . $id . $time);
+//        var_dump($signKey);
+        return base64_encode($sign . "|" . $id . "|" . $time);
+    }
+
+    /**
+     * 获取token
+     * @return array|mixed|string|null
+     */
+    public function getToken(): mixed
+    {
+        $token = null;
+        if (!$token) {
+            $token = request()->header("token");
+        }
+        if (!$token) {
+            $token = input("token");
+        }
+        return $token;
+    }
+}

+ 413 - 0
api/app/common/util/ExcelHelper.php

@@ -0,0 +1,413 @@
+<?php
+
+
+namespace app\common\util;
+
+use PhpOffice\PhpSpreadsheet\Reader\Xlsx;
+use PhpOffice\PhpSpreadsheet\Reader\Xls;
+use PhpOffice\PhpSpreadsheet\IOFactory;
+use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
+use PhpOffice\PhpSpreadsheet\Spreadsheet;
+use PhpOffice\PhpSpreadsheet\Worksheet\PageSetup;
+use PhpOffice\PhpSpreadsheet\Cell\DataType;
+use PhpOffice\PhpSpreadsheet\Style\Fill;
+use PhpOffice\PhpSpreadsheet\Style\Color;
+use PhpOffice\PhpSpreadsheet\Style\Alignment;
+use PhpOffice\PhpSpreadsheet\Style\Border;
+use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
+use think\facade\Log;
+
+
+class ExcelHelper extends SingleObjectClass
+{
+    public $columnNameArray = [];
+
+
+    /**
+     * 使用PhpSpreadsheet导入
+     *
+     * @param string $file 文件地址
+     * @param int $sheet 工作表sheet(传0则获取第一个sheet)
+     * @param int $columnCnt 列数(传0则自动获取最大列)
+     * @param array $options 操作选项
+     *                          array mergeCells 合并单元格数组
+     *                          array formula    公式数组
+     *                          array format     单元格格式数组
+     *
+     * @return array
+     * @throws Exception
+     */
+    public function importExecl(string $file = '', int $sheet = 0, int $columnCnt = 0, &$options = [])
+    {
+        try {
+            /* 转码 */
+            $file = iconv("utf-8", "gb2312", $file);
+
+            if (empty($file) or !file_exists($file)) {
+                throw new \Exception('文件不存在!');
+            }
+
+            /** @var Xlsx $objRead */
+            $objRead = IOFactory::createReader('Xlsx');
+
+            if (!$objRead->canRead($file)) {
+                /** @var Xls $objRead */
+                $objRead = IOFactory::createReader('Xls');
+
+                if (!$objRead->canRead($file)) {
+                    throw new \Exception('只支持导入Excel文件!');
+                }
+            }
+
+            /* 如果不需要获取特殊操作,则只读内容,可以大幅度提升读取Excel效率 */
+            empty($options) && $objRead->setReadDataOnly(true);
+            /* 建立excel对象 */
+            $obj = $objRead->load($file);
+            /* 获取指定的sheet表 */
+            $currSheet = $obj->getSheet($sheet);
+
+            if (isset($options['mergeCells'])) {
+                /* 读取合并行列 */
+                $options['mergeCells'] = $currSheet->getMergeCells();
+            }
+
+            if (0 == $columnCnt) {
+                /* 取得最大的列号 */
+                $columnH = $currSheet->getHighestColumn();
+                /* 兼容原逻辑,循环时使用的是小于等于 */
+                $columnCnt = Coordinate::columnIndexFromString($columnH);
+            }
+
+            /* 获取总行数 */
+            $rowCnt = $currSheet->getHighestRow();
+            $data = [];
+
+            /* 读取内容 */
+            for ($_row = 1; $_row <= $rowCnt; $_row++) {
+                $isNull = true;
+
+                for ($_column = 1; $_column <= $columnCnt; $_column++) {
+                    $cellName = Coordinate::stringFromColumnIndex($_column);
+                    $cellId = $cellName . $_row;
+                    $cell = $currSheet->getCell($cellId);
+
+                    if (isset($options['format'])) {
+                        /* 获取格式 */
+                        $format = $cell->getStyle()->getNumberFormat()->getFormatCode();
+                        /* 记录格式 */
+                        $options['format'][$_row][$cellName] = $format;
+                    }
+
+                    if (isset($options['formula'])) {
+                        /* 获取公式,公式均为=号开头数据 */
+                        $formula = $currSheet->getCell($cellId)->getValue();
+
+                        if (0 === strpos($formula, '=')) {
+                            $options['formula'][$cellName . $_row] = $formula;
+                        }
+                    }
+
+                    if (isset($format) && 'm/d/yyyy' == $format) {
+                        /* 日期格式翻转处理 */
+                        $cell->getStyle()->getNumberFormat()->setFormatCode('yyyy/mm/dd');
+                    }
+
+                    $data[$_row][$cellName] = trim($currSheet->getCell($cellId)->getFormattedValue());
+
+                    if (!empty($data[$_row][$cellName])) {
+                        $isNull = false;
+                    }
+                }
+
+                /* 判断是否整行数据为空,是的话删除该行数据 */
+                if ($isNull) {
+                    unset($data[$_row]);
+                }
+            }
+
+            return $data;
+        } catch (\Exception $e) {
+            throw $e;
+        }
+    }
+
+    /**
+     * PhpSpreadsheet Excel导出,
+     *
+     * @param array $datas 导出数据,格式['A1' => 'XXXX公司报表', 'B1' => '序号']
+     * @param string $fileName 导出文件名称
+     * @param array $options 操作选项,例如:
+     *                           bool   print       设置打印格式
+     *                           bool   defaultWrap  默认换行,true或false
+     *                           bool   defaultAlignCenter  默认居中,true或false
+     *                           number defaultFontSize  默认字体大小
+     *                           string freezePane  锁定行数,例如表头为第一行,则锁定表头输入A2
+     *                           array  setARGB     设置背景色,例如['A1', 'C1']
+     *                           array  setWidth    设置宽度,例如['A' => 30, 'C' => 20]
+     *                           bool|string   setBorder   设置单元格边框 A1:B5
+     *                           array  mergeCells  设置合并单元格,例如['A1:J1' => 'A1:J1']
+     *                           array  formula     设置公式,例如['F2' => '=IF(D2>0,E42/D2,0)']
+     *                           array  format      设置格式,整列设置,例如['A' => 'General','B'=>'number']
+     *                           array  alignCenter 设置居中样式,例如['A1', 'A2']
+     *                           array  bold        设置加粗样式,例如['A1', 'A2']
+     *                           array  wrap        设置换行,例如['A1', 'A2']
+     *                           array  rowHeight   设置行高,例如['9'=>"20"]
+     *                           array  fontSize   设置行高,例如['A1'=>"20"]
+     *                           string savePath    保存路径,设置后则文件保存到服务器,不通过浏览器下载
+     */
+    public function exportExcel(array $datas, string $fileName = '', array $options = []): bool
+    {
+        try {
+            if (empty($datas)) {
+                return false;
+            }
+
+            set_time_limit(0);
+            /** @var Spreadsheet $objSpreadsheet */
+            $objSpreadsheet = app(Spreadsheet::class);
+            /* 设置默认文字居左,上下居中 */
+            $styleArray = [
+                'alignment' => [
+                    'horizontal' => Alignment::HORIZONTAL_LEFT,
+                    'vertical' => Alignment::VERTICAL_CENTER,
+                ],
+            ];
+            $objSpreadsheet->getDefaultStyle()->applyFromArray($styleArray);
+            /* 设置Excel Sheet */
+            $activeSheet = $objSpreadsheet->setActiveSheetIndex(0);
+
+            /* 打印设置 */
+            if (isset($options['print']) && $options['print']) {
+                /* 设置打印为A4效果 */
+                $activeSheet->getPageSetup()->setPaperSize(PageSetup:: PAPERSIZE_A4);
+                /* 设置打印时边距 */
+                $pValue = 1 / 2.54;
+                $activeSheet->getPageMargins()->setTop($pValue / 2);
+                $activeSheet->getPageMargins()->setBottom($pValue * 2);
+                $activeSheet->getPageMargins()->setLeft($pValue / 2);
+                $activeSheet->getPageMargins()->setRight($pValue / 2);
+            }
+
+            /* 行数据处理 */
+            foreach ($datas as $sKey => $sItem) {
+                /* 默认文本格式 */
+                $pDataType = DataType::TYPE_STRING;
+
+                /* 设置单元格格式 */
+                if (isset($options['format']) && !empty($options['format'])) {
+                    $colRow = Coordinate::coordinateFromString($sKey);
+
+                    /* 存在该列格式并且有特殊格式 */
+                    if (isset($options['format'][$colRow[0]]) &&
+                        NumberFormat::FORMAT_GENERAL != $options['format'][$colRow[0]]) {
+                        $activeSheet->getStyle($sKey)->getNumberFormat()
+                            ->setFormatCode($options['format'][$colRow[0]]);
+
+                        if (false !== strpos($options['format'][$colRow[0]], '0.00') &&
+                            is_numeric(str_replace(['¥', ','], '', $sItem))) {
+                            /* 数字格式转换为数字单元格 */
+                            $pDataType = DataType::TYPE_NUMERIC;
+                            $sItem = str_replace(['¥', ','], '', $sItem);
+                        }
+                    } elseif (is_int($sItem) || is_numeric($sItem)) {
+                        $pDataType = DataType::TYPE_NUMERIC;
+                    }
+                }
+
+                $activeSheet->setCellValueExplicit($sKey, $sItem, $pDataType);
+
+                /* 存在:形式的合并行列,列入A1:B2,则对应合并 */
+                if (false !== strstr($sKey, ":")) {
+                    $options['mergeCells'][$sKey] = $sKey;
+                }
+
+                /* 设置换行 */
+                if (isset($options['defaultWrap']) && !empty($options['defaultWrap'])) {
+                    $activeSheet->getStyle($sKey)->getAlignment()->setWrapText(true);
+                }
+                /* 设置字体 */
+                if (isset($options['defaultFontSize']) && !empty($options['defaultFontSize'])) {
+                    $activeSheet->getStyle($sKey)->getFont()->setSize($options["defaultFontSize"]);
+                }
+                /* 设置居中 */
+                if (isset($options['defaultAlignCenter']) && !empty($options['defaultAlignCenter'])) {
+                    $styleArray = [
+                        'alignment' => [
+                            'horizontal' => Alignment::HORIZONTAL_CENTER,
+                            'vertical' => Alignment::VERTICAL_CENTER,
+                        ],
+                    ];
+                    $activeSheet->getStyle($sKey)->applyFromArray($styleArray);
+                }
+            }
+            unset($datas);
+
+            /* 设置锁定行 */
+            if (isset($options['freezePane']) && !empty($options['freezePane'])) {
+                $activeSheet->freezePane($options['freezePane']);
+                unset($options['freezePane']);
+            }
+
+            /* 设置宽度 */
+            if (isset($options['setWidth']) && !empty($options['setWidth'])) {
+                foreach ($options['setWidth'] as $swKey => $swItem) {
+                    $activeSheet->getColumnDimension($swKey)->setWidth($swItem);
+                }
+
+                unset($options['setWidth']);
+            }
+
+            /* 设置背景色 */
+            if (isset($options['setARGB']) && !empty($options['setARGB'])) {
+                foreach ($options['setARGB'] as $sItem) {
+                    $activeSheet->getStyle($sItem)
+                        ->getFill()->setFillType(Fill::FILL_SOLID)
+                        ->getStartColor()->setARGB(Color::COLOR_YELLOW);
+                }
+
+                unset($options['setARGB']);
+            }
+
+            /* 设置公式 */
+            if (isset($options['formula']) && !empty($options['formula'])) {
+                foreach ($options['formula'] as $fKey => $fItem) {
+                    $activeSheet->setCellValue($fKey, $fItem);
+                }
+
+                unset($options['formula']);
+            }
+
+            /* 合并行列处理 */
+            if (isset($options['mergeCells']) && !empty($options['mergeCells'])) {
+                $activeSheet->setMergeCells($options['mergeCells']);
+                unset($options['mergeCells']);
+            }
+
+            /* 设置居中 */
+            if (isset($options['alignCenter']) && !empty($options['alignCenter'])) {
+                $styleArray = [
+                    'alignment' => [
+                        'horizontal' => Alignment::HORIZONTAL_CENTER,
+                        'vertical' => Alignment::VERTICAL_CENTER,
+                    ],
+                ];
+
+                foreach ($options['alignCenter'] as $acItem) {
+                    $activeSheet->getStyle($acItem)->applyFromArray($styleArray);
+                }
+
+                unset($options['alignCenter']);
+            }
+
+            /* 设置加粗 */
+            if (isset($options['bold']) && !empty($options['bold'])) {
+                foreach ($options['bold'] as $bItem) {
+                    $activeSheet->getStyle($bItem)->getFont()->setBold(true);
+                }
+
+                unset($options['bold']);
+            }
+
+
+            /* 设置换行 */
+            if (isset($options['wrap']) && !empty($options['wrap'])) {
+
+                foreach ($options['wrap'] as $acItem) {
+                    $activeSheet->getStyle($acItem)->getAlignment()->setWrapText(true);
+                }
+
+                unset($options['wrap']);
+            }
+            //设置行高 rowHeight
+            if (isset($options['rowHeight']) && !empty($options['rowHeight'])) {
+
+                foreach ($options['rowHeight'] as $key => $item) {
+                    $activeSheet->getRowDimension($key)->setRowHeight($item);
+                }
+
+                unset($options['rowHeight']);
+            }
+            /* 设置字体大小 */
+            if (isset($options['fontSize']) && !empty($options['fontSize'])) {
+
+                foreach ($options['fontSize'] as $key => $value) {
+                    $activeSheet->getStyle($key)->getFont()->setSize($value);
+                }
+
+                unset($options['fontSize']);
+            }
+
+            /* 设置单元格边框,整个表格设置即可,必须在数据填充后才可以获取到最大行列 */
+            if (isset($options['setBorder']) && $options['setBorder']) {
+                $border = [
+                    'borders' => [
+                        'allBorders' => [
+                            'borderStyle' => Border::BORDER_THIN, // 设置border样式
+                            'color' => ['argb' => 'FF000000'], // 设置border颜色
+                        ],
+                    ],
+                ];
+                if ($options['setBorder'] === true) {
+                    $setBorder = 'A1:' . $activeSheet->getHighestColumn() . $activeSheet->getHighestRow();
+                } else {
+                    $setBorder = $options['setBorder'];
+                }
+
+                $activeSheet->getStyle($setBorder)->applyFromArray($border);
+                unset($options['setBorder']);
+            }
+
+            $fileName = !empty($fileName) ? $fileName : (date('YmdHis') . '.xlsx');
+
+            if (!isset($options['savePath'])) {
+                /* 直接导出Excel,无需保存到本地,输出07Excel文件 */
+                header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
+                header(
+                    "Content-Disposition:attachment;filename=" . iconv(
+                        "utf-8", "GB2312//TRANSLIT", $fileName
+                    )
+                );
+                header('Cache-Control: max-age=0');//禁止缓存
+                $savePath = 'php://output';
+            } else {
+                $savePath = $options['savePath'];
+            }
+            if (ob_get_length() > 0) {
+                ob_clean();
+                ob_start();
+            }
+
+            $objWriter = IOFactory::createWriter($objSpreadsheet, 'Xlsx');
+            $objWriter->save($savePath);
+            /* 释放内存 */
+            $objSpreadsheet->disconnectWorksheets();
+            unset($objSpreadsheet);
+
+            if (ob_get_length() > 0) {
+                ob_end_flush();
+            }
+            return true;
+        } catch (Exception $e) {
+            return false;
+        }
+    }
+
+    public function __construct()
+    {
+        $this->setColumnName();
+    }
+
+    private function setColumnName()
+    {
+        $columnNameArr = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"];
+        $columnNameArrLen = count($columnNameArr);
+        $firstWord = "";
+        for ($i = 0; $i < 500; $i++) {
+            if ($i % $columnNameArrLen == 0 && $i != 0) {
+                $firstWord = $columnNameArr[$i / $columnNameArrLen - 1];
+            }
+            $this->columnNameArray[] = $firstWord . $columnNameArr[$i % $columnNameArrLen];
+        }
+
+    }
+}

+ 66 - 0
api/app/common/util/IdCard.php

@@ -0,0 +1,66 @@
+<?php
+
+namespace app\common\util;
+
+class IdCard
+{
+    private static function idcard_verify_number($idcard_base)
+    {
+        if (strlen($idcard_base) != 17) {
+            return false;
+        }
+        //加权因子
+        $factor = array(7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2);
+        //校验码对应值
+        $verify_number_list = array('1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2');
+        $checksum = 0;
+        for ($i = 0; $i < strlen($idcard_base); $i++) {
+            $checksum += substr($idcard_base, $i, 1) * $factor[$i];
+        }
+        $mod = $checksum % 11;
+        $verify_number = $verify_number_list[$mod];
+        return $verify_number;
+    }
+
+    /**
+     * 将15位身份证升级到18位
+     * @param string $idcard
+     * @return boolean
+     */
+    private static function idcard_15to18($idcard)
+    {
+        if (strlen($idcard) != 15) {
+            return false;
+        } else {
+            // 如果身份证顺序码是996 997 998 999,这些是为百岁以上老人的特殊编码
+            if (array_search(substr($idcard, 12, 3), array('996', '997', '998', '999')) !== false) {
+                $idcard = substr($idcard, 0, 6) . '18' . substr($idcard, 6, 9);
+            } else {
+                $idcard = substr($idcard, 0, 6) . '19' . substr($idcard, 6, 9);
+            }
+        }
+        $idcard = $idcard . self::idcard_verify_number($idcard);
+        return $idcard;
+    }
+
+    /**
+     * 18位身份证校验码有效性检查
+     * @param string $idcard
+     * @return boolean|string
+     */
+    public static function check($idcard)
+    {
+        if (strlen($idcard) == 15) {
+            $idcard = self::idcard_15to18($idcard);
+        }
+        if (strlen($idcard) != 18) {
+            return false;
+        }
+        $idcard_base = substr($idcard, 0, 17);
+        if (self::idcard_verify_number($idcard_base) != strtoupper(substr($idcard, 17, 1))) {
+            return false;
+        } else {
+            return $idcard;
+        }
+    }
+}

+ 128 - 0
api/app/common/util/ImgCompress.php

@@ -0,0 +1,128 @@
+<?php
+
+
+namespace app\common\util;
+
+
+class ImgCompress
+{
+    private $src;
+    private $image;
+    private $imageinfo;
+    private $percent;
+
+    /**
+     * 图片压缩
+     * @param $src
+     * @param float $percent 压缩比例
+     */
+    public function __construct($src, $percent = 1)
+    {
+        $this->src = $src;
+        $this->percent = $percent;
+    }
+
+    /** 高清压缩图片
+     * @param string $saveName 提供图片名(可不带扩展名,用源图扩展名)用于保存。或不提供文件名直接显示
+     */
+    public function compressImg(string $saveName = '')
+    {
+        $this->_openImage();
+        if (!empty($saveName)) $this->_saveImage($saveName);  //保存
+        else $this->_showImage();
+    }
+
+    /**
+     * 内部:打开图片
+     */
+    private function _openImage()
+    {
+        list($width, $height, $type, $attr) = getimagesize($this->src);
+        $this->imageinfo = array(
+            'width' => $width,
+            'height' => $height,
+            'type' => image_type_to_extension($type, false),
+            'attr' => $attr
+        );
+        $fun = "imagecreatefrom" . $this->imageinfo['type'];
+        $this->image = $fun($this->src);
+        $this->_thumpImage();
+    }
+
+    /**
+     * 内部:操作图片
+     */
+    private function _thumpImage()
+    {
+        $bai_fen = $this->imageinfo['width'] / $this->imageinfo['height'];
+
+        $new_width = 600;
+        $new_height = intval(600 / number_format($bai_fen, 2));
+        $image_thump = imagecreatetruecolor($new_width, $new_height);
+        //将原图复制带图片载体上面,并且按照一定比例压缩,极大的保持了清晰度
+        imagecopyresampled($image_thump, $this->image, 0, 0, 0, 0, $new_width, $new_height, $this->imageinfo['width'], $this->imageinfo['height']);
+        imagedestroy($this->image);
+        $this->image = $image_thump;
+    }
+
+    /**
+     * 输出图片:保存图片则用saveImage()
+     */
+    private function _showImage()
+    {
+        header('Content-Type: image/' . $this->imageinfo['type']);
+        $funcs = "image" . $this->imageinfo['type'];
+        $funcs($this->image);
+    }
+
+    /**
+     * 保存图片到硬盘:
+     * @param string $dstImgName 1、可指定字符串不带后缀的名称,使用源图扩展名 。2、直接指定目标图片名带扩展名。
+     */
+    private function _saveImage(string $dstImgName)
+    {
+        if (empty($dstImgName)) return false;
+        $allowImgs = ['.jpg', '.jpeg', '.png', '.bmp', '.wbmp', '.gif'];   //如果目标图片名有后缀就用目标图片扩展名 后缀,如果没有,则用源图的扩展名
+        $dstExt = strrchr($dstImgName, ".");
+        $sourseExt = strrchr($this->src, ".");
+        if (!empty($dstExt)) $dstExt = strtolower($dstExt);
+        if (!empty($sourseExt)) $sourseExt = strtolower($sourseExt);
+        //有指定目标名扩展名
+        if (!empty($dstExt) && in_array($dstExt, $allowImgs)) {
+            $dstName = $dstImgName;
+        } elseif (!empty($sourseExt) && in_array($sourseExt, $allowImgs)) {
+            $dstName = $dstImgName . $sourseExt;
+        } else {
+            $dstName = $dstImgName . $this->imageinfo['type'];
+        }
+        $funcs = "image" . $this->imageinfo['type'];
+        $funcs($this->image, $dstName);
+    }
+
+    /**
+     * 销毁图片
+     */
+    public function __destruct()
+    {
+        imagedestroy($this->image);
+    }
+
+    public function getImgList($path, &$files)
+    {
+
+        if (is_dir($path)) {
+            $dp = dir($path);
+            while ($file = $dp->read()) {
+                if ($file !== "." && $file !== "..") {
+                    $this->getImgList($path . "/" . $file, $files);
+                }
+            }
+            $dp->close();
+        }
+        if (is_file($path)) {
+            $files[] = $path;
+        }
+
+        return $files;
+    }
+}

+ 35 - 0
api/app/common/util/LogHelper.php

@@ -0,0 +1,35 @@
+<?php
+
+namespace app\common\util;
+
+use think\facade\Request;
+use app\common\model\Order;
+use app\common\model\Payment;
+use app\common\model\OrderLog;
+use app\common\model\PaymentLog;
+
+class LogHelper
+{
+
+    public static function logPayment(Payment $payment, $response)
+    {
+        $request = Request::instance();
+
+        $logData = [
+            'payment_id' => $payment->id,
+            'order_id' => $payment->order_id,
+            'name' => $request->action(),
+            'content' => $request->getContent(),
+            'url' => $request->url(),
+            'method' => $request->method(),
+            'ip' => $request->ip(),
+            'params' => json_encode($request->param()),
+            'controller' => $request->controller(),
+            'action' => $request->action(),
+            'response' => json_encode($response)
+        ];
+        PaymentLog::create($logData);
+
+        return $response;
+    }
+}

+ 175 - 0
api/app/common/util/MiniProgram.php

@@ -0,0 +1,175 @@
+<?php
+
+namespace app\common\util;
+
+use EasyWeChat\Factory;
+use EasyWeChat\Kernel\Exceptions\DecryptException;
+use EasyWeChat\Kernel\Exceptions\InvalidConfigException;
+use EasyWeChat\Kernel\Support\Collection;
+use GuzzleHttp\Exception\GuzzleException;
+use Psr\Http\Message\ResponseInterface;
+
+
+class MiniProgram
+{
+//    public const pay_notify_url = 'https://natapp.sphper.com';
+    public const pay_notify_url = 'https://test.server.ycxxkj.com/deliver/api/public/index.php';
+
+    /**
+     * 小程序实例
+     * @return object
+     */
+    public static function getApp(): object
+    {
+        $wechat = config('wechat');
+        $config = [
+            // 必要配置
+            'app_id' => $wechat['app_id'],//appId
+            'secret' => $wechat['secret'],
+
+            // 下面为可选项
+            // 指定 API 调用返回结果的类型:array(default)/collection/object/raw/自定义类名
+            'response_type' => 'array',
+        ];
+        return Factory::miniProgram($config);
+    }
+
+    /**
+     * 支付实例
+     * @return object
+     */
+    public static function getPay(): object
+    {
+        $wechat = config('wechat');
+        $config = [
+            // 必要配置
+            'app_id' => $wechat['app_id'],
+            'mch_id' => $wechat['mch_id'],
+            'key' => $wechat['key'],
+            'cert_path' => $wechat['cert_path'],
+            'key_path' => $wechat['key_path'],
+            'notify_url' => $wechat['notify_url'],
+        ];
+        return Factory::payment($config);
+    }
+
+    /**
+     * 获取手机号 非解密
+     * @param $code
+     * @return array|Collection|object|ResponseInterface|string
+     * @throws InvalidConfigException
+     * @throws GuzzleException
+     */
+    public static function getPhoneNumber($code)
+    {
+        $params = [
+            'code' => $code
+        ];
+        $app = self::getApp();
+        return $app->live->httpPostJson('wxa/business/getuserphonenumber', $params);
+    }
+
+    /**
+     * 获取手机号 解密
+     * @param $session_key
+     * @param $iv
+     * @param $encryptedData
+     * @return array
+     * @throws DecryptException
+     */
+    public static function getPhoneNumberIv($session_key, $iv, $encryptedData): array
+    {
+        $app = self::getApp();
+        return $app->encryptor->decryptData($session_key, $iv, $encryptedData);
+    }
+
+    /**
+     * NATIVE
+     * @param $order_no
+     * @param $total_fee
+     * @return false|string
+     */
+    public static function nativePay($order_no, $total_fee)
+    {
+        $app = self::getPay();
+        $result = $app->order->unify([
+            'trade_type' => 'NATIVE',
+            'body' => '快递柜费用',
+            'out_trade_no' => $order_no,
+            'total_fee' => $total_fee * 100,
+            'notify_url' => self::pay_notify_url . '/mobile/payNotify/index',
+            'product_id' => 1, // $message['product_id'] 则为生成二维码时的产品 ID
+            // ...
+        ]);
+        if ($result['return_code'] == 'SUCCESS' && $result['result_code'] == 'SUCCESS') {
+            return QrCode::createEwm($result['code_url']);
+        }
+        return false;
+    }
+
+    /**
+     * JSAPI
+     * @param $order_no
+     * @param $total_fee
+     * @param $openid
+     * @return mixed
+     */
+    public static function JsApiPay($order_no, $total_fee, $openid)
+    {
+        $app = self::getPay();
+        $jsSdk = $app->jssdk;
+
+        $result = $app->order->unify([
+            'body' => '快递柜费用',
+            'out_trade_no' => $order_no,
+            'total_fee' => intval($total_fee * 100),
+            'notify_url' => self::pay_notify_url . '/api/payNotify/index', // 支付结果通知网址,如果不设置则会使用配置里的默认地址
+            'trade_type' => 'JSAPI', // 请对应换成你的支付方式对应的值类型
+            'openid' => $openid,
+        ]);
+        $pay_json = false;
+        if ($result['return_code']=='SUCCESS'&&$result['result_code']=='SUCCESS'){
+            $pay_json = json_decode($jsSdk->bridgeConfig($result['prepay_id']));
+        }
+
+
+        return $pay_json;
+    }
+
+
+    /**
+     * 创建二维码
+     * @param $param
+     * @return string
+     */
+    public static function createEwm($param): string
+    {
+        $app = self::getApp();
+
+        $response = $app->app_code->get(config('ewm.path') . '?' . $param);
+
+        $file_name = self::rand() . '.png';
+        $dir = public_path() . 'storage/topic/' . date('Ymd');
+        if (!is_dir($dir)) {
+            mkdir($dir);
+        }
+        $saveFile = $dir . '/' . $file_name;
+        $url = request()->domain() . getVirRootDir() . '/storage/topic/' . date('Ymd') . '/' . $file_name;
+        file_put_contents($saveFile, $response);
+        return $url;
+    }
+
+    private static function rand()
+    {
+        $len = 10;
+        $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz';
+        $string = time();
+        for (; $len >= 1; $len--) {
+            $position = rand() % strlen($chars);
+            $position2 = rand() % strlen($string);
+            $string = substr_replace($string, substr($chars, $position, 1), $position2, 0);
+        }
+        return $string;
+
+    }
+}

+ 33 - 0
api/app/common/util/OrderHelper.php

@@ -0,0 +1,33 @@
+<?php
+
+namespace app\common\util;
+
+use think\db\exception\DataNotFoundException;
+use think\db\exception\DbException;
+use think\db\exception\ModelNotFoundException;
+use think\facade\Db;
+
+class OrderHelper
+{
+    /**
+     * 生成订单号
+     * @param $user_id
+     * @return string|null
+     * @throws DataNotFoundException
+     * @throws DbException
+     * @throws ModelNotFoundException
+     */
+    public static function createOrderNo($user_id): ?string
+    {
+        for ($i = 0; $i < 10000; $i++) {
+            //订单号规则 年月日时分秒+4位随机数+用户ID
+            $no = date('YmdHis') . rand(1000, 9999) . $user_id;
+            $exist = (new Db)->name('order')->where(["order_no" => $no])->find();
+
+            if (!$exist) {
+                return $no;
+            }
+        }
+        return null;
+    }
+}

+ 176 - 0
api/app/common/util/PHPExcel.php

@@ -0,0 +1,176 @@
+<?php
+
+namespace app\common\util;
+
+use app\api\controller\BaseAuthorized;
+use PHPExcel_IOFactory;
+use think\facade\Db;
+
+class PHPExcel{
+    public static function import($path,$login_staff)
+    {
+        $LeaveModel = new \app\common\model\Leave();
+        $result = [
+            'code'=>999,
+            'msg'=>'错误'
+        ];
+        //实例化PHPExcel类
+        $path = public_path().'storage/'.$path;
+        $PHPExcel = new \PHPExcel();
+        //默认用excel2007读取excel,若格式不对,则用之前的版本进行读取
+        $PHPReader = new \PHPExcel_Reader_Excel2007();
+        if (!$PHPReader->canRead($path)) {
+            $PHPReader = new \PHPExcel_Reader_Excel5();
+            if (!$PHPReader->canRead($path)) {
+                $result['msg'] = '请上传excel文件';
+                return $result;
+            }
+        }
+
+        //读取Excel文件
+        $PHPExcel = $PHPReader->load($path);
+
+        //读取excel文件中的第一个工作表
+        $sheet = $PHPExcel->getSheet(0);
+
+        //取得最大的列号
+        $allColumn = $sheet->getHighestColumn();
+
+        //取得最大的行号
+        $allRow = $sheet->getHighestRow();
+
+        #清除表
+        //从第二行开始插入,第一行是列名
+        $month = [];
+        $month2 = [];
+        $i = 0;
+        //检测数据
+        for ($currentRow = 4; $currentRow <= $allRow; $currentRow++){
+            $number = $PHPExcel->getActiveSheet()->getCell("A" . $currentRow)->getValue();
+            if(!$number){
+                break;
+            }
+            $array = [
+                'month'=>$PHPExcel->getActiveSheet()->getCell("B" . $currentRow)->getValue(),
+                'phone'=>$PHPExcel->getActiveSheet()->getCell("C" . $currentRow)->getValue(),
+//                'name'=>$PHPExcel->getActiveSheet()->getCell("D" . $currentRow)->getValue(),
+                'days'=>$PHPExcel->getActiveSheet()->getCell("E" . $currentRow)->getValue(),
+                'real_days'=>$PHPExcel->getActiveSheet()->getCell("F" . $currentRow)->getValue(),
+                'late'=>$PHPExcel->getActiveSheet()->getCell("G" . $currentRow)->getValue(),
+                'leave'=>$PHPExcel->getActiveSheet()->getCell("H" . $currentRow)->getValue(),
+                'absenteeism'=>$PHPExcel->getActiveSheet()->getCell("I" . $currentRow)->getValue(),
+                'late_deduct'=>$PHPExcel->getActiveSheet()->getCell("J" . $currentRow)->getValue(),
+                'leave_deduct'=>$PHPExcel->getActiveSheet()->getCell("K" . $currentRow)->getValue(),
+                'absenteeism_deduct'=>$PHPExcel->getActiveSheet()->getCell("L" . $currentRow)->getValue(),
+                'other_deduct'=>$PHPExcel->getActiveSheet()->getCell("M" . $currentRow)->getValue(),
+                'sum_deduct'=>$PHPExcel->getActiveSheet()->getCell("N" . $currentRow)->getValue(),
+                'other_reward'=>$PHPExcel->getActiveSheet()->getCell("O" . $currentRow)->getValue(),
+                'remark'=>$PHPExcel->getActiveSheet()->getCell("P" . $currentRow)->getValue(),
+                'create_time'=>date('Y-m-d'),
+                'valid'=>1,
+                'enterprise_id'=>$login_staff->enterprise_id,
+            ];
+
+            $month[$i][$array['month']] = $array['phone'];
+            $month2[$i][$array['phone']] = $array['month'];
+            $i++;
+
+            $user = \app\common\model\User::where('phone',$array['phone'])->find();
+            if (!$user){
+                $msg = '导入数据错误,请检查报表格式或者手机号数据·1';
+                $result['msg'] = $msg;
+                return $result;
+            }
+            $staff = \app\common\model\Staff::where(['user_id'=>$user->id,'enterprise_id'=>$login_staff->enterprise_id])->find();
+            if (!$staff){
+                $msg = '导入数据错误,请检查报表格式或者手机号数据·2';
+                $result['msg'] = $msg;
+                return $result;
+            }
+
+            if ($array['days']>31){
+                $msg = $staff->name.'应出勤天数不得超过31天';
+                $result['msg'] = $msg;
+                return $result;
+            }
+
+            if ($array['late_deduct']+$array['leave_deduct']+$array['absenteeism_deduct']+$array['other_deduct']!=$array['sum_deduct']){
+                $msg = $staff->name.'扣款合计不正确';
+                $result['msg'] = $msg;
+                return $result;
+            }
+        }
+
+        if (count(self::get_repeat_data($month))){
+            $msg = '导入数据重复';
+            $result['msg'] = $msg;
+            return $result;
+        }
+
+        for ($currentRow = 4; $currentRow <= $allRow; $currentRow++) {
+            //获取B列的值
+            $number = $PHPExcel->getActiveSheet()->getCell("A" . $currentRow)->getValue();
+            if(!$number){
+                $msg = '导入成功';
+                $result['code'] = 0;
+                $result['msg'] = $msg;
+                break;
+            }
+
+            $data = [
+                'month'=>$PHPExcel->getActiveSheet()->getCell("B" . $currentRow)->getValue(),
+                'phone'=>$PHPExcel->getActiveSheet()->getCell("C" . $currentRow)->getValue(),
+//                'name'=>$PHPExcel->getActiveSheet()->getCell("D" . $currentRow)->getValue(),
+                'days'=>$PHPExcel->getActiveSheet()->getCell("E" . $currentRow)->getValue(),
+                'real_days'=>$PHPExcel->getActiveSheet()->getCell("F" . $currentRow)->getValue(),
+                'late'=>$PHPExcel->getActiveSheet()->getCell("G" . $currentRow)->getValue(),
+                'leave'=>$PHPExcel->getActiveSheet()->getCell("H" . $currentRow)->getValue(),
+                'absenteeism'=>$PHPExcel->getActiveSheet()->getCell("I" . $currentRow)->getValue(),
+                'late_deduct'=>$PHPExcel->getActiveSheet()->getCell("J" . $currentRow)->getValue(),
+                'leave_deduct'=>$PHPExcel->getActiveSheet()->getCell("K" . $currentRow)->getValue(),
+                'absenteeism_deduct'=>$PHPExcel->getActiveSheet()->getCell("L" . $currentRow)->getValue(),
+                'other_deduct'=>$PHPExcel->getActiveSheet()->getCell("M" . $currentRow)->getValue(),
+                'sum_deduct'=>$PHPExcel->getActiveSheet()->getCell("N" . $currentRow)->getValue(),
+                'other_reward'=>$PHPExcel->getActiveSheet()->getCell("O" . $currentRow)->getValue(),
+                'remark'=>$PHPExcel->getActiveSheet()->getCell("P" . $currentRow)->getValue(),
+                'create_time'=>date('Y-m-d'),
+                'valid'=>1,
+                'enterprise_id'=>$login_staff->enterprise_id,
+//              'time'=>self::get_date_by_excel($PHPExcel->getActiveSheet()->getCell("F" . $currentRow)->getValue()),
+            ];
+
+            $user = \app\common\model\User::where('phone',$data['phone'])->find();
+            $staff = \app\common\model\Staff::where(['user_id'=>$user->id,'enterprise_id'=>$login_staff->enterprise_id])->find();
+            $data['staff_id'] = $staff->id;
+            $data['name'] = $staff->name;
+            $where['enterprise_id']=$login_staff->enterprise_id;
+            $where['month']=$data['month'];
+            $where['staff_id']=$data['staff_id'];
+            $LeaveModel->where($where)->delete();
+            #插入表
+            $LeaveModel = new \app\common\model\Leave();
+            $LeaveModel->save($data);
+        }
+        return $result;
+    }
+
+    //获取二维数组的重复数据
+    public static function get_repeat_data($array){
+        //这里的$array在数据库查询的时候要group by排序一下
+        //计算出数组的总数量
+        $count = count($array);
+        $repeat_data = [];
+        //循环从0开始逐次+1
+        for($i=0;$i<$count;$i++){
+            //每次都比第一层循环+1,如果第一层循环0数据,第二层就循环1数据,以此类推
+            for($j=$i+1;$j<$count;$j++){
+                //比较两次数据是否相等
+                if($array[$i] == $array[$j]){
+                    $repeat_data[$i]=$array[$i];
+                    $repeat_data[$j]=$array[$j];
+                }
+            }
+        }
+        return $repeat_data;
+    }
+}

+ 123 - 0
api/app/common/util/PhpOffice.php

@@ -0,0 +1,123 @@
+<?php
+/**
+ * Created by PhpStorm.
+ * User: yckj-yf7
+ * Date: 2021/9/26
+ * Time: 11:14
+ */
+
+namespace app\common\util;
+
+use PhpOffice\PhpSpreadsheet\Spreadsheet;
+use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
+
+class PhpOffice
+{
+    public static function export()
+    {
+        #获取对象
+        $spreadsheet = new Spreadsheet();
+        #获取工作簿
+        $sheet = $spreadsheet->getActiveSheet();
+
+        $QuestionAnswerRecordModel = new \app\common\model\Order();
+        $pageList = $QuestionAnswerRecordModel->analysis();
+        $list = $pageList['data'];
+        $arr = [];
+        #问题
+        $title = '问卷提交统计Excel';
+        $sheet->setCellValue(2,1,$title);
+        $sheet->getStyle('B1')
+            ->getFont()->setBold(true)
+            ->setName('宋体')
+            ->setSize(20);
+
+        $styleArray = [
+            'borders' => [
+                'left' => [
+                    'borderStyle' => \PhpOffice\PhpSpreadsheet\Style\Border::BORDER_THIN //细边框
+                ],
+                'right' => [
+                    'borderStyle' => \PhpOffice\PhpSpreadsheet\Style\Border::BORDER_THIN //细边框
+                ],
+                'top' => [
+                    'borderStyle' => \PhpOffice\PhpSpreadsheet\Style\Border::BORDER_THIN //细边框
+                ],
+                'bottom' => [
+                    'borderStyle' => \PhpOffice\PhpSpreadsheet\Style\Border::BORDER_THIN //细边框
+                ],
+            ],
+            'alignment' => [
+                'horizontal' =>  \PhpOffice\PhpSpreadsheet\Style\Alignment::HORIZONTAL_CENTER,
+            ],
+        ];
+
+        $styleArrayTitle = [
+            'alignment' => [
+                'horizontal' =>  \PhpOffice\PhpSpreadsheet\Style\Alignment::HORIZONTAL_CENTER,
+            ],
+        ];
+
+        $sheet->getColumnDimension('B')->setWidth(70);
+        $sheet->getStyle('B1')->applyFromArray($styleArrayTitle);
+
+        $i = 2;
+        foreach ($list['PaperQuestion'] as $key => $value) {
+//            $sheet->setCellValueByColumnAndRow(2,$i+1,'选项');
+//            $sheet->setCellValueByColumnAndRow(8,$i+1,'小计');
+//            $sheet->setCellValueByColumnAndRow(9,$i+1,'比例');
+//                 $sheet->mergeCells('')
+            $sheet->setCellValue(2,$i+1,$value['content']);
+            $sheet->getStyle('B'.($i+1).':'.'F'.($i+1))->applyFromArray($styleArray);
+            $sheet->getStyle('B'.($i+1))->getFont()->setBold(true)->setName('宋体')->setSize(15);
+            foreach ($value['paperQuestionAnswer'] as $key2=>$value2){
+                $sheet->setCellValue(2,$i+2,$value2['content']);
+                $sheet->setCellValue(5,$i+2,$value2['num']);
+//                $sheet->setCellValueByColumnAndRow(6,$i+2,$value2['avg'].'%');
+                $sheet->getStyle('B'.($i+2).':'.'F'.($i+2))->applyFromArray($styleArray);
+                $i++;
+            }
+            $i+=3;
+        }
+
+        self::exportBinary($spreadsheet);
+//        $this->exportFile($spreadsheet);
+    }
+
+    /**
+     *   ##导出一 直接生成xlsx文件
+     */
+    public static function exportFile($spreadsheet)
+    {
+        $writer = new Xlsx($spreadsheet);
+        $root = $_SERVER['DOCUMENT_ROOT'];
+        $filePath = $root . '/excel/';
+        if (!is_dir($filePath)) {
+            mkdir($filePath, 0777, true);
+        }
+        ##文件地址
+        $file =  time() . '.xlsx';
+        $fileName = $filePath . $file;
+        $writer->save($fileName);
+        $data = [
+            'fileName'=>$fileName,
+            'url'=>request()->host().'/excel/'.$file,
+        ];
+        return $data;
+    }
+
+    /**
+     * 导出二 直接返回二进制数据
+     */
+    public static function exportBinary($spreadsheet)
+    {
+        header('Content-Type:application/vnd.ms-excel');
+        header('Content-Disposition:attachment;filename=' . time() . '.xls');
+        header('Cache-Control:max-age=0');
+        $writer = \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($spreadsheet, 'Xls');
+        $writer->save('php://output');
+        exit;
+    }
+
+
+}

+ 210 - 0
api/app/common/util/PhpSpreadsheetExport.php

@@ -0,0 +1,210 @@
+<?php
+
+namespace app\common\util;
+
+use PhpOffice\PhpSpreadsheet\Exception;
+use PhpOffice\PhpSpreadsheet\IOFactory;
+use PhpOffice\PhpSpreadsheet\Spreadsheet;
+use PhpOffice\PhpSpreadsheet\Style\Alignment;
+use PhpOffice\PhpSpreadsheet\Style\Border;
+use PhpOffice\PhpSpreadsheet\Style\Color;
+use PhpOffice\PhpSpreadsheet\Style\Fill;
+
+class PhpSpreadsheetExport
+{
+
+    public function index()
+    {
+        $data = [
+            ['title1' => '111', 'title2' => '111', 'title3' => 666],
+            ['title1' => '222', 'title2' => '222'],
+            ['title1' => '333', 'title2' => '333']
+        ];
+        $tableHeader = [
+            ['第一行标题', '第一行标题'],
+            ['第二行标题', '第二行标题']
+        ];
+        $mergeCells = [
+            ['A1:B1' => '第一行标题', 'C1:F1' => '第一111行标题'],
+            ['A2:B2' => '第一行标题', 'C2:E2' => '第一222行标题'],
+        ];
+        $fileName = "8888.xlsx";
+
+        $this->saveFile($data, $fileName, $tableHeader);
+    }
+
+    public static array $excelCol = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
+        'AA', 'AB', 'AC', 'AD', 'AE', 'AF', 'AG', 'AH', 'AI', 'AJ', 'AK', 'AL', 'AM', 'AN', 'AO', 'AP', 'AQ', 'AR', 'AS', 'AT', 'AU', 'AV', 'AW', 'AX', 'AY', 'AZ'];
+
+    public static bool $setBold = false; //是否加粗
+    public static string $setName = '宋体'; //字体
+    public static string $setSize = '12'; //字体大小
+    public static string $setBgRGB = 'FFFF00'; //单元格背景色
+    public static string $setFontRGB = 'FF000000'; //字体颜色
+    public static array $styleArray = [
+        'alignment' => [
+            'horizontal' => Alignment::HORIZONTAL_CENTER,
+            'vertical' => Alignment::VERTICAL_CENTER
+        ],
+        'borders' => [
+            'allBorders' => [
+                'borderStyle' => Border::BORDER_THIN,
+                'color' => ['argb' => '000000'],
+            ],
+        ],
+    ];
+
+//    /**
+//     * 读取excel
+//     * @param $filePath
+//     * @param int $pageIndex
+//     * @param int $readRow
+//     * @return array
+//     * @throws Exception
+//     */
+//    public static function read($filePath, int $pageIndex = 0, int $readRow = 0): array
+//    {
+//        //加载文件
+//        $spreadSheet = IOFactory::load($filePath);
+//        //获取文件内容
+//        $workSheet = $spreadSheet->getSheet($pageIndex)->toArray('', true, true, false);
+//        //删除表头几行
+//        if ($readRow > 0) {
+//            for ($i = 0; $i < $readRow; $i++) {
+//                array_shift($workSheet);
+//            }
+//        }
+//        return $workSheet;
+//    }
+//
+//    /**
+//     * @param $data
+//     * @param $fileName
+//     * @param array $tableHeader
+//     * @param array $mergeCells
+//     * @param string $suffix
+//     * @return void
+//     * @throws Exception
+//     * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
+//     */
+//    public static function download($data, $fileName, array $tableHeader = [], array $mergeCells = [], string $suffix = 'xlsx'): void
+//    {
+//        $spreadsheet = self::write($data, $tableHeader, $mergeCells);
+//        // 将输出重定向到客户端的网络浏览器(Xlsx)
+//        header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
+//        header('Content-Disposition: attachment;filename="' . $fileName . '"');//文件名
+//        header('Cache-Control: max-age=0');
+//        // 如果你服务于IE 9,那么以下可能是需要的
+//        header('Cache-Control: max-age=1');
+//        // 如果您通过SSL为工业工程服务,那么可能需要以下内容
+//        header('Expires: Mon, 26 Jul 1997 05:00:00 GMT'); // Date in the past
+//        header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT'); // always modified
+//        header('Cache-Control: cache, must-revalidate'); // HTTP/1.1
+//        header('Pragma: public'); // HTTP/1.0
+//        $writer = \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($spreadsheet, ucwords($suffix));
+//        $writer->save('php://output');
+//    }
+
+    /**
+     * 保存文件
+     * @param $data
+     * @param $fileName
+     * @param array $tableHeader
+     * @param array $mergeCells
+     * @param string $suffix
+     * @return bool
+     */
+    public function saveFile($data, $fileName, array $tableHeader = [], array $mergeCells = [], string $suffix = 'xlsx'): bool
+    {
+        try {
+            $spreadsheet = self::write($data, $tableHeader, $mergeCells);
+            $writer = IOFactory::createWriter($spreadsheet, ucwords($suffix));
+            $writer->save($fileName, true);
+            return true;
+        } catch (\Exception) {
+            return false;
+        }
+    }
+
+    /**
+     * 写入数据
+     * @param $data
+     * @param $tableHeader
+     * @param $mergeCells
+     * @return Spreadsheet
+     * @throws Exception
+     */
+    public static function write($data, $tableHeader, $mergeCells): Spreadsheet
+    {
+        // 创建excel对象
+        $spreadsheet = new Spreadsheet();
+        $sheet = $spreadsheet->getActiveSheet();
+        $totalCol = 0;
+
+        //设置表头合并单元格
+        foreach ($mergeCells as $row => $rows) {
+            $i = 0;
+            foreach ($rows as $col => $colValue) {
+                //合并单元格
+                $sheet->mergeCells($col);
+                //设置样式
+                self::setStyle($sheet, $i, $totalCol, $row);
+                //单元格内容写入
+                $sheet->setCellValue(substr($col, 0, strpos($col, ":")), $colValue);
+                $i++;
+            }
+        }
+        $totalCol = count($mergeCells);
+
+        //设置表头
+        foreach ($tableHeader as $row => $rows) {
+            $headerRowDatas = array_values($rows);
+            foreach ($headerRowDatas as $col => $colValue) {
+                //设置样式
+                self::setStyle($sheet, $col, $totalCol, $row);
+                //单元格内容写入
+                $sheet->setCellValue(self::$excelCol[$col] . ($totalCol + $row + 1), $colValue);
+            }
+        }
+        $totalCol += count($tableHeader);
+
+        //设置内容
+        foreach ($data as $row => $rows) {
+            $rowDatas = array_values($rows);
+            foreach ($rowDatas as $col => $colValue) {
+                // 单元格内容写入
+                $sheet->setCellValue(self::$excelCol[$col] . ($totalCol + $row + 1), $colValue);
+            }
+        }
+        return $spreadsheet;
+    }
+
+    /**
+     * 设置单元格样式
+     * @param $sheet //某个sheet
+     * @param $col //某列
+     * @param $totalCol //总行数
+     * @param $row //某行
+     */
+    public static function setStyle($sheet, $col, $totalCol, $row): void
+    {
+        //设置单元格居中
+        $sheet->getStyle(self::$excelCol[$col] . ($totalCol + $row + 1))->applyFromArray(self::$styleArray);
+        //设置单元格
+        $sheet->getStyle(self::$excelCol[$col] . ($totalCol + $row + 1))
+            ->getFill()
+            ->setFillType(Fill::FILL_SOLID)
+            ->getStartColor()
+            ->setRGB(self::$setBgRGB);
+        //设置单元格字体样式、字体、字体大小
+        $sheet->getStyle(self::$excelCol[$col] . ($totalCol + $row + 1))
+            ->getFont()
+            ->setBold(self::$setBold)
+            ->setName(self::$setName)
+            ->setSize(self::$setSize);
+        //设置字体颜色
+        $sheet->getStyle(self::$excelCol[$col] . ($totalCol + $row + 1))
+            ->getFont()
+            ->getColor()->setRGB(self::$setFontRGB);
+    }
+}

+ 169 - 0
api/app/common/util/PhpSpreadsheetExportV2.php

@@ -0,0 +1,169 @@
+<?php
+
+namespace app\common\util;
+
+use PhpOffice\PhpSpreadsheet\Exception;
+use PhpOffice\PhpSpreadsheet\IOFactory;
+use PhpOffice\PhpSpreadsheet\Spreadsheet;
+use PhpOffice\PhpSpreadsheet\Style\Alignment;
+use PhpOffice\PhpSpreadsheet\Style\Border;
+use PhpOffice\PhpSpreadsheet\Style\Color;
+use PhpOffice\PhpSpreadsheet\Style\Fill;
+use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
+use think\route\Domain;
+
+class PhpSpreadsheetExportV2
+{
+    public static array $excelCol = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
+        'AA', 'AB', 'AC', 'AD', 'AE', 'AF', 'AG', 'AH', 'AI', 'AJ', 'AK', 'AL', 'AM', 'AN', 'AO', 'AP', 'AQ', 'AR', 'AS', 'AT', 'AU', 'AV', 'AW', 'AX', 'AY', 'AZ'];
+
+    public static bool $setBold = false; //是否加粗
+    public static string $setName = '宋体'; //字体
+    public static string $setSize = '12'; //字体大小
+    public static string $setBgRGB = 'FFFFFFFF'; //单元格背景色
+    public static string $setFontRGB = 'FF000000'; //字体颜色
+    public static array $styleArray = [
+        'alignment' => [
+            'horizontal' => Alignment::HORIZONTAL_CENTER,
+            'vertical' => Alignment::VERTICAL_CENTER
+        ],
+        'borders' => [
+            'allBorders' => [
+                'borderStyle' => Border::BORDER_THIN,
+                'color' => ['argb' => '000000'],
+            ],
+        ],
+    ];
+
+
+    /**
+     * 保存文件
+     * @param object $spreadsheet
+     * @param $fileName
+     * @param string $suffix
+     */
+    public static function saveFile(object $spreadsheet, $fileName, string $suffix = 'xlsx')
+    {
+        $writer = IOFactory::createWriter($spreadsheet, ucwords($suffix));
+        $writer->save($fileName, true);
+    }
+
+    /**
+     * 导出excel文件
+     * @param \ArrayAccess $data
+     * @param array<array> $tableHeader
+     * @param string $fileName
+     * @param array $mergeCells
+     * @return array
+     */
+    public static function outputFile($data, $tableHeader, $fileName, $mergeCells = [])
+    {
+        $path = "/storage/topic/output_excel/" . date('Ymd') . "/";
+        $dir = app()->getRootPath() . "public" . $path;
+        if (!file_exists($dir)) {
+            mkdir($dir, '0755', true);
+        }
+        $fileName = $fileName . ".xlsx";
+        $file = $dir . $fileName;
+        $SpreadSheet = new Spreadsheet();
+        $sheet = $SpreadSheet->getActiveSheet();
+        \app\common\util\PhpSpreadsheetExportV2::write($sheet, $data, $tableHeader, $mergeCells);
+
+        \app\common\util\PhpSpreadsheetExportV2::saveFile($SpreadSheet, $file);
+
+        $url = self::get_domain() . getVirRootDir() . $path . $fileName;
+        return ['url' => $url, 'file' => $file];
+    }
+
+    /**
+     * 写入数据
+     * @param $sheet
+     * @param $data
+     * @param $tableHeader
+     * @param $mergeCells
+     * @return bool
+     */
+    public static function write($sheet, $data, $tableHeader, $mergeCells): bool
+    {
+        $totalCol = 0;
+
+        //设置表头合并单元格
+        foreach ($mergeCells as $row => $rows) {
+            $i = 0;
+            foreach ($rows as $col => $colValue) {
+                //合并单元格
+                $sheet->mergeCells($col);
+                //设置样式
+                self::setStyle($sheet, $i, $totalCol, $row);
+                //单元格内容写入
+                $sheet->setCellValue(substr($col, 0, strpos($col, ":")), $colValue);
+                $i++;
+            }
+        }
+        $totalCol = count($mergeCells);
+
+        //设置表头
+        foreach ($tableHeader as $row => $rows) {
+            $headerRowDatas = array_values($rows);
+            foreach ($headerRowDatas as $col => $colValue) {
+                //设置样式
+                self::setStyle($sheet, $col, $totalCol, $row);
+                //单元格内容写入
+                $sheet->getCell(self::$excelCol[$col] . ($totalCol + $row + 1))->setValueExplicit($colValue,'s');//统一格式化为字符串
+//                $sheet->setCellValue(self::$excelCol[$col] . ($totalCol + $row + 1), $colValue);
+            }
+        }
+        $totalCol += count($tableHeader);
+
+        //设置内容
+        foreach ($data as $row => $rows) {
+            $rowDatas = array_values($rows);
+            foreach ($rowDatas as $col => $colValue) {
+                // 单元格内容写入
+//                $sheet->setCellValue(self::$excelCol[$col] . ($totalCol + $row + 1), $colValue);
+                $sheet->getCell(self::$excelCol[$col] . ($totalCol + $row + 1))->setValueExplicit($colValue,'s');//统一格式化为字符串
+            }
+        }
+        return true;
+    }
+
+    /**
+     * 设置单元格样式
+     * @param $sheet //某个sheet
+     * @param $col //某列
+     * @param $totalCol //总行数
+     * @param $row //某行
+     */
+    public static function setStyle($sheet, $col, $totalCol, $row): void
+    {
+        //设置单元格居中
+        $sheet->getStyle(self::$excelCol[$col] . ($totalCol + $row + 1))->applyFromArray(self::$styleArray);
+        //设置单元格
+        $sheet->getStyle(self::$excelCol[$col] . ($totalCol + $row + 1))
+            ->getFill()
+            ->setFillType(Fill::FILL_SOLID)
+            ->getStartColor()
+            ->setRGB(self::$setBgRGB);
+        //设置单元格字体样式、字体、字体大小
+        $sheet->getStyle(self::$excelCol[$col] . ($totalCol + $row + 1))
+            ->getFont()
+            ->setBold(self::$setBold)
+            ->setName(self::$setName)
+            ->setSize(self::$setSize);
+        //设置字体颜色
+        $sheet->getStyle(self::$excelCol[$col] . ($totalCol + $row + 1))
+            ->getFont()
+            ->getColor()->setRGB(self::$setFontRGB);
+    }
+
+    /**
+     * 获取当前网站的域名地址
+     * 
+     * @return string 域名地址
+     */
+    protected static function get_domain()
+    {
+        $sys_protocal = isset($_SERVER['SERVER_PORT']) && $_SERVER['SERVER_PORT'] == '443' ? 'https://' : 'http://';
+        return $sys_protocal . (isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : '');
+    }
+}

+ 76 - 0
api/app/common/util/PhpSpreadsheetImport.php

@@ -0,0 +1,76 @@
+<?php
+
+namespace app\common\util;
+
+use PhpOffice\PhpSpreadsheet\Exception;
+use PhpOffice\PhpSpreadsheet\IOFactory;
+use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
+
+/**
+ * Excel表格功能封装类
+ */
+class PhpSpreadsheetImport
+{
+    /**
+     * 读取表格数据
+     * @param string $path 路径
+     * @param int $sheetIndex 工作簿索引
+     * @param array $field 初始化数组键名
+     * @param int $row 从第几行开始读
+     * @param string $scene
+     * @return array
+     */
+    public static function readData(string $path, array $field = [], int $row = 2, int $sheetIndex = 0, string $scene = 'excel'): array
+    {
+        try {
+            // 创建读操作对象
+            $reader = IOFactory::createReader('Xlsx');
+            // 忽略任何格式的信息
+            $reader->setReadDataOnly(true);
+            // 打开文件、载入excel表格
+            $spreadsheet = $reader->load($path);
+            // 获取活动工作薄
+            $sheet = $spreadsheet->getSheet($sheetIndex);
+            // 返回表格数据
+            return self::getCellData($row, $sheet, $field);
+        } catch (\Exception $e) {
+            // 有异常发生
+            return ['code' => $e->getCode(), 'errMsg' => $e->getMessage()];
+        }
+    }
+
+    /**
+     * 获取单元格数据
+     * @param $row
+     * @param object $sheet
+     * @param array $field
+     * @return array
+     * @throws Exception
+     */
+    private static function getCellData($row, object $sheet, array $field):array
+    {
+        # 获取最高列 返回字母 如: C
+        $highestColumn = $sheet->getHighestColumn();
+        # 获取最大行 返回数字 如: 4
+        $highestRow = $sheet->getHighestRow();
+        # 列数 改为数字显示
+        $highestColumnIndex = Coordinate::columnIndexFromString($highestColumn);
+        $data = [];
+        // 从第二行开始读取数据
+        for ($row; $row <= $highestRow; $row++) {
+            $build = [];
+            // 从第一列读取数据
+            for ($col = 1; $col <= $highestColumnIndex; $col++) {
+                // 'A' 对应的ASCII码十进制为 64
+                // 将ASCII值转为字符
+                $chr = chr(64 + $col);
+                // 列转为数据库字段名
+                $key = $field[$chr] ?? $chr;
+                // 构建当前行数据
+                $build[$key] = $sheet->getCellByColumnAndRow($col, $row)->getValue();
+            }
+            $data[] = $build; //当前行数据
+        }
+        return $data;
+    }
+}

+ 43 - 0
api/app/common/util/Power.php

@@ -0,0 +1,43 @@
+<?php
+
+
+namespace app\common\util;
+
+class Power
+{
+    /**
+     * 获取一维权限列表
+     * @param string $apCodes
+     * @return array|mixed
+     */
+    public function getPowerList(string $apCodes = ""): mixed
+    {
+        $power = config('power');
+        if (!empty($apCodes)) {
+            foreach ($power as $k => $v) {
+                if (strpos($apCodes, ',' . $v ['id'] . ',')) {
+                    $power[$k]['checked'] = true;
+                }
+            }
+        }
+        return $power;
+    }
+
+    /**
+     * 获取二维维权限列表
+     * @param string $pkey
+     * @param array $power
+     * @return array
+     */
+    public function getPowerListV2(string $pkey, array $power): array
+    {
+        $resData = array();
+        foreach ($power as $k => $v) {
+            if ($pkey === $v['pId']) {
+                unset($power[$k]);
+                $resData['children'][] = array_merge($v, $this->getPowerListV2($v['id'], $power));
+            }
+        }
+        return $resData;
+    }
+}

+ 57 - 0
api/app/common/util/QrCode.php

@@ -0,0 +1,57 @@
+<?php
+
+namespace app\common\util;
+
+use Endroid\QrCode\Builder\Builder;
+use Endroid\QrCode\Encoding\Encoding;
+use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelHigh;
+use Endroid\QrCode\Label\Alignment\LabelAlignmentCenter;
+use Endroid\QrCode\Label\Font\NotoSans;
+use Endroid\QrCode\RoundBlockSizeMode\RoundBlockSizeModeMargin;
+use Endroid\QrCode\Writer\PngWriter;
+use Endroid\QrCode\Color\Color;
+use Endroid\QrCode\Label\Margin\Margin;
+
+class QrCode
+{
+    public static function createEwm($data): string
+    {
+        $code = Builder::create()
+            ->writer(new PngWriter())
+            ->writerOptions([])
+            ->data($data)   //文本或url地址
+//            ->errorCorrectionLevel(new ErrorCorrectionLevelHigh())
+//            ->size(500)     //二维码大小
+//
+//            ->margin(10)    //外边距
+//
+//            ->foregroundColor($color)    //二维码颜色
+//
+//            ->backgroundColor($color1)    //背景颜色
+//
+//            ->logoResizeToWidth('80')    //logo宽
+//
+//            ->logoResizeToHeight('80')    //logo高
+//
+//            ->roundBlockSizeMode(new RoundBlockSizeModeMargin())
+//            ->logoPath(__DIR__ . '/assets/symfony.png')     //logo图片位置
+//
+//            ->labelText('二维码')     //标题文字
+//
+//            ->labelMargin($mar)     //标题文字的外边距
+//
+//            ->labelTextColor($color3)     //标题文字颜色
+//
+//            ->labelBackgroundColor($color2)   //标题背景颜色
+//
+//            ->labelFont(new NotoSans(20))
+//            ->labelAlignment(new LabelAlignmentCenter())
+            ->build();
+        // 设置页面文本类型
+//        header('content-type:'.$code->getMimeType());
+//        // 二维码保存位置
+        $code->saveToFile('qrcode.png');
+//        // 生成图像数据url
+        return $code->getDataUri();
+    }
+}

+ 89 - 0
api/app/common/util/Result.php

@@ -0,0 +1,89 @@
+<?php
+
+namespace app\common\util;
+
+use think\Model;
+
+/**
+ * 通用返回值
+ */
+class Result
+{
+    /**
+     * $code 业务码
+     *
+     * @var mixed
+     */
+    protected $code = -1;
+
+    /**
+     * $data 数据
+     *
+     * @var mixed
+     */
+    protected $data;
+
+    /**
+     * $msg 提示消息
+     *
+     * @var string
+     */
+    protected $msg = 'Never init error';
+
+    public function __construct($data,  $code = 0, $msg = '')
+    {
+        $this->code = $code;
+        $this->data = $data;
+        $this->msg = $msg;
+    }
+
+    public static function of($data, $code = 0, $msg = 'success')
+    {
+        return new Result($data, $code, $msg);
+    }
+
+    public static function success()
+    {
+        return new Result(null, 0, 'success');
+    }
+
+    public static function failed($code = -1, $msg = 'failed')
+    {
+        return new Result(null, $code, $msg);
+    }
+
+    public function toArray() {
+        return [
+            'code' => $this->code,
+            'msg' => $this->msg,
+            'data' => $this->data,
+        ];
+    }
+
+    /**
+     * 生成 Http Json Result 数据
+     *
+     * @param mixed $data
+     * @param int $code
+     * @param string $msg
+     *
+     * @return \think\Response\Json
+     */
+    public static function rest($data = null, $code = 0, $msg = 'success')
+    {
+        return json(self::of($data, $code, $msg)->toArray());
+    }
+
+    /**
+     * 生成 Http Json Result 失败的数据
+     *
+     * @param int $code
+     * @param string $msg
+     *
+     * @return \think\Response\Json
+     */
+    public static function restf($code, $msg)
+    {
+        return self::rest(null, $code, $msg);
+    }
+}

+ 135 - 0
api/app/common/util/Rsa.php

@@ -0,0 +1,135 @@
+<?php
+/**
+ * Created by PhpStorm.
+ * User: yckj_lzj
+ * Date: 2019-10-22
+ * Time: 19:27
+ */
+
+namespace app\common\util;
+
+
+class Rsa extends SingleObjectClass
+{
+    protected $priKeyStr;
+    protected $pubKeyStr;
+
+    public  function setPriPubKey($pubKey,$priKey){
+        $this->priKeyStr=$priKey;
+        $this->pubKeyStr=$pubKey;
+//        $this->$pubKeyStr=$pubKey;
+    }
+
+    public function setPriKey($priKey){
+        $this->priKeyStr=$priKey;
+    }
+
+    public function setPubKey($pubKey){
+        $this->pubKeyStr=$pubKey;
+    }
+
+    private  function getPriKeyStr()
+    {
+        return "-----BEGIN PRIVATE KEY-----\n".$this->priKeyStr."\n-----END PRIVATE KEY-----";
+    }
+
+    public  function getPubKeyStr(){
+        return "-----BEGIN PUBLIC KEY-----\n".$this->pubKeyStr."\n-----END PUBLIC KEY-----";
+    }
+
+
+    /**
+     * 获取公钥     *
+     * @return bool|resource
+     */
+    private  function getPublicKey()
+    {
+
+
+        $content = $this->getPubKeyStr();
+
+        return openssl_pkey_get_public($content);
+    }
+
+    /**
+     * 获取私钥
+     * @return bool|resource
+     */
+    private  function getPrivateKey()
+    {
+
+        $content = $this->getPriKeyStr();
+//        echo $content;
+        return openssl_pkey_get_private($content);
+    }
+
+
+    /**
+     * 私钥加密
+     * @param $signString
+     * @return string
+     */
+    public  function priSign($signString){
+        $privKeyId = $this->getPrivateKey();
+        $signature = '';
+        openssl_sign($signString, $signature, $privKeyId);
+        openssl_free_key($privKeyId);
+        return base64_encode($signature);
+
+    }
+
+
+    /**
+     * 私钥加密
+     * @param string $data
+     * @return null|string
+     */
+    public  function privEncrypt($data = '')
+    {
+
+        if (!is_string($data)) {
+            return null;
+        }
+        return openssl_private_encrypt($data, $encrypted, $this->getPrivateKey()) ? base64_encode($encrypted) : null;
+    }
+
+    /**
+     * 公钥加密
+     * @param string $data
+     * @return null|string
+     */
+    public  function publicEncrypt($data = '')
+    {
+        if (!is_string($data)) {
+            return null;
+        }
+        var_dump($this->getPublicKey());
+        return openssl_public_encrypt($data, $encrypted, $this->getPublicKey()) ? base64_encode($encrypted) : null;
+    }
+
+    /**
+     * 私钥解密
+     * @param string $encrypted
+     * @return null
+     */
+    public  function privDecrypt($encrypted = '')
+    {
+        if (!is_string($encrypted)) {
+            return null;
+        }
+        return (openssl_private_decrypt(base64_decode($encrypted), $decrypted, $this->getPrivateKey())) ? $decrypted : null;
+    }
+
+    /**
+     * 公钥解密
+     * @param string $encrypted
+     * @return null
+     */
+    public  function publicDecrypt($encrypted = '')
+    {
+        if (!is_string($encrypted)) {
+            return null;
+        }
+        return (openssl_public_decrypt(base64_decode($encrypted), $decrypted, $this->getPublicKey())) ? $decrypted : null;
+    }
+}

+ 85 - 0
api/app/common/util/Salary.php

@@ -0,0 +1,85 @@
+<?php
+
+namespace app\common\util;
+
+use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
+
+/**
+ * 表达式操作类
+ */
+class Salary
+{
+    /**
+     * 计算
+     * @param string $expression
+     * @return array
+     */
+    public function evaluate(string $expression)
+    {
+        $language = new ExpressionLanguage();
+        try {
+            $result = $language->evaluate($expression);
+            return ['code' => 0, 'msg' => '计算成功','data'=>$result];
+        } catch (\Exception $exception) {
+            return ['code' => 999, 'msg' => '公式格式不正确,请检查'];
+        }
+    }
+
+    /**
+     * @return void
+     */
+    public function compile(): void
+    {
+        $language = new ExpressionLanguage();
+        var_dump($language->compile('1 + 2')); // displ
+    }
+
+      /**
+     * @param $value
+     * @param $num
+     * @return array
+     */
+    function checkSalaryItem($value, $num): array
+    {
+        $bj_num = substr_count($value, '#');
+        if ($bj_num % 2 == 0) {
+            if ($bj_num == 0 && $num > 0) {
+                return \app\facade\Salary::evaluate($value);
+            }
+            if (!str_contains($value, '#')) {
+                return \app\facade\Salary::evaluate($value);
+            }
+
+            $var = $this->getBetweenAB($value);
+            $res2 = (new SalaryItemModel)->detail(['variable' => $var]);
+            if (!$res2) {
+                return ['code' => 999, 'msg' => '公式错误,变量' . $var . '未找到'];
+            }
+            $value = str_replace('#' . $var . '#', $res2->value, $value);
+            $num += 1;
+            return $this->checkSalaryItem($value, $num);
+        } else {
+            return ['code' => 999, 'msg' => '公式错误,请检查变量#符号数量是否准确'];
+        }
+    }
+
+    /**
+     * @param $str
+     * @param string $begin
+     * @param string $end
+     * @return string
+     */
+    private function getBetweenAB($str, string $begin = "#", string $end = "#"): string
+    {
+        if ($begin == '') return '';
+        $beginPos = mb_strpos($str, $begin);
+        if ($beginPos === false) return '';       // 起始字符不存在,直接返回空。合理
+        $start = $beginPos + mb_strlen($begin);       // 1.1、开始截取下标
+        if ($end == '') $endPos = mb_strlen($str);// 结束字符不存在,默认截取到字符串末尾。合理
+        else $endPos = mb_strpos($str, $end, $start); // 1.2、从开始下标之后查找
+        if ($endPos === false) $endPos = mb_strlen($str);
+        $length = $endPos - $start;                   // 2、截取字符的长度
+        return mb_substr($str, $start, $length);
+    }
+}
+

+ 59 - 0
api/app/common/util/SingleObjectClass.php

@@ -0,0 +1,59 @@
+<?php
+/**
+ * Created by PhpStorm.
+ * User: yckj_lzj
+ * Date: 2019-08-21
+ * Time: 16:38
+ */
+
+namespace app\common\util;
+
+
+use think\Log;
+
+class SingleObjectClass
+{
+    protected $openLog=true;
+    //输出结果
+    protected function writeln($info, $type = "debug")
+    {
+        if ($this->openLog) {
+            Log::write(print_r($info, true), $type);
+        }
+    }
+
+    /**
+     * 返回内容
+     * @param $code
+     * @param $data
+     */
+    protected function getResult($code, $data = null)
+    {
+        $result['code'] = $code;
+        $result['msg'] = ActQuestionErrCode::$errInfo[$code];
+        if ($data) {
+            $result['data'] = $data;
+        }
+        return $result;
+    }
+
+    /**
+     * 类实例化(单例模式)
+     */
+    public static function instance()
+    {
+        static $_instance = array();
+
+        $classFullName = get_called_class();
+        if (!isset($_instance[$classFullName])) {
+            // $_instance[$classFullName] = new $classFullName();
+            // 1、先前这样写的话,PhpStrom 代码提示功能失效;
+            // 2、并且中间变量不能是 数组,如 不能用 return $_instance[$classFullName] 形式返回实例对象,否则 PhpStrom 代码提示功能失效;
+            $instance = $_instance[$classFullName] = new static();
+            return $instance;
+        }
+
+        return $_instance[$classFullName];
+    }
+
+}

+ 64 - 0
api/app/common/util/Tree.php

@@ -0,0 +1,64 @@
+<?php
+
+namespace app\common\util;
+class Tree
+{
+    /**
+     * 示例数组
+     * @var array|array[]
+     */
+    public array $array = [
+        ['id' => 1, 'name' => 'ha1', 'parent_id' => 0],
+        ['id' => 2, 'name' => 'ha2', 'parent_id' => 0],
+        ['id' => 3, 'name' => 'ha3', 'parent_id' => 0],
+        ['id' => 4, 'name' => 'ha4', 'parent_id' => 1],
+        ['id' => 5, 'name' => 'ha5', 'parent_id' => 1],
+        ['id' => 6, 'name' => 'ha6', 'parent_id' => 2],
+    ];
+
+    /**
+     * 1级转1维数组无限极分类
+     * @param array $array
+     * @param int $parent_id
+     * @return array
+     */
+    public function getTree(array $array, int $parent_id = 0): array
+    {
+        static $data = [];
+        foreach ($array as $key => $value) {
+            if ($value['parent_id'] == $parent_id) {
+                $data[] = $value;
+                unset($array[$key]);
+                $this->getTree($array, $value['id']);
+            }
+        }
+        return $data;
+    }
+
+    /**
+     * 1级转2维数组数无限极分类
+     * @param array $array
+     * @return array
+     */
+    public function getTreeV2(array $array): array
+    {
+        $refer = [];
+        $tree = [];
+        foreach ($array as $key => $val) {
+            $refer[$val['id']] = &$array[$key];
+            $refer[$val['id']]['label'] = $val['name'];
+        }
+        foreach ($array as $k => $v) {
+            $pid = $v['parent_id']; //获取当前分类的父级id
+            if ($pid == 0) {
+                $tree[] = &$array[$k]; //顶级栏目
+            } else {
+                if (isset($refer[$pid])) {
+                    $refer[$pid]['children'][] = &$array[$k]; //如果存在父级栏目,则添加进父级栏目的子栏目数组中
+                }
+            }
+        }
+        return $tree;
+    }
+
+}

+ 82 - 0
api/app/common/util/TxySms.php

@@ -0,0 +1,82 @@
+<?php
+namespace app\common\util;
+
+// 导入对应产品模块的client
+use TencentCloud\Sms\V20210111\SmsClient;
+// 导入要请求接口对应的Request类
+use TencentCloud\Sms\V20210111\Models\SendSmsRequest;
+use TencentCloud\Common\Exception\TencentCloudSDKException;
+use TencentCloud\Common\Credential;
+// 导入可选配置类
+use TencentCloud\Common\Profile\ClientProfile;
+use TencentCloud\Common\Profile\HttpProfile;
+
+class TxySms
+{
+    //发送短信
+    public static function SendSms($template_code,$phone_all){
+        try {
+            /* 必要步骤:
+             * 实例化一个认证对象,入参需要传入腾讯云账户密钥对secretId,secretKey。
+             * 这里采用的是从环境变量读取的方式,需要在环境变量中先设置这两个值。
+             * 以免泄露密钥对危及你的财产安全。
+             * SecretId、SecretKey 查询: https://console.cloud.tencent.com/cam/capi */
+            $cred = new Credential(config('tencentcloud.SecretId'), config('tencentcloud.SecretKey'));
+
+            // 实例化一个http选项,可选的,没有特殊需求可以跳过
+            $httpProfile = new HttpProfile();
+            // 配置代理(无需要直接忽略)
+            // $httpProfile->setProxy("https://ip:port");
+            $httpProfile->setReqMethod("GET");  // post请求(默认为post请求)
+            $httpProfile->setReqTimeout(30);    // 请求超时时间,单位为秒(默认60秒)
+            $httpProfile->setEndpoint("sms.tencentcloudapi.com");  // 指定接入地域域名(默认就近接入)
+
+            // 实例化一个client选项,可选的,没有特殊需求可以跳过
+            $clientProfile = new ClientProfile();
+            $clientProfile->setSignMethod("TC3-HMAC-SHA256");  // 指定签名算法(默认为HmacSHA256)
+            $clientProfile->setHttpProfile($httpProfile);
+
+            // 实例化要请求产品(以sms为例)的client对象,clientProfile是可选的
+            // 第二个参数是地域信息,可以直接填写字符串ap-guangzhou,支持的地域列表参考 https://cloud.tencent.com/document/api/382/52071#.E5.9C.B0.E5.9F.9F.E5.88.97.E8.A1.A8
+            $client = new SmsClient($cred, "ap-guangzhou", $clientProfile);
+
+            // 实例化一个 sms 发送短信请求对象,每个接口都会对应一个request对象。
+            $req = new SendSmsRequest();
+
+
+
+            /* 短信应用ID: 短信SdkAppId在 [短信控制台] 添加应用后生成的实际SdkAppId,示例如1400006666 */
+            $req->SmsSdkAppId = config('sms.SdkAppId');
+            /* 短信签名内容: 使用 UTF-8 编码,必须填写已审核通过的签名 */
+            $req->SignName = config('sms.sign');
+            /* 模板 ID: 必须填写已审核通过的模板 ID */
+            $req->TemplateId = config('sms.template_id');
+            /* 模板参数: 模板参数的个数需要与 TemplateId 对应模板的变量个数保持一致,若无模板参数,则设置为空*/
+            $req->TemplateParamSet = [$template_code];//数组多个参数
+            /* 下发手机号码,采用 E.164 标准,+[国家或地区码][手机号]
+             * 示例如:+8613711112222, 其中前面有一个+号 ,86为国家码,13711112222为手机号,最多不要超过200个手机号*/
+            $req->PhoneNumberSet = ['+86'.$phone_all];//数组可以发多个
+            /* 用户的 session 内容(无需要可忽略): 可以携带用户侧 ID 等上下文信息,server 会原样返回 */
+            $req->SessionContext = "";
+            /* 短信码号扩展号(无需要可忽略): 默认未开通,如需开通请联系 [腾讯云短信小助手] */
+            $req->ExtendCode = "";
+            /* 国际/港澳台短信 SenderId(无需要可忽略): 国内短信填空,默认未开通,如需开通请联系 [腾讯云短信小助手] */
+            $req->SenderId = "";
+
+            // 通过client对象调用SendSms方法发起请求。注意请求方法名与请求对象是对应的
+            // 返回的resp是一个SendSmsResponse类的实例,与请求对象对应
+            $resp = $client->SendSms($req);
+
+            // 输出json格式的字符串回包
+            $json_data = $resp->toJsonString();
+            $arr_data = json_decode($json_data,true);
+            return $arr_data;
+        }
+        catch(TencentCloudSDKException $e) {
+            echo $e->getMessage();
+        }
+    }
+
+
+}
+

+ 31 - 0
api/app/common/util/Upload.php

@@ -0,0 +1,31 @@
+<?php
+namespace app\common\util;
+class Upload
+{
+    public function file(): array
+    {
+        $file = request()->file('file');
+        // 上传到本地服务器
+        $rename = \think\facade\Filesystem::disk('public')->putFile('topic', $file);
+        $rename = str_replace('\\', '/', $rename);
+        $url = request()->domain() . getVirRootDir() . '/storage/' . $rename;
+
+        $file_path = app()->getRootPath() . 'public/storage/' . $rename;
+        $file_info = mime_content_type($file_path);
+
+        if (str_contains($file_info, 'image')) {
+            $attr = getimagesize($file_path);
+            $size = filesize($file_path);
+            if ($size / 1000 >= 700 || $attr[0] >= 900) {
+                $img_compress = new \app\common\util\ImgCompress($file_path, 0.5);
+                $img_compress->compressImg($file_path);
+            }
+        }
+
+        return [
+            "file" => $rename,
+            "url" => $url,
+            "size"=>filesize($file_path)
+        ];
+    }
+}

+ 130 - 0
api/app/common/util/Util.php

@@ -0,0 +1,130 @@
+<?php
+/**
+ * Created by PhpStorm.
+ * User: yckj-yf7
+ * Date: 2021/9/24
+ * Time: 17:25
+ */
+
+namespace app\common\util;
+
+/**
+ * 工具类
+ * @package app\common\util
+ */
+class Util
+{
+    /**
+     * 获取一定配置下的父子级的数据集合
+     * @param $list 一维的数据集合
+     * @param array $option 配置
+     *              field 判断子级的字段名,默认为id
+     *              parent_field 判断父级的字段名,默认为pid
+     *              children 写入返回值的字段名,默认为children
+     *              default 默认搜索开始的数值,默认为0
+     * @return array|mixed
+     */
+    public static function getChildrenList($list, $option = []){
+        $field = isset($option['field'])?$option['field']:'id';
+        $parent_field = isset($option['parent_field'])?$option['parent_field']:'pid';
+        $children_name = isset($option['children'])?$option['children']:'children';
+        $default = isset($option['default'])?$option['default']:0;
+
+        //按父级生成索引数组
+        $index_data = [];
+        foreach ($list as $item){
+            $index_data[$item[$parent_field]][] = $item;
+        }
+        unset($item);
+
+        return self::setChildren($index_data,$default,$field,$children_name);
+    }
+
+    /**
+     * 写入子集
+     * @param $index_data 索引数组
+     * @param $pid 父级数值
+     * @param $field 关联字段名
+     * @param $children_name 写入的子集字段名
+     * @return array|mixed
+     */
+    private static function setChildren($index_data, $pid, $field, $children_name){
+        if (isset($index_data[$pid])){
+            $list = $index_data[$pid];
+            foreach ($list as &$item){
+                $children = self::setChildren($index_data,$item[$field],$field, $children_name);
+                if (count($children)>0){
+                    $item[$children_name] = $children;
+                }
+            }
+            return $list;
+        }else{
+            return [];
+        }
+    }
+
+    /**
+     * 获取两个日期之间的日期数组
+     * @param $start_time
+     * @param $end_time
+     * @return mixed
+     */
+    public static function getPeriodDate($start_time,$end_time){
+        $start_time = strtotime($start_time);
+        $end_time = strtotime($end_time);
+        $i = 0;
+        $arr = [];
+        while ($start_time <= $end_time){
+            $arr[$i] = date('Y-m-d',$start_time);
+            $start_time = strtotime('+1 day',$start_time);
+            $i++;
+        }
+
+        return $arr;
+    }
+
+    /**
+     * 根据数字转换成excel的字母
+     * @param $num
+     * @return string
+     */
+    public static function numToExcelLetter($num)
+    {
+        //由于大写字母只有26个,所以基数为26
+        $base = 26;
+        $result = '';
+        while ($num > 0 ) {
+            $mod = (int)($num % $base);
+            $num = (int)($num / $base);
+
+            if($mod == 0){
+                $num -= 1;
+                $temp = self::numToLetter($base) . $result;
+            } elseif ($num == 0) {
+                $temp = self::numToLetter($mod) . $result;
+            } else {
+                $temp = self::numToLetter($mod) . $result;
+            }
+            $result = $temp;
+        }
+
+        return $result;
+    }
+
+    /**
+     * 数字转字母
+     * @param $num
+     * @return string
+     */
+    public static function numToLetter($num)
+    {
+        if ($num == 0) {
+            return '';
+        }
+
+        $num = (int)$num - 1;
+        //获取A的ascii码
+        $ordA = ord('A');
+        return chr($ordA + $num);
+    }
+}

+ 212 - 0
api/app/common/util/WhereBuilder.php

@@ -0,0 +1,212 @@
+<?php
+
+namespace app\common\util;
+
+/**
+ * where语句构建器
+ */
+class WhereBuilder implements \ArrayAccess
+{
+    private $where = [];
+
+    public static function builder()
+    {
+        return new WhereBuilder;
+    }
+
+    /**
+     * 添加条件
+     *
+     * @param string|\think\db\Raw $column 数据库字段名
+     * @param string $expr 表达式
+     * @param mixed $condition 查询条件
+     * @param bool $when 为false时不添加
+     * 
+     * @return \app\common\util\WhereBuilder
+     */
+    public function where($column, $expr = null, $condition = null, $when = true)
+    {
+        if ($when) {
+            $this->where[] = [$column, $expr, $condition];
+        }
+        return $this;
+    }
+
+    /**
+     * 一对一对应的构建,条件为空时不添加
+     *
+     * @param string $column 数据库字段名
+     * @param string $expr 表达式
+     * @param mixed $condition 查询条件
+     * 
+     * @return \app\common\util\WhereBuilder
+     */
+    public function corres($column, $expr, $condition)
+    {
+        if (!empty($condition)) {
+            $this->where($column, $expr, $condition);
+        }
+        return $this;
+    }
+
+    /**
+     * 简单eq
+     *
+     * @param mixed $column 数据库字段名
+     * @param mixed $condition 表达式
+     * @param bool $when 为true时一定会添加,其余时刻判断数据是否为空
+     * 
+     * @return \app\common\util\WhereBuilder
+     */
+    public function eq($column, $condition, $when = false)
+    {
+        $valid = false;
+        if (is_int($condition)) {
+            $valid = isset($condition);
+        } elseif (is_string($condition)) {
+            $valid = !empty($condition) || $condition === '0';
+        } else {
+            $valid = !empty($condition);
+        }
+        $this->where($column, '=', $condition, $when || $valid);
+        return $this;
+    }
+
+    /**
+     * 简单not eq
+     *
+     * @param mixed $column 数据库字段名
+     * @param mixed $condition 表达式
+     * @param bool $when 为true时一定会添加,其余时刻判断数据是否为空
+     * 
+     * @return \app\common\util\WhereBuilder
+     */
+    public function neq($column, $condition, $when = false)
+    {
+        $valid = false;
+        if (is_int($condition)) {
+            $valid = isset($condition);
+        } elseif (is_string($condition)) {
+            $valid = !empty($condition) || $condition === '0';
+        } else {
+            $valid = !empty($condition);
+        }
+        $this->where($column, '<>', $condition, $when || $valid);
+        return $this;
+    }
+
+    /**
+     * %like%
+     *
+     * @param mixed $column 数据库字段名
+     * @param mixed $condition 表达式
+     * @param bool $when 为true时一定会添加,其余时刻判断数据是否为空
+     * 
+     * @return \app\common\util\WhereBuilder
+     */
+    public function like($column, ?string $condition = '', $when = false)
+    {
+        $condition = $condition ?: '';
+        $this->where($column, 'like', "%{$condition}%", $when || !empty($condition));
+        return $this;
+    }
+
+    public function in($column, ?array $condition = [], $exclude = [], $when = false)
+    {
+        $condition = $condition ?: [];
+        $condition = array_diff($condition, $exclude);
+        $this->where($column, 'in', $condition, $when || !empty($condition));
+        return $this;
+    }
+
+    public function isnull($column, $when = false)
+    {
+        $this->where($column, 'null', null, $when);
+        return $this;
+    }
+
+    public function notnull($column, $when = false)
+    {
+        $this->where($column, 'not null', null, $when);
+        return $this;
+    }
+
+    public function between($column, $left, $right, $when = false)
+    {
+        if ($when || (self::empty($left) && self::empty($right))) {
+            $this->where($column, 'between', [$left, $right]);
+            return $this;
+        } elseif (self::empty($left) && !self::empty($right)) {
+            $this->where($column, '>=', $left);
+            return $this;
+        } elseif (!self::empty($left) && self::empty($right)) {
+            $this->where($column, '<=', $left);
+            return $this;
+        } else {
+            return $this;
+        }
+    }
+
+    /**
+     * 直接添加
+     *
+     * @param array $where
+     * 
+     * @return \app\common\util\WhereBuilder
+     */
+    public function push($where, $when = true)
+    {
+        if (!$when) {
+            return $this;
+        }
+        $this->where = array_merge($this->where, $where);
+        return $this;
+    }
+
+    /**
+     * 构建where
+     *
+     * @return array
+     */
+    public function build()
+    {
+        return $this->where;
+    }
+
+    public function offsetSet($offset, $value)
+    {
+        if (is_null($offset)) {
+            $this->where[] = $value;
+        } else {
+            $this->where[$offset] = $value;
+        }
+    }
+
+    public function offsetExists($offset)
+    {
+        return isset($this->where[$offset]);
+    }
+
+    public function offsetUnset($offset)
+    {
+        unset($this->where[$offset]);
+    }
+
+    public function offsetGet($offset)
+    {
+        return isset($this->where[$offset]) ? $this->where[$offset] : null;
+    }
+
+    private static function empty($condition)
+    {
+        $valid = false;
+        if (is_int($condition)) {
+            $valid = isset($condition);
+        } elseif (is_string($condition)) {
+            $valid = !empty($condition) || $condition === '0';
+        } else {
+            $valid = !empty($condition);
+        }
+        return $valid;
+    }
+}

+ 31 - 0
api/app/common/util/WxTemplate.php

@@ -0,0 +1,31 @@
+<?php
+namespace app\common\util;
+
+class WxTemplate
+{
+    /**
+     * 发送消息模板
+     */
+    public static function templateMessageSend($openid, $phone, $code, $order_no)
+    {
+        $send_data = [
+            'touser' => $openid,//openid
+            'template_id' => 'rh9v7reaIwQ-PQaXT536Y0nSgQsP1jq4-8SnEyP9KZc',//模板id
+            'miniprogram_state'=>'trial',
+            'miniprogram' => [
+                'appid' => config('wechat.app_id'),
+                'pagepath' => 'pages/index/index',
+            ],
+            'data' => [
+                'first' => '【' . $phone . '】快递代收成功',
+                'keyword1' => '宇晨科技保安亭旁快递柜',
+                'keyword2' => $code,
+                'keyword3' => $order_no,
+                'keyword4' => getNow(),
+            ],
+        ];
+        $hpRose = new YcApiHprose();
+        $hpRose->templateMessageSend($send_data);
+    }
+}
+

+ 0 - 0
api/app/common/util/YcApiHprose.php


Einige Dateien werden nicht angezeigt, da zu viele Dateien in diesem Diff geändert wurden.