任敬轩 4 anni fa
commit
494b205447
100 ha cambiato i file con 41340 aggiunte e 0 eliminazioni
  1. 15 0
      .editorconfig
  2. 59 0
      .env.docker
  3. 57 0
      .env.example
  4. 5 0
      .gitattributes
  5. 29 0
      .gitignore
  6. 13 0
      .styleci.yml
  7. 661 0
      LICENSE
  8. 59 0
      README-EN.md
  9. 60 0
      README.md
  10. 18112 0
      _ide_helper.php
  11. 41 0
      app/Console/Kernel.php
  12. 28 0
      app/Events/ServerStartEvent.php
  13. 24 0
      app/Events/WorkerStartEvent.php
  14. 62 0
      app/Exceptions/Handler.php
  15. 260 0
      app/Http/Controllers/Api/ChatController.php
  16. 680 0
      app/Http/Controllers/Api/DocsController.php
  17. 2793 0
      app/Http/Controllers/Api/ProjectController.php
  18. 400 0
      app/Http/Controllers/Api/ReportController.php
  19. 289 0
      app/Http/Controllers/Api/SystemController.php
  20. 586 0
      app/Http/Controllers/Api/UsersController.php
  21. 10 0
      app/Http/Controllers/Api/apidoc.json
  22. 89 0
      app/Http/Controllers/Api/apidoc.php
  23. 13 0
      app/Http/Controllers/Controller.php
  24. 46 0
      app/Http/Controllers/IndexController.php
  25. 66 0
      app/Http/Kernel.php
  26. 32 0
      app/Http/Middleware/ApiMiddleware.php
  27. 21 0
      app/Http/Middleware/Authenticate.php
  28. 17 0
      app/Http/Middleware/CheckForMaintenanceMode.php
  29. 17 0
      app/Http/Middleware/EncryptCookies.php
  30. 27 0
      app/Http/Middleware/RedirectIfAuthenticated.php
  31. 18 0
      app/Http/Middleware/TrimStrings.php
  32. 23 0
      app/Http/Middleware/TrustProxies.php
  33. 36 0
      app/Http/Middleware/VerifyCsrfToken.php
  34. 29 0
      app/Jobs/Timer/SystemCronJob.php
  35. 526 0
      app/Model/DBCache.php
  36. 55 0
      app/Model/DBClause.php
  37. 32 0
      app/Model/DOModel.php
  38. 2556 0
      app/Module/Base.php
  39. 144 0
      app/Module/BillExport.php
  40. 16 0
      app/Module/BillImport.php
  41. 245 0
      app/Module/Chat.php
  42. 105 0
      app/Module/Docs.php
  43. 235 0
      app/Module/Ihttp.php
  44. 220 0
      app/Module/Project.php
  45. 115 0
      app/Module/Umeng.php
  46. 350 0
      app/Module/Users.php
  47. 8469 0
      app/Module/ip/all_cn.txt
  48. 28 0
      app/Providers/AppServiceProvider.php
  49. 30 0
      app/Providers/AuthServiceProvider.php
  50. 21 0
      app/Providers/BroadcastServiceProvider.php
  51. 34 0
      app/Providers/EventServiceProvider.php
  52. 80 0
      app/Providers/RouteServiceProvider.php
  53. 468 0
      app/Services/WebSocketService.php
  54. 102 0
      app/Tasks/AutoArchivedTask.php
  55. 47 0
      app/Tasks/ChromeExtendTask.php
  56. 50 0
      app/Tasks/NotificationTask.php
  57. 27 0
      app/Tasks/PushTask.php
  58. 39 0
      app/User.php
  59. 53 0
      artisan
  60. 26 0
      bin/fswatch
  61. 28 0
      bin/inotify
  62. 165 0
      bin/laravels
  63. 61 0
      bin/wookteam
  64. 55 0
      bootstrap/app.php
  65. 2 0
      bootstrap/cache/.gitignore
  66. 101 0
      cmd
  67. 76 0
      composer.json
  68. 232 0
      config/app.php
  69. 117 0
      config/auth.php
  70. 59 0
      config/broadcasting.php
  71. 104 0
      config/cache.php
  72. 34 0
      config/cors.php
  73. 147 0
      config/database.php
  74. 85 0
      config/filesystems.php
  75. 52 0
      config/hashing.php
  76. 99 0
      config/laravels.php
  77. 104 0
      config/logging.php
  78. 109 0
      config/mail.php
  79. 89 0
      config/queue.php
  80. 33 0
      config/services.php
  81. 199 0
      config/session.php
  82. 36 0
      config/view.php
  83. 2 0
      database/.gitignore
  84. 28 0
      database/factories/UserFactory.php
  85. 43 0
      database/migrations/2020_06_05_165357_create_pre_chat_dialog_table.php
  86. 37 0
      database/migrations/2020_06_05_165357_create_pre_chat_msg_table.php
  87. 34 0
      database/migrations/2020_06_05_165357_create_pre_docs_book_table.php
  88. 36 0
      database/migrations/2020_06_05_165357_create_pre_docs_content_table.php
  89. 38 0
      database/migrations/2020_06_05_165357_create_pre_docs_section_table.php
  90. 43 0
      database/migrations/2020_06_05_165357_create_pre_project_files_table.php
  91. 34 0
      database/migrations/2020_06_05_165357_create_pre_project_label_table.php
  92. 39 0
      database/migrations/2020_06_05_165357_create_pre_project_lists_table.php
  93. 38 0
      database/migrations/2020_06_05_165357_create_pre_project_log_table.php
  94. 53 0
      database/migrations/2020_06_05_165357_create_pre_project_task_table.php
  95. 37 0
      database/migrations/2020_06_05_165357_create_pre_project_users_table.php
  96. 35 0
      database/migrations/2020_06_05_165357_create_pre_report_ccuser_table.php
  97. 33 0
      database/migrations/2020_06_05_165357_create_pre_report_content_table.php
  98. 39 0
      database/migrations/2020_06_05_165357_create_pre_report_lists_table.php
  99. 34 0
      database/migrations/2020_06_05_165357_create_pre_setting_table.php
  100. 0 0
      database/migrations/2020_06_05_165357_create_pre_users_table.php

+ 15 - 0
.editorconfig

@@ -0,0 +1,15 @@
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+insert_final_newline = true
+indent_style = space
+indent_size = 4
+trim_trailing_whitespace = true
+
+[*.md]
+trim_trailing_whitespace = false
+
+[*.{yml,yaml}]
+indent_size = 2

+ 59 - 0
.env.docker

@@ -0,0 +1,59 @@
+APP_NAME=Wookteam
+APP_ENV=local
+APP_KEY=
+APP_DEBUG=true
+APP_URL=http://localhost
+APP_PORT=8000
+APP_PORT_SSL=44300
+
+LOG_CHANNEL=stack
+
+DB_CONNECTION=mysql
+DB_HOST=mariadb
+DB_PORT=3306
+DB_DATABASE=wookteam
+DB_USERNAME=wookteam
+DB_PASSWORD=123456
+DB_ROOT_PASSWORD=123456
+DB_PREFIX=pre_
+
+BROADCAST_DRIVER=log
+CACHE_DRIVER=redis
+QUEUE_CONNECTION=redis
+SESSION_DRIVER=redis
+SESSION_LIFETIME=120
+
+REDIS_HOST=redis
+REDIS_PASSWORD=null
+REDIS_PORT=6379
+
+MAIL_MAILER=smtp
+MAIL_HOST=smtp.mailtrap.io
+MAIL_PORT=2525
+MAIL_USERNAME=null
+MAIL_PASSWORD=null
+MAIL_ENCRYPTION=null
+MAIL_FROM_ADDRESS=null
+MAIL_FROM_NAME="${APP_NAME}"
+
+AWS_ACCESS_KEY_ID=
+AWS_SECRET_ACCESS_KEY=
+AWS_DEFAULT_REGION=us-east-1
+AWS_BUCKET=
+
+PUSHER_APP_ID=
+PUSHER_APP_KEY=
+PUSHER_APP_SECRET=
+PUSHER_APP_CLUSTER=mt1
+
+UMENG_PUSH_IOS_APPKEY=
+UMENG_PUSH_IOS_APPMASTERSECRET=
+UMENG_PUSH_ANDROID_APPKEY=
+UMENG_PUSH_ANDROID_APPMASTERSECRET=
+
+MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
+MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
+
+LARAVELS_LISTEN_IP=0.0.0.0
+LARAVELS_LISTEN_PORT=5200
+LARAVELS_PROXY_URL=

+ 57 - 0
.env.example

@@ -0,0 +1,57 @@
+APP_NAME=Wookteam
+APP_ENV=local
+APP_KEY=
+APP_DEBUG=true
+APP_URL=http://localhost
+APP_PORT=80
+
+LOG_CHANNEL=stack
+
+DB_CONNECTION=mysql
+DB_HOST=127.0.0.1
+DB_PORT=3306
+DB_DATABASE=wookteam
+DB_USERNAME=root
+DB_PASSWORD=123456
+DB_PREFIX=pre_
+
+BROADCAST_DRIVER=log
+CACHE_DRIVER=file
+QUEUE_CONNECTION=sync
+SESSION_DRIVER=file
+SESSION_LIFETIME=120
+
+REDIS_HOST=127.0.0.1
+REDIS_PASSWORD=null
+REDIS_PORT=6379
+
+MAIL_MAILER=smtp
+MAIL_HOST=smtp.mailtrap.io
+MAIL_PORT=2525
+MAIL_USERNAME=null
+MAIL_PASSWORD=null
+MAIL_ENCRYPTION=null
+MAIL_FROM_ADDRESS=null
+MAIL_FROM_NAME="${APP_NAME}"
+
+AWS_ACCESS_KEY_ID=
+AWS_SECRET_ACCESS_KEY=
+AWS_DEFAULT_REGION=us-east-1
+AWS_BUCKET=
+
+PUSHER_APP_ID=
+PUSHER_APP_KEY=
+PUSHER_APP_SECRET=
+PUSHER_APP_CLUSTER=mt1
+
+UMENG_PUSH_IOS_APPKEY=
+UMENG_PUSH_IOS_APPMASTERSECRET=
+UMENG_PUSH_ANDROID_APPKEY=
+UMENG_PUSH_ANDROID_APPMASTERSECRET=
+
+MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
+MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
+
+LARAVELS_LISTEN_IP=127.0.0.1
+LARAVELS_LISTEN_PORT=5200
+LARAVELS_PROXY_URL=

+ 5 - 0
.gitattributes

@@ -0,0 +1,5 @@
+* text=auto
+*.css linguist-vendored
+*.scss linguist-vendored
+*.js linguist-vendored
+CHANGELOG.md export-ignore

+ 29 - 0
.gitignore

@@ -0,0 +1,29 @@
+/node_modules
+/public/hot
+/public/tmp
+/public/uploads/*
+/public/.well-known
+/public/.user.ini
+/storage/*.key
+/vendor
+/docker/log/
+/docker/redis/
+/docker/mariadb/
+/tmp
+._*
+.env
+.env.backup
+.idea
+.vscode
+.vagrant
+.phpunit.result.cache
+Homestead.json
+Homestead.yaml
+npm-debug.log
+yarn-error.log
+test.*
+composer.lock
+package-lock.json
+laravels-timer-process.pid
+.DS_Store
+vars.yaml

+ 13 - 0
.styleci.yml

@@ -0,0 +1,13 @@
+php:
+  preset: laravel
+  disabled:
+    - unused_use
+  finder:
+    not-name:
+      - index.php
+      - server.php
+js:
+  finder:
+    not-name:
+      - webpack.mix.js
+css: true

+ 661 - 0
LICENSE

@@ -0,0 +1,661 @@
+                    GNU AFFERO GENERAL PUBLIC LICENSE
+                       Version 3, 19 November 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+our General Public Licenses are intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+  Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
+
+  A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate.  Many developers of free software are heartened and
+encouraged by the resulting cooperation.  However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
+
+  The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community.  It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server.  Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
+
+  An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals.  This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing under
+this license.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                       TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU Affero General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Remote Network Interaction; Use with the GNU General Public License.
+
+  Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your version
+supports such interaction) an opportunity to receive the Corresponding
+Source of your version by providing access to the Corresponding Source
+from a network server at no charge, through some standard or customary
+means of facilitating copying of software.  This Corresponding Source
+shall include the Corresponding Source for any work covered by version 3
+of the GNU General Public License that is incorporated pursuant to the
+following paragraph.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the work with which it is combined will remain governed by version
+3 of the GNU General Public License.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU Affero General Public License from time to time.  Such new versions
+will be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+  Each version is given a distinguishing version number.  If the
+Program specifies that a certain numbered version of the GNU Affero General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU Affero General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU Affero General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source.  For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code.  There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for the
+specific requirements.
+
+  You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU AGPL, see
+<https://www.gnu.org/licenses/>.

+ 59 - 0
README-EN.md

@@ -0,0 +1,59 @@
+# Introduction
+
+**[中文文档](README.md)**
+
+- `wookteam` Is a lightweight online team collaboration tool, provides a variety of documentation tools, online mind mapping, online flow chart, project management, task distribution, knowledge base management tools.
+- `wookteam` Support team online chat communication, subscribe task dynamic real-time push.
+- **`wookteam` All open source.**
+
+## Installation
+- [Install(Docker)](install/en/DOCKER.md)
+- [Install(Server)](install/en/SERVER.md)
+- [Install(Bt Panel)](install/BT.md)
+
+## Website
+
+- [https://www.wookteam.com](https://www.wookteam.com)
+
+## Demo
+
+- [https://demo.wookteam.com](https://demo.wookteam.com) (admin/123456)
+
+## WeChat
+
+- ![WeChat](resources/assets/statics/other/wxqr.jpeg)
+
+## Technology
+
+- API Framework: [Laravel7](https://laravel.com/) + LaravelS
+- UI Framework: [Vue 2.0](https://cn.vuejs.org/) + Iview UI
+- Database: Mysql
+- Communication: Swoole
+- Theme style: Kooteam
+
+## Internationalization
+
+- `WookTeam`Support internationalization, support: simplified Chinese, English, English translation from Google translation. If you have a better grammar you are welcome to write it. [Translation data](https://docs.google.com/spreadsheets/d/1m0de8-5vCwjKRwW_lsgzsi8wmOmQRl_bIMGN988Keak/edit?usp=sharing)
+
+## Function
+
+**1. Four quadrants: Highlight priorities, help employees arrange their time properly, and improve work efficiency**
+![Four quadrants: Highlight priorities, help employees arrange their time properly, and improve work efficiency](resources/assets/statics/images/index/todo.jpg)
+
+**2. Online flow chart: online flow chart tool, easy to use**
+![Online flow chart: online flow chart tool, easy to use](resources/assets/statics/images/index/banner/1.jpg)
+
+**3. Online mind mapping: Organize your thoughts and optimize your workflow**
+![Online mind mapping: Organize your thoughts and optimize your workflow](resources/assets/statics/images/index/banner/2.jpg)
+
+**4. Project management: Custom project kanban, visual task scheduling**
+![Project management: Custom project kanban, visual task scheduling](resources/assets/statics/images/index/project.jpg)
+
+**5. Online knowledge base: online flowcharts, online documentation, and visual catalogs, document management without worry**
+![Online knowledge base: online flowcharts, online documentation, and visual catalogs, document management without worry](resources/assets/statics/images/index/wiki.jpg)
+
+**6. Task Gantt chart: Visual task time planning, intuitive and convenient**
+![Task Gantt chart: Visual task time planning, intuitive and convenient](resources/assets/statics/images/index/gantt.jpg)
+
+**7. Instant chat: team internal communication, real-time understanding of project dynamics**
+![Instant chat: team internal communication, real-time understanding of project dynamics](resources/assets/statics/images/index/chat.jpg)

+ 60 - 0
README.md

@@ -0,0 +1,60 @@
+# 产品介绍
+
+**[English Documentation](README-EN.md)**
+
+- `wookteam` 是一款轻量级的在线团队协作工具,提供各类文档工具、在线思维导图、在线流程图、项目管理、任务分发,知识库管理等工具。
+- `wookteam` 支持团队在线聊天沟通,订阅任务动态实时推送。
+- **`wookteam` 全部开源。**
+
+## 安装教程
+
+- [安装教程(Docker)](install/DOCKER.md)
+- [安装教程(服务器)](install/SERVER.md)
+- [安装教程(宝塔面板)](install/BT.md)
+
+## 官网地址
+
+- [https://www.wookteam.com](https://www.wookteam.com)
+
+## 演示地址
+
+- [https://demo.wookteam.com](https://demo.wookteam.com) (admin/123456)
+
+## 微信咨询
+
+- ![二维码](resources/assets/statics/other/wxqr.jpeg)
+
+## 技术选型
+
+- 后端框架:[Laravel7](https://laravel.com/) + LaravelS
+- 前端框架:[Vue 2.0](https://cn.vuejs.org/) + Iview UI
+- 数据库:Mysql
+- 通讯框架:Swoole
+- 主题样式:Kooteam
+
+## 国际化
+
+- `WookTeam`支持国际化,支持:简体中文、英文,英文译文来自谷歌翻译。如果你有更好的语法表达欢迎参与[译文编写](https://docs.google.com/spreadsheets/d/1m0de8-5vCwjKRwW_lsgzsi8wmOmQRl_bIMGN988Keak/edit?usp=sharing)
+
+## 功能简介
+
+**1. 待办四象限:突出事情优先级,帮助员工合理安排时间,提高工作效率**
+![待办四象限:突出事情优先级,帮助员工合理安排时间,提高工作效率](resources/assets/statics/images/index/todo.jpg)
+
+**2. 在线流程图:在线流程图工具,使用方便**
+![在线流程图:在线流程图工具,使用方便](resources/assets/statics/images/index/banner/1.jpg)
+
+**3. 在线思维导图:梳理思路,优化工作流程**
+![在线思维导图:梳理思路,优化工作流程](resources/assets/statics/images/index/banner/2.jpg)
+
+**4. 项目管理:自定义项目看板,可视化任务安排**
+![项目管理:自定义项目看板,可视化任务安排](resources/assets/statics/images/index/project.jpg)
+
+**5. 在线知识库:在线流程图,在线文档,以及可视化的目录编排,文档管理无忧**
+![在线知识库:在线流程图,在线文档,以及可视化的目录编排,文档管理无忧](resources/assets/statics/images/index/wiki.jpg)
+
+**6. 任务甘特图:可视化任务时间规划,直观方便**
+![即时聊天:团队内部沟通,项目动态实时了解](resources/assets/statics/images/index/gantt.jpg)
+
+**7. 即时聊天:团队内部沟通,项目动态实时了解**
+![即时聊天:团队内部沟通,项目动态实时了解](resources/assets/statics/images/index/chat.jpg)

File diff suppressed because it is too large
+ 18112 - 0
_ide_helper.php


+ 41 - 0
app/Console/Kernel.php

@@ -0,0 +1,41 @@
+<?php
+
+namespace App\Console;
+
+use Illuminate\Console\Scheduling\Schedule;
+use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
+
+class Kernel extends ConsoleKernel
+{
+    /**
+     * The Artisan commands provided by your application.
+     *
+     * @var array
+     */
+    protected $commands = [
+        //
+    ];
+
+    /**
+     * Define the application's command schedule.
+     *
+     * @param  \Illuminate\Console\Scheduling\Schedule  $schedule
+     * @return void
+     */
+    protected function schedule(Schedule $schedule)
+    {
+        // $schedule->command('inspire')->hourly();
+    }
+
+    /**
+     * Register the commands for the application.
+     *
+     * @return void
+     */
+    protected function commands()
+    {
+        $this->load(__DIR__.'/Commands');
+
+        require base_path('routes/console.php');
+    }
+}

+ 28 - 0
app/Events/ServerStartEvent.php

@@ -0,0 +1,28 @@
+<?php
+
+
+namespace App\Events;
+
+
+use Hhxsv5\LaravelS\Swoole\Events\ServerStartInterface;
+use Swoole\Http\Server;
+
+class ServerStartEvent implements ServerStartInterface
+{
+
+    public function __construct()
+    {
+    }
+
+    public function handle(Server $server)
+    {
+        $server->startMsecTime = $this->msecTime();
+    }
+
+    private function msecTime()
+    {
+        list($msec, $sec) = explode(' ', microtime());
+        $time = explode(".", $sec . ($msec * 1000));
+        return $time[0];
+    }
+}

+ 24 - 0
app/Events/WorkerStartEvent.php

@@ -0,0 +1,24 @@
+<?php
+
+namespace App\Events;
+
+use Cache;
+use DB;
+use Hhxsv5\LaravelS\Swoole\Events\WorkerStartInterface;
+use Swoole\Http\Server;
+
+class WorkerStartEvent implements WorkerStartInterface
+{
+
+    public function __construct()
+    {
+    }
+
+    public function handle(Server $server, $workerId)
+    {
+        if (isset($server->startMsecTime) && Cache::get("swooleServerStartMsecTime") != $server->startMsecTime) {
+            Cache::forever("swooleServerStartMsecTime", $server->startMsecTime);
+            DB::table('ws')->delete();
+        }
+    }
+}

+ 62 - 0
app/Exceptions/Handler.php

@@ -0,0 +1,62 @@
+<?php
+
+namespace App\Exceptions;
+
+use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
+use Throwable;
+
+class Handler extends ExceptionHandler
+{
+    /**
+     * A list of the exception types that are not reported.
+     *
+     * @var array
+     */
+    protected $dontReport = [
+        //
+    ];
+
+    /**
+     * A list of the inputs that are never flashed for validation exceptions.
+     *
+     * @var array
+     */
+    protected $dontFlash = [
+        'password',
+        'password_confirmation',
+    ];
+
+    /**
+     * Report or log an exception.
+     *
+     * @param  \Throwable  $exception
+     * @return void
+     *
+     * @throws \Exception
+     */
+    public function report(Throwable $exception)
+    {
+        parent::report($exception);
+    }
+
+    /**
+     * Render an exception into an HTTP response.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @param  \Throwable  $exception
+     * @return \Symfony\Component\HttpFoundation\Response
+     *
+     * @throws \Throwable
+     */
+    public function render($request, Throwable $exception)
+    {
+        if ($exception instanceof \Symfony\Component\HttpKernel\Exception\NotFoundHttpException) {
+            if ($exception->getStatusCode() == 404) {
+                if (!\App\Module\Base::leftExists($request->getRequestUri(), '/api')) {
+                    return response()->view('main', ['version' => \App\Module\Base::getVersion()]);
+                }
+            }
+        }
+        return parent::render($request, $exception);
+    }
+}

+ 260 - 0
app/Http/Controllers/Api/ChatController.php

@@ -0,0 +1,260 @@
+<?php
+
+namespace App\Http\Controllers\Api;
+
+use App\Http\Controllers\Controller;
+use App\Module\Base;
+use App\Module\Chat;
+use App\Module\Users;
+use DB;
+use Request;
+
+/**
+ * @apiDefine chat
+ *
+ * 聊天
+ */
+class ChatController extends Controller
+{
+    public function __invoke($method, $action = '')
+    {
+        $app = $method ? $method : 'main';
+        if ($action) {
+            $app .= "__" . $action;
+        }
+        return (method_exists($this, $app)) ? $this->$app() : Base::ajaxError("404 not found (" . str_replace("__", "/", $app) . ").");
+    }
+
+    /**
+     * 对话列表
+     */
+    public function dialog__lists()
+    {
+        $user = Users::authE();
+        if (Base::isError($user)) {
+            return $user;
+        } else {
+            $user = $user['data'];
+        }
+        //
+        $lists = Base::DBC2A(DB::table('chat_dialog')
+            ->where(function ($query) use ($user) {
+                return $query->where('user1', $user['username'])->where('del1', 0);
+            })
+            ->orWhere(function ($query) use ($user) {
+                return $query->where('user2', $user['username'])->where('del2', 0);
+            })
+            ->orderByDesc('lastdate')
+            ->take(200)
+            ->get());
+        if (count($lists) <= 0) {
+            return Base::retError('暂无对话记录');
+        }
+        $hasUnset = false;
+        foreach ($lists AS $key => $item) {
+            $tmpUserInfo = Users::username2basic($item['user1'] == $user['username'] ? $item['user2'] : $item['user1']);
+            if (empty($tmpUserInfo)) {
+                DB::table('chat_dialog')->where('id', $item['id'])->update(['del1' => 1, 'del2' => 1]);
+                $hasUnset = true;
+                unset($lists[$key]);
+                continue;
+            }
+            $lists[$key] = array_merge($item, $tmpUserInfo);
+            $lists[$key]['lastdate'] = $item['lastdate'] ?: $item['indate'];
+            $unread = 0;
+            if ($item['user1'] == $user['username']) $unread+= $item['unread1'];
+            if ($item['user2'] == $user['username']) $unread+= $item['unread2'];
+            $lists[$key]['unread'] = $unread;
+        }
+        if ($hasUnset) {
+            $lists = array_values($lists);
+        }
+        return Base::retSuccess('success', $lists);
+    }
+
+    /**
+     * 清空聊天记录(删除对话)
+     *
+     * @apiParam {String} username             用户名
+     * @apiParam {Number} [delete]             是否删除对话,1:是
+     */
+    public function dialog__clear()
+    {
+        $user = Users::authE();
+        if (Base::isError($user)) {
+            return $user;
+        } else {
+            $user = $user['data'];
+        }
+        //
+        $delete = intval(Request::input('delete'));
+        $res = Chat::openDialog($user['username'], trim(Request::input('username')));
+        if (Base::isError($res)) {
+            return $res;
+        }
+        $dialog = $res['data'];
+        $lastMsg = Base::DBC2A(DB::table('chat_msg')
+            ->select(['id'])
+            ->where('did', $dialog['id'])
+            ->orderByDesc('indate')
+            ->orderByDesc('id')
+            ->first());
+        $upArray = [
+            ($dialog['recField'] == 1 ? 'lastid2' : 'lastid1') => $lastMsg ? $lastMsg['id'] : 0
+        ];
+        if ($delete === 1) {
+            $upArray[($dialog['recField'] == 1 ? 'del2' : 'del1')] = 1;
+        }
+        DB::table('chat_dialog')->where('id', $dialog['id'])->update($upArray);
+        Chat::forgetDialog($dialog['user1'], $dialog['user2']);
+        return Base::retSuccess($delete ? '删除成功!' : '清除成功!');
+    }
+
+    /**
+     * 消息列表
+     *
+     * @apiParam {String} username             用户名
+     * @apiParam {Number} [page]               当前页,默认:1
+     * @apiParam {Number} [pagesize]           每页显示数量,默认:20,最大:100
+     */
+    public function message__lists()
+    {
+        $user = Users::authE();
+        if (Base::isError($user)) {
+            return $user;
+        } else {
+            $user = $user['data'];
+        }
+        //
+        $res = Chat::openDialog($user['username'], trim(Request::input('username')));
+        if (Base::isError($res)) {
+            return $res;
+        }
+        $dialog = $res['data'];
+        $lastid = $dialog[($dialog['recField'] == 1 ? 'lastid2' : 'lastid1')];
+        $whereArray = [];
+        $whereArray[] = ['did', '=', $dialog['id']];
+        if ($lastid > 0) {
+            $whereArray[] = ['id', '>', $lastid];
+        }
+        $lists = DB::table('chat_msg')
+            ->where($whereArray)
+            ->orderByDesc('indate')
+            ->orderByDesc('id')
+            ->paginate(Base::getPaginate(100, 20));
+        $lists = Base::getPageList($lists, false);
+        //
+        foreach ($lists['lists'] AS $key => $item) {
+            $basic = Users::username2basic($item['username']);
+            $item['nickname'] = $basic ? $basic['nickname'] : ($item['nickname'] || '');
+            $item['userimg'] = $basic ? $basic['userimg'] : ($item['userimg'] || '');
+            $item['message'] = Base::string2array($item['message']);
+            $lists['lists'][$key] = $item;
+        }
+        //
+        return Base::retSuccess('success', $lists);
+    }
+
+    /**
+     * 文件 - 上传
+     *
+     * @apiParam {String} username            get-发给用户名(群组名)
+     * @apiParam {String} [filename]          post-文件名称
+     * @apiParam {String} [image64]           post-base64图片(二选一)
+     * @apiParam {File} [files]               post-文件对象(二选一)
+     */
+    public function files__upload()
+    {
+        $user = Users::authE();
+        if (Base::isError($user)) {
+            return $user;
+        } else {
+            $user = $user['data'];
+        }
+        //
+        $username = trim(Request::input('username'));
+        if (Base::leftExists($username, "group::")) {
+            //$res = Chat::groupOpen($username);
+            return Base::retError('参数错误');
+        } else {
+            $res = Chat::openDialog($user['username'], $username);
+        }
+        if (Base::isError($res)) {
+            return $res;
+        }
+        $did = $res['data']['id'];
+        //
+        $path = "uploads/chat/" . Users::token2userid() . "/";
+        $image64 = trim(Base::getPostValue('image64'));
+        $fileName = trim(Base::getPostValue('filename'));
+        if ($image64) {
+            $data = Base::image64save([
+                "image64" => $image64,
+                "path" => $path,
+                "fileName" => $fileName,
+            ]);
+        } else {
+            $data = Base::upload([
+                "file" => Request::file('files'),
+                "type" => 'file',
+                "path" => $path,
+                "fileName" => $fileName,
+            ]);
+        }
+        //
+        if (Base::isError($data)) {
+            return Base::retError($data['msg']);
+        } else {
+            $fileData = $data['data'];
+            $fileData['thumb'] = $fileData['thumb'] ?: 'images/files/file.png';
+            switch ($fileData['ext']) {
+                case "docx":
+                    $fileData['thumb'] = 'images/files/doc.png';
+                    break;
+                case "xlsx":
+                    $fileData['thumb'] = 'images/files/xls.png';
+                    break;
+                case "pptx":
+                    $fileData['thumb'] = 'images/files/ppt.png';
+                    break;
+                case "ai":
+                case "avi":
+                case "bmp":
+                case "cdr":
+                case "doc":
+                case "eps":
+                case "gif":
+                case "mov":
+                case "mp3":
+                case "mp4":
+                case "pdf":
+                case "ppt":
+                case "pr":
+                case "psd":
+                case "rar":
+                case "svg":
+                case "tif":
+                case "txt":
+                case "xls":
+                case "zip":
+                    $fileData['thumb'] = 'images/files/' . $fileData['ext'] . '.png';
+                    break;
+            }
+            $array = [
+                'did' => $did,
+                'group' => Base::leftExists($username, 'group::') ? $username : '',
+                'name' => $fileData['name'],
+                'size' => $fileData['size'] * 1024,
+                'ext' => $fileData['ext'],
+                'path' => $fileData['path'],
+                'thumb' => $fileData['thumb'],
+                'username' => $user['username'],
+                'indate' => Base::time(),
+            ];
+            DB::table('chat_files')->insertGetId($array);
+            //
+            $fileData['thumb'] = Base::fillUrl($fileData['thumb']);
+            return Base::retSuccess('success', $fileData);
+        }
+    }
+}

+ 680 - 0
app/Http/Controllers/Api/DocsController.php

@@ -0,0 +1,680 @@
+<?php
+
+namespace App\Http\Controllers\Api;
+
+use App\Http\Controllers\Controller;
+use App\Module\Base;
+use App\Module\Docs;
+use App\Module\Users;
+use App\Tasks\PushTask;
+use Cache;
+use DB;
+use Hhxsv5\LaravelS\Swoole\Task\Task;
+use Request;
+
+/**
+ * @apiDefine docs
+ *
+ * 知识库
+ */
+class DocsController extends Controller
+{
+    public function __invoke($method, $action = '')
+    {
+        $app = $method ? $method : 'main';
+        if ($action) {
+            $app .= "__" . $action;
+        }
+        return (method_exists($this, $app)) ? $this->$app() : Base::ajaxError("404 not found (" . str_replace("__", "/", $app) . ").");
+    }
+
+    /**
+     * 知识库列表
+     *
+     * @apiParam {Number} [page]                当前页,默认:1
+     * @apiParam {Number} [pagesize]            每页显示数量,默认:20,最大:100
+     */
+    public function book__lists()
+    {
+        $user = Users::authE();
+        if (Base::isError($user)) {
+            return $user;
+        } else {
+            $user = $user['data'];
+        }
+        //
+        $lists = DB::table('docs_book')
+            ->where('username', $user['username'])
+            ->orWhere('role_edit', 'reg')
+            ->orWhere('role_look', 'reg')
+            ->orWhere(function ($query) use ($user) {
+                $query->where('role_edit', 'private')->where('username', $user['username']);
+            })
+            ->orWhere(function ($query) use ($user) {
+                $query->where('role_edit', 'member')->whereIn('id', function ($query2) use ($user) {
+                    $query2->select('bookid')
+                        ->from('docs_users')
+                        ->where('username', $user['username'])
+                        ->whereRaw(env('DB_PREFIX') . 'docs_book.id = bookid');
+                });
+            })
+            ->orderByDesc('id')
+            ->paginate(Base::getPaginate(100, 20));
+        $lists = Base::getPageList($lists);
+        if ($lists['total'] == 0) {
+            return Base::retError('暂无知识库', $lists);
+        }
+        return Base::retSuccess('success', $lists);
+    }
+
+    /**
+     * 添加/修改知识库
+     *
+     * @apiParam {Number} id                知识库数据ID
+     * @apiParam {String} title             知识库名称
+     */
+    public function book__add()
+    {
+        $user = Users::authE();
+        if (Base::isError($user)) {
+            return $user;
+        } else {
+            $user = $user['data'];
+        }
+        //
+        $id = intval(Request::input('id'));
+        $title = trim(Request::input('title'));
+        if ($id > 0) {
+            $role = Docs::checkRole($id, 'edit');
+            if (Base::isError($role)) {
+                return $role;
+            }
+        }
+        if (mb_strlen($title) < 2 || mb_strlen($title) > 100) {
+            return Base::retError('标题限制2-100个字!');
+        }
+        if ($id > 0) {
+            // 修改
+            $row = Base::DBC2A(DB::table('docs_book')->where('id', $id)->first());
+            if (empty($row)) {
+                return Base::retError('知识库不存在或已被删除!');
+            }
+            $data = [
+                'title' => $title,
+            ];
+            DB::table('docs_book')->where('id', $id)->update($data);
+            return Base::retSuccess('修改成功!', $data);
+        } else {
+            // 添加
+            $data = [
+                'username' => $user['username'],
+                'title' => $title,
+                'indate' => Base::time(),
+            ];
+            $id = DB::table('docs_book')->insertGetId($data);
+            if (empty($id)) {
+                return Base::retError('系统繁忙,请稍后再试!');
+            }
+            $data['id'] = $id;
+            return Base::retSuccess('添加成功!', $data);
+        }
+    }
+
+    /**
+     * 设置知识库
+     *
+     * @apiParam {Number} id                知识库数据ID
+     * @apiParam {String} role_edit
+     * @apiParam {String} role_view
+     */
+    public function book__setting()
+    {
+        $user = Users::authE();
+        if (Base::isError($user)) {
+            return $user;
+        } else {
+            $user = $user['data'];
+        }
+        //
+        $id = intval(Request::input('id'));
+        $type = trim(Request::input('type'));
+        $role = Docs::checkRole($id, 'edit');
+        if (Base::isError($role) && $role['ret'] < 0) {
+            return $role;
+        }
+        $row = Base::DBC2A(DB::table('docs_book')->where('id', $id)->first());
+        if (empty($row)) {
+            return Base::retError('知识库不存在或已被删除!');
+        }
+        $setting = Base::string2array($row['setting']);
+        if ($type == 'save') {
+            if (Base::isError($role)) {
+                return $role;
+            }
+            foreach (Request::input() AS $key => $value) {
+                if (in_array($key, ['role_edit', 'role_look', 'role_view'])) {
+                    $setting[$key] = $value;
+                }
+            }
+            DB::table('docs_book')->where('id', $id)->update([
+                'role_edit' => $setting['role_edit'],
+                'role_look' => $setting['role_look'],
+                'role_view' => $setting['role_view'],
+                'setting' => Base::array2string($setting),
+            ]);
+        }
+        return Base::retSuccess($type == 'save' ? '修改成功!' : 'success', $setting ?: json_decode('{}'));
+    }
+
+    /**
+     * 删除知识库
+     *
+     * @apiParam {Number} id                知识库数据ID
+     */
+    public function book__delete()
+    {
+        $user = Users::authE();
+        if (Base::isError($user)) {
+            return $user;
+        } else {
+            $user = $user['data'];
+        }
+        //
+        $id = intval(Request::input('id'));
+        $row = Base::DBC2A(DB::table('docs_book')->where('id', $id)->first());
+        if (empty($row)) {
+            return Base::retError('知识库不存在或已被删除!');
+        }
+        if ($row['username'] != $user['username']) {
+            return Base::retError('此操作仅限知识库负责人!');
+        }
+        DB::table('docs_book')->where('id', $id)->delete();
+        DB::table('docs_section')->where('bookid', $id)->delete();
+        DB::table('docs_content')->where('bookid', $id)->delete();
+        return Base::retSuccess('删除成功!');
+    }
+
+    /**
+     * 成员-列表
+     *
+     * @apiParam {Number} id            知识库数据ID
+     * @apiParam {Number} [page]        当前页,默认:1
+     * @apiParam {Number} [pagesize]    每页显示数量,默认:20,最大:100
+     */
+    public function users__lists()
+    {
+        $user = Users::authE();
+        if (Base::isError($user)) {
+            return $user;
+        } else {
+            $user = $user['data'];
+        }
+        //
+        $id = intval(Request::input('id'));
+        $role = Docs::checkRole($id, 'edit');
+        if (Base::isError($role)) {
+            return $role;
+        }
+        $row = Base::DBC2A(DB::table('docs_book')->where('id', $id)->first());
+        if (empty($row)) {
+            return Base::retError('知识库不存在或已被删除!');
+        }
+        //
+        $lists = DB::table('docs_book')
+            ->join('docs_users', 'docs_book.id', '=', 'docs_users.bookid')
+            ->select(['docs_book.title', 'docs_users.*'])
+            ->where([
+                ['docs_book.id', $id],
+            ])
+            ->orderByDesc('docs_users.id')->paginate(Base::getPaginate(100, 20));
+        $lists = Base::getPageList($lists);
+        if ($lists['total'] == 0) {
+            return Base::retError('未找到任何相关的成员');
+        }
+        foreach ($lists['lists'] AS $key => $item) {
+            $userInfo = Users::username2basic($item['username']);
+            $lists['lists'][$key]['userimg'] = $userInfo['userimg'];
+            $lists['lists'][$key]['nickname'] = $userInfo['nickname'];
+            $lists['lists'][$key]['profession'] = $userInfo['profession'];
+        }
+        return Base::retSuccess('success', $lists);
+    }
+
+    /**
+     * 成员-添加、删除
+     *
+     * @apiParam {String} act
+     * - delete: 删除成员
+     * - else: 添加成员
+     * @apiParam {Number} id                    知识库数据ID
+     * @apiParam {Array|String} username        用户名(或用户名组)
+     */
+    public function users__join()
+    {
+        $user = Users::authE();
+        if (Base::isError($user)) {
+            return $user;
+        } else {
+            $user = $user['data'];
+        }
+        //
+        $id = intval(Request::input('id'));
+        $role = Docs::checkRole($id, 'edit');
+        if (Base::isError($role)) {
+            return $role;
+        }
+        $row = Base::DBC2A(DB::table('docs_book')->where('id', $id)->first());
+        if (empty($row)) {
+            return Base::retError('知识库不存在或已被删除!');
+        }
+        //
+        $usernames = Request::input('username');
+        if (empty($usernames)) {
+            return Base::retError('参数错误!');
+        }
+        if (!is_array($usernames)) {
+            if (Base::strExists($usernames, ',')) {
+                $usernames = explode(',', $usernames);
+            } else {
+                $usernames = [$usernames];
+            }
+        }
+        //
+        foreach ($usernames AS $username) {
+            $inRow = Base::DBC2A(DB::table('docs_users')->where(['bookid' => $id, 'username' => $username])->first());
+            switch (Request::input('act')) {
+                case 'delete': {
+                    if ($inRow) {
+                        DB::table('docs_users')->where([
+                            'bookid' => $id,
+                            'username' => $username
+                        ])->delete();
+                    }
+                    break;
+                }
+                default: {
+                    if (!$inRow && $username != $user['username']) {
+                        DB::table('docs_users')->insert([
+                            'bookid' => $id,
+                            'username' => $username,
+                            'indate' => Base::time()
+                        ]);
+                    }
+                    break;
+                }
+            }
+        }
+        return Base::retSuccess('操作完成!');
+    }
+
+    /**
+     * 章节列表
+     *
+     * @apiParam {String} act                   请求方式,用于判断权限
+     * - edit: 管理页请求
+     * - view: 阅读页请求
+     * @apiParam {Number} bookid                知识库数据ID
+     */
+    public function section__lists()
+    {
+        $bookid = intval(Request::input('bookid'));
+        $role = Docs::checkRole($bookid, Request::input('act'));
+        if (Base::isError($role) && $role['ret'] < 0) {
+            return $role;
+        }
+        $lists = Base::DBC2A(DB::table('docs_section')
+            ->where('bookid', $bookid)
+            ->orderByDesc('inorder')
+            ->orderByDesc('id')
+            ->take(500)
+            ->get());
+        if (empty($lists)) {
+            return Base::retError('暂无章节');
+        }
+        foreach ($lists AS $key => $item) {
+            $lists[$key]['icon'] = Base::fillUrl('images/files/' . $item['type'] . '.png');
+        }
+        $bookDetail = Base::DBC2A(DB::table('docs_book')->select(['title'])->where('id', $bookid)->first());
+        return Base::retSuccess('success', [
+            'book' => $bookDetail ?: json_decode('{}'),
+            'tree' => Base::list2Tree($lists, 'id', 'parentid')
+        ]);
+    }
+
+    /**
+     * 添加/修改章节
+     *
+     * @apiParam {Number} bookid                知识库数据ID
+     * @apiParam {String} title                 章节名称
+     * @apiParam {String} type                  章节类型
+     */
+    public function section__add()
+    {
+        $user = Users::authE();
+        if (Base::isError($user)) {
+            return $user;
+        } else {
+            $user = $user['data'];
+        }
+        //
+        $bookid = intval(Request::input('bookid'));
+        $role = Docs::checkRole($bookid, 'edit');
+        if (Base::isError($role)) {
+            return $role;
+        }
+        $bookRow = Base::DBC2A(DB::table('docs_book')->where('id', $bookid)->first());
+        if (empty($bookRow)) {
+            return Base::retError('知识库不存在或已被删除!');
+        }
+        $count = DB::table('docs_section')->where('bookid', $bookid)->count();
+        if ($count >= 500) {
+            return Base::retError(['知识库章节已经超过最大限制(%)!', 500]);
+        }
+        //
+        $id = intval(Request::input('id'));
+        $title = trim(Request::input('title'));
+        $type = trim(Request::input('type'));
+        if (mb_strlen($title) < 2 || mb_strlen($title) > 100) {
+            return Base::retError('标题限制2-100个字!');
+        }
+        if ($id > 0) {
+            // 修改
+            $row = Base::DBC2A(DB::table('docs_section')->where('id', $id)->first());
+            if (empty($row)) {
+                return Base::retError('知识库不存在或已被删除!');
+            }
+            $data = [
+                'title' => $title,
+            ];
+            DB::table('docs_section')->where('id', $id)->update($data);
+            return Base::retSuccess('修改成功!', $data);
+        } else {
+            // 添加
+            if (!in_array($type, ['document', 'mind', 'sheet', 'flow', 'folder'])) {
+                return Base::retError('参数错误!');
+            }
+            $parentid = 0;
+            if ($id < 0) {
+                $count = Base::DBC2A(DB::table('docs_section')->where('id', abs($id))->where('bookid', $bookid)->count());
+                if ($count > 0) {
+                    $parentid = abs($id);
+                }
+            }
+            $data = [
+                'bookid' => $bookid,
+                'parentid' => $parentid,
+                'username' => $user['username'],
+                'title' => $title,
+                'type' => $type,
+                'inorder' => intval(DB::table('docs_section')->select(['inorder'])->where('bookid', $bookid)->orderByDesc('inorder')->value('inorder')) + 1,
+                'indate' => Base::time(),
+            ];
+            $id = DB::table('docs_section')->insertGetId($data);
+            if (empty($id)) {
+                return Base::retError('系统繁忙,请稍后再试!');
+            }
+            $data['id'] = $id;
+            return Base::retSuccess('添加成功!', $data);
+        }
+    }
+
+    /**
+     * 排序章节
+     *
+     * @apiParam {Number} bookid                知识库数据ID
+     * @apiParam {String} oldsort               旧排序数据
+     * @apiParam {String} newsort               新排序数据
+     */
+    public function section__sort()
+    {
+        $user = Users::authE();
+        if (Base::isError($user)) {
+            return $user;
+        } else {
+            $user = $user['data'];
+        }
+        //
+        $bookid = intval(Request::input('bookid'));
+        $role = Docs::checkRole($bookid, 'edit');
+        if (Base::isError($role)) {
+            return $role;
+        }
+        $bookRow = Base::DBC2A(DB::table('docs_book')->where('id', $bookid)->first());
+        if (empty($bookRow)) {
+            return Base::retError('知识库不存在或已被删除!');
+        }
+        //
+        $newSort = explode(";", Request::input('newsort'));
+        if (count($newSort) == 0) {
+            return Base::retError('参数错误!');
+        }
+        //
+        $count = count($newSort);
+        foreach ($newSort AS $sort => $item) {
+            list($newId, $newParentid) = explode(':', $item);
+            DB::table('docs_section')->where([
+                'id' => $newId,
+                'bookid' => $bookid
+            ])->update([
+                'inorder' => $count - intval($sort),
+                'parentid' => $newParentid
+            ]);
+        }
+        return Base::retSuccess('保存成功!');
+    }
+
+    /**
+     * 删除章节
+     *
+     * @apiParam {Number} id                章节数据ID
+     */
+    public function section__delete()
+    {
+        $user = Users::authE();
+        if (Base::isError($user)) {
+            return $user;
+        } else {
+            $user = $user['data'];
+        }
+        //
+        $id = intval(Request::input('id'));
+        $row = Base::DBC2A(DB::table('docs_section')->where('id', $id)->first());
+        if (empty($row)) {
+            return Base::retError('文档不存在或已被删除!');
+        }
+        $role = Docs::checkRole($row['bookid'], 'edit');
+        if (Base::isError($role)) {
+            return $role;
+        }
+        DB::table('docs_section')->where('parentid', $id)->update([ 'parentid' => $row['parentid'] ]);
+        DB::table('docs_section')->where('id', $id)->delete();
+        DB::table('docs_content')->where('sid', $id)->delete();
+        return Base::retSuccess('删除成功!');
+    }
+
+    /**
+     * 获取章节内容
+     *
+     * @apiParam {String} act                   请求方式,用于判断权限
+     * - edit: 管理页请求
+     * - view: 阅读页请求
+     * @apiParam {Number|String} id             章节数据ID(或:章节数据ID-历史数据ID)
+     */
+    public function section__content()
+    {
+        $id = Request::input('id');
+        $hid = 0;
+        if (Base::strExists($id, '-')) {
+            list($id, $hid) = explode("-", $id);
+        }
+        $id = intval($id);
+        $hid = intval($hid);
+        $row = Base::DBC2A(DB::table('docs_section')->where('id', $id)->first());
+        if (empty($row)) {
+            return Base::retError('文档不存在或已被删除!');
+        }
+        $role = Docs::checkRole($row['bookid'], Request::input('act'));
+        if (Base::isError($role) && $role['ret'] < 0) {
+            return $role;
+        }
+        $whereArray = [];
+        if ($hid > 0) {
+            $whereArray[] = ['id', '=', $hid];
+        }
+        $whereArray[] = ['sid', '=', $id];
+        $cRow = Base::DBC2A(DB::table('docs_content')->select(['id AS hid', 'content'])->where($whereArray)->orderByDesc('id')->first());
+        if (empty($cRow)) {
+            $cRow = [ 'hid' => 0, 'content' => '' ];
+        }
+        return Base::retSuccess('success', array_merge($row, $cRow));
+    }
+
+    /**
+     * 获取章节历史内容
+     *
+     * @apiParam {Number} id                章节数据ID
+     */
+    public function section__history()
+    {
+        $user = Users::authE();
+        if (Base::isError($user)) {
+            return $user;
+        } else {
+            $user = $user['data'];
+        }
+        //
+        $id = intval(Request::input('id'));
+        $row = Base::DBC2A(DB::table('docs_section')->where('id', $id)->first());
+        if (empty($row)) {
+            return Base::retError('文档不存在或已被删除!');
+        }
+        $role = Docs::checkRole($row['bookid'], 'edit');
+        if (Base::isError($role) && $role['ret'] < 0) {
+            return $role;
+        }
+        //
+        $lists = Base::DBC2A(DB::table('docs_content')
+            ->where('sid', $id)
+            ->orderByDesc('id')
+            ->take(50)
+            ->get());
+        if (count($lists) <= 1) {
+            return Base::retError('暂无历史数据');
+        }
+        return Base::retSuccess('success', $lists);
+    }
+
+    /**
+     * {post} 保存章节内容
+     *
+     * @apiParam {Number} id                章节数据ID
+     * @apiParam {Object} [D]               Request Payload 提交
+     * - content: 内容
+     */
+    public function section__save()
+    {
+        $user = Users::authE();
+        if (Base::isError($user)) {
+            return $user;
+        } else {
+            $user = $user['data'];
+        }
+        //
+        $id = intval(Base::getPostValue('id'));
+        $row = Base::DBC2A(DB::table('docs_section')->where('id', $id)->first());
+        if (empty($row)) {
+            return Base::retError('文档不存在或已被删除!');
+        }
+        $role = Docs::checkRole($row['bookid'], 'edit');
+        if (Base::isError($role)) {
+            return $role;
+        }
+        if ($row['lockdate'] + 60 > Base::time() && $row['lockname'] != $user['username']) {
+            return Base::retError(['已被会员【%】锁定!', Users::nickname($row['lockname'])]);
+        }
+        $content = Base::getPostValue('content');
+        $text = '';
+        if ($row['type'] == 'document') {
+            $data = Base::json2array($content);
+            $isRep = false;
+            preg_match_all("/<img\s*src=\"data:image\/(png|jpg|jpeg);base64,(.*?)\"/s", $data['content'], $matchs);
+            foreach ($matchs[2] as $key => $text) {
+                $p = "uploads/docs/document/" . $id . "/";
+                Base::makeDir(public_path($p));
+                $p.= md5($text) . "." . $matchs[1][$key];
+                $r = file_put_contents(public_path($p), base64_decode($text));
+                if ($r) {
+                    $data['content'] = str_replace($matchs[0][$key], '<img src="' . Base::fillUrl($p) . '"', $data['content']);
+                    $isRep = true;
+                }
+            }
+            $text = strip_tags($data['content']);
+            if ($isRep == true) {
+                $content = Base::array2json($data);
+            }
+        }
+        DB::table('docs_content')->where('sid', $id)->update(['text' => '']);
+        DB::table('docs_content')->insert([
+            'bookid' => $row['bookid'],
+            'sid' => $id,
+            'content' => $content,
+            'text' => $text,
+            'username' => $user['username'],
+            'indate' => Base::time()
+        ]);
+        Docs::notice($id, [ 'type' => 'update' ]);
+        //
+        return Base::retSuccess('保存成功!', [
+            'sid' => $id,
+            'content' => Base::json2array($content),
+        ]);
+    }
+
+    /**
+     * 锁定章节内容
+     *
+     * @apiParam {String} act
+     * - lock: 锁定
+     * - unlock: 解锁
+     * @apiParam {Number} id                章节数据ID
+     */
+    public function section__lock()
+    {
+        $user = Users::authE();
+        if (Base::isError($user)) {
+            return $user;
+        } else {
+            $user = $user['data'];
+        }
+        //
+        $id = intval(Request::input('id'));
+        $act = trim(Request::input('act'));
+        $row = Base::DBC2A(DB::table('docs_section')->where('id', $id)->first());
+        if (empty($row)) {
+            return Base::retError('文档不存在或已被删除!');
+        }
+        $role = Docs::checkRole($row['bookid'], 'edit');
+        if (Base::isError($role)) {
+            return $role;
+        }
+        if ($row['lockdate'] + 60 > Base::time() && $row['lockname'] != $user['username']) {
+            return Base::retError(['已被会员【%】锁定!', Users::nickname($row['lockname'])]);
+        }
+        if ($act == 'lock') {
+            $upArray = [
+                'lockname' => $user['username'],
+                'lockdate' => Base::time(),
+            ];
+        } else {
+            $upArray = [
+                'lockname' => '',
+                'lockdate' => 0,
+            ];
+        }
+        DB::table('docs_section')->where('id', $id)->update($upArray);
+        $upArray['type'] = $act;
+        Docs::notice($id, $upArray);
+        //
+        return Base::retSuccess($act == 'lock' ? '锁定成功' : '已解除锁定', $upArray);
+    }
+}

File diff suppressed because it is too large
+ 2793 - 0
app/Http/Controllers/Api/ProjectController.php


+ 400 - 0
app/Http/Controllers/Api/ReportController.php

@@ -0,0 +1,400 @@
+<?php
+
+namespace App\Http\Controllers\Api;
+
+use App\Http\Controllers\Controller;
+use App\Module\Base;
+use App\Module\Users;
+use DB;
+use Request;
+
+/**
+ * @apiDefine report
+ *
+ * 汇报
+ */
+class ReportController extends Controller
+{
+    public function __invoke($method, $action = '')
+    {
+        $app = $method ? $method : 'main';
+        if ($action) {
+            $app .= "__" . $action;
+        }
+        return (method_exists($this, $app)) ? $this->$app() : Base::ajaxError("404 not found (" . str_replace("__", "/", $app) . ").");
+    }
+
+    /**
+     * 获取内容
+     *
+     * @apiParam {Number} id           数据ID
+     */
+    public function content()
+    {
+        $row = Base::DBC2A(DB::table('report_content')->select(['rid', 'content'])->where('rid', intval(Request::input('id')))->first());
+        if (empty($row)) {
+            return Base::retError('内容不存在或已被删除!');
+        }
+        return Base::retSuccess('success', $row);
+    }
+
+    /**
+     * {post} 获取模板、保存、发送、删除
+     *
+     * @apiParam {String} type           类型
+     * - 日报
+     * - 周报
+     * @apiParam {Number} [id]          数据ID
+     * @apiParam {String} [act]         请求方式
+     * - submit: 保存
+     * - send: 仅发送
+     * - delete: 删除汇报
+     * - else: 获取
+     * @apiParam {String} [send]        是否发送(1:是),仅act=submit且未发送过的有效
+     * @apiParam {Object} [D]           Request Payload 提交
+     * - title: 标题
+     * - ccuser: 抄送人
+     * - content: 内容
+     */
+    public function template()
+    {
+        $user = Users::authE();
+        if (Base::isError($user)) {
+            return $user;
+        } else {
+            $user = $user['data'];
+        }
+        //
+        $id = intval(Base::getPostValue('id'));
+        $act = trim(Base::getPostValue('act'));
+        $type = trim(Base::getPostValue('type'));
+        if (!in_array($type, ['日报', '周报'])) {
+            return Base::retError('参数错误!');
+        }
+        $dateTitle = "";
+        //
+        $whereArray = [];
+        $whereArray[] = ['username', '=', $user['username']];
+        if ($id > 0) {
+            $whereArray[] = ['id', '=', $id];
+        } else {
+            switch ($type) {
+                case "日报":
+                    $whereArray[] = ['type', '=', '日报'];
+                    $whereArray[] = ['date', '=', date("Ymd")];
+                    $dateTitle = date("Y-m-d");
+                    break;
+                case "周报":
+                    $whereArray[] = ['type', '=', '周报'];
+                    $whereArray[] = ['date', '=', date("W")];
+                    $dateTitle = date("Y年m月") . "第" . Base::getMonthWeek() . "周";
+                    break;
+            }
+        }
+        //
+        $reportDetail = Base::DBC2A(DB::table('report_lists')->where($whereArray)->first());
+        if ($id > 0 && empty($reportDetail)) {
+            return Base::retError('没有相关的数据!');
+        }
+        if ($act == 'send') {
+            if (empty($reportDetail)) {
+                return Base::retError('没有相关的数据或已被删除!');
+            }
+            $ccuser = Base::string2array($reportDetail['ccuser']);
+            DB::table('report_ccuser')->where(['rid' => $reportDetail['id']])->update(['cc' => 0]);
+            foreach ($ccuser AS $ck => $cuser) {
+                if (!$cuser) {
+                    unset($ccuser[$ck]);
+                    continue;
+                }
+                DB::table('report_ccuser')->updateOrInsert([
+                    'rid' => $reportDetail['id'],
+                    'username' => $cuser,
+                    'indate' => Base::time()
+                ], [
+                    'cc' => 1
+                ]);
+            }
+            DB::table('report_lists')->where('id', $reportDetail['id'])->update([
+                'status' => '已发送',
+                'ccuser' => Base::array2string($ccuser)
+            ]);
+            $reportDetail['ccuser'] = implode(',', $ccuser);
+            $reportDetail['ccuserArray'] = explode(',', $reportDetail['ccuser']);
+            return Base::retSuccess('发送成功!', array_merge($reportDetail, [
+                'ccuserAgain' => $reportDetail['status'] == '已发送'
+            ]));
+        } elseif ($act == 'delete') {
+            if (empty($reportDetail)) {
+                return Base::retError('没有相关的数据或已被删除!');
+            }
+            if ($reportDetail['status'] == '已发送') {
+                return Base::retError('汇报已发送,无法删除!');
+            }
+            DB::table('report_lists')->where('id', $reportDetail['id'])->delete();
+            DB::table('report_ccuser')->where('rid', $reportDetail['id'])->delete();
+            DB::table('report_content')->where('rid', $reportDetail['id'])->delete();
+            return Base::retSuccess('删除成功!');
+        } elseif ($act == 'submit') {
+            if (mb_strlen(Base::getPostValue('title')) < 2 || mb_strlen(Base::getPostValue('title')) > 100) {
+                return Base::retError('标题限制2-100个字!');
+            }
+            if (empty($reportDetail)) {
+                DB::table('report_lists')->insert([
+                    'username' => $user['username'],
+                    'title' => Base::getPostValue('title'),
+                    'type' => $type,
+                    'status' => '未发送',
+                    'date' => $type=='日报'?date("Ymd"):date("W"),
+                    'indate' => Base::time(),
+                ]);
+                $reportDetail = Base::DBC2A(DB::table('report_lists')->where($whereArray)->first());
+            }
+            if (empty($reportDetail)) {
+                return Base::retError('系统繁忙,请稍后再试!');
+            }
+            //
+            $ccuserArr = explode(",", Base::getPostValue('ccuser'));
+            $send = $reportDetail['status'] == '已发送' ? 1 : intval(Base::getPostValue('send'));
+            $ccuserAgain = $reportDetail['status'] == '已发送';
+            if ($send) {
+                DB::table('report_ccuser')->where(['rid' => $reportDetail['id']])->update(['cc' => 0]);
+                foreach ($ccuserArr AS $ck => $cuser) {
+                    if (!$cuser) {
+                        unset($ccuserArr[$ck]);
+                        continue;
+                    }
+                    DB::table('report_ccuser')->updateOrInsert([
+                        'rid' => $reportDetail['id'],
+                        'username' => $cuser,
+                        'indate' => Base::time()
+                    ], [
+                        'cc' => 1
+                    ]);
+                }
+                $reportDetail['status'] = '已发送';
+            }
+            //
+            DB::table('report_lists')->where('id', $reportDetail['id'])->update([
+                'title' => Base::getPostValue('title'),
+                'status' => $send ? '已发送' : '未发送',
+                'ccuser' => Base::array2string($ccuserArr)
+            ]);
+            DB::table('report_content')->updateOrInsert(['rid' => $reportDetail['id']], ['content' => Base::getPostValue('content')]);
+            //
+            $reportDetail = array_merge($reportDetail, [
+                'ccuserAgain' => $ccuserAgain,
+                'ccuser' => $ccuserArr,
+                'title' => Base::getPostValue('title'),
+                'content' => Base::getPostValue('content'),
+            ]);
+        }
+        if (empty($reportDetail)) {
+            //已完成
+            $completeContent = '';
+            $startTime = $type == '日报' ? strtotime(date('Y-m-d 00:00:00')) : strtotime(date('Y-m-d 00:00:00', strtotime('this week')));
+            $lists = Base::DBC2A(DB::table('project_task')
+                ->select(['title', 'completedate'])
+                ->where('username', $user['username'])
+                ->where('complete', 1)
+                ->where('delete', 0)
+                ->whereBetween('completedate', [$startTime, time()])
+                ->orderBy('completedate')
+                ->orderBy('id')
+                ->get());
+            foreach ($lists as $item) {
+                $pre = $type == '周报' ? ('<span>[周' . ['日', '一', '二', '三', '四', '五', '六'][date('w')] . ']</span>&nbsp;') : '';
+                $completeContent .= '<li>' . $pre . $item['title'] . '</li>';
+            }
+            if (empty($completeContent)) {
+                $completeContent = '<li>&nbsp;</li>';
+            }
+            //未完成
+            $unfinishedContent = '';
+            $finishTime = $type == '日报' ? strtotime(date('Y-m-d 23:59:59')) : strtotime(date('Y-m-d 23:59:59', strtotime('last day next week')));
+            $lists = Base::DBC2A(DB::table('project_task')
+                ->select(['title', 'enddate'])
+                ->where('username', $user['username'])
+                ->where('complete', 0)
+                ->where('delete', 0)
+                ->where('startdate', '>', 0)
+                ->where('enddate', '<', $finishTime)
+                ->orderBy('id')
+                ->get());
+            foreach ($lists as $item) {
+                $pre = $item['enddate'] > 0 && $item['enddate'] < time() ? '<span style="color:#ff0000;">[超期]</span>&nbsp;' : '';
+                $unfinishedContent .= '<li>' . $pre . $item['title'] . '</li>';
+            }
+            if (empty($unfinishedContent)) {
+                $unfinishedContent = '<li>&nbsp;</li>';
+            }
+            //
+            $reportDetail['title'] = ($user['nickname'] ?: $user['username']) . '的' . $type . '[' . $dateTitle . ']';
+            $reportDetail['ccuser'] = '';
+            $reportDetail['content'] = '<h2>已完成工作</h2><ol>' . $completeContent . '</ol><h2>未完成的工作</h2><ol>' . $unfinishedContent . '</ol>';
+            $reportDetail['status'] = '未保存';
+        } else {
+            $reportDetail['ccuser'] = implode(',', Base::string2array($reportDetail['ccuser']));
+            if (!isset($reportDetail['content'])) {
+                $reportDetail['content'] = DB::table('report_content')->select(['content'])->where('rid', $reportDetail['id'])->value('content');
+            }
+        }
+        $reportDetail['ccuserAgain'] = isset($reportDetail['ccuserAgain']) ? $reportDetail['ccuserAgain'] : false;
+        $reportDetail['ccuserArray'] = explode(',', $reportDetail['ccuser']);
+        return Base::retSuccess($act == 'submit' ? '保存成功!' : 'success', $reportDetail);
+    }
+
+    /**
+     * 我的汇报
+     *
+     * @apiParam {Number} [page]                当前页,默认:1
+     * @apiParam {Number} [pagesize]            每页显示数量,默认:20,最大:100
+     */
+    public function my()
+    {
+        $user = Users::authE();
+        if (Base::isError($user)) {
+            return $user;
+        } else {
+            $user = $user['data'];
+        }
+        //
+        $whereArray = [];
+        $whereArray[] = ['username', '=', $user['username']];
+        if (trim(Request::input('username'))) {
+            $whereArray[] = ['username', '=', trim(Request::input('username'))];
+        }
+        if (in_array(trim(Request::input('type')), ['日报', '周报'])) {
+            $whereArray[] = ['type', '=', trim(Request::input('type'))];
+        }
+        $indate = Request::input('indate');
+        if (is_array($indate)) {
+            if ($indate[0] > 0) $whereArray[] = ['indate', '>=', Base::dayTimeF($indate[0])];
+            if ($indate[1] > 0) $whereArray[] = ['indate', '<=', Base::dayTimeE($indate[1])];
+        }
+        //
+        $orderBy = '`indate` DESC,`id` DESC';
+        $sorts = Base::json2array(Request::input('sorts'));
+        if (in_array($sorts['order'], ['asc', 'desc'])) {
+            switch ($sorts['key']) {
+                case 'date':
+                case 'indate':
+                    $orderBy = '`' . $sorts['key'] . '` ' . $sorts['order'] . ',`id` DESC';
+                    break;
+            }
+        }
+        //
+        $lists = DB::table('report_lists')
+            ->where($whereArray)
+            ->orderByRaw($orderBy)
+            ->paginate(Base::getPaginate(100, 20));
+        $lists = Base::getPageList($lists);
+        if ($lists['total'] == 0) {
+            return Base::retError('未找到任何相关的汇报', $lists);
+        }
+        foreach ($lists['lists'] AS $key => $item) {
+            $lists['lists'][$key]['ccuser'] = Base::string2array($item['ccuser']);
+        }
+        return Base::retSuccess('success', $lists);
+    }
+
+    /**
+     * 我的汇报
+     *
+     * @apiParam {String} [username]            汇报者用户名
+     * @apiParam {Array} [indate]               汇报时间
+     * @apiParam {String} [type]                类型
+     * - 日报
+     * - 周报
+     * @apiParam {Object} [sorts]               排序方式,格式:{key:'', order:''}
+     * - key: title|username|indate
+     * - order: asc|desc
+     * @apiParam {Number} [page]                当前页,默认:1
+     * @apiParam {Number} [pagesize]            每页显示数量,默认:20,最大:100
+     */
+    public function receive()
+    {
+        $user = Users::authE();
+        if (Base::isError($user)) {
+            return $user;
+        } else {
+            $user = $user['data'];
+        }
+        //
+        $whereArray = [];
+        $whereArray[] = ['report_ccuser.username', '=', $user['username']];
+        $whereArray[] = ['report_ccuser.cc', '=', 1];
+        if (trim(Request::input('username'))) {
+            $whereArray[] = ['report_lists.username', '=', trim(Request::input('username'))];
+        }
+        if (in_array(trim(Request::input('type')), ['日报', '周报'])) {
+            $whereArray[] = ['report_lists.type', '=', trim(Request::input('type'))];
+        }
+        $indate = Request::input('indate');
+        if (is_array($indate)) {
+            if ($indate[0] > 0) $whereArray[] = ['report_lists.indate', '>=', Base::dayTimeF($indate[0])];
+            if ($indate[1] > 0) $whereArray[] = ['report_lists.indate', '<=', Base::dayTimeE($indate[1])];
+        }
+        //
+        $builder = DB::table('report_lists')
+            ->join('report_ccuser', 'report_lists.id', '=', 'report_ccuser.rid')
+            ->select(['report_lists.*', 'report_ccuser.indate as senddate'])
+            ->where($whereArray);
+        $sorts = Base::json2array(Request::input('sorts'));
+        if (in_array($sorts['order'], ['asc', 'desc'])) {
+            switch ($sorts['key']) {
+                case 'title':
+                case 'username':
+                    $builder->orderBy($sorts['key'], $sorts['order']);
+                    break;
+                case 'indate':
+                    $builder->orderBy('report_ccuser.indate', $sorts['order']);
+                    break;
+            }
+        } else {
+            $builder->orderByDesc('report_ccuser.indate');
+            $builder->orderByDesc('report_ccuser.id');
+        }
+        //
+        $builder->orderByDesc('date');
+        $lists = $builder->paginate(Base::getPaginate(100, 20));
+        $lists = Base::getPageList($lists);
+        if ($lists['total'] == 0) {
+            return Base::retError('未找到任何相关的汇报', $lists);
+        }
+        foreach ($lists['lists'] AS $key => $item) {
+            $lists['lists'][$key]['ccuser'] = Base::string2array($item['ccuser']);
+        }
+        return Base::retSuccess('success', $lists);
+    }
+
+    /**
+     * 获取我上次抄送的人
+     * @return array|mixed
+     */
+    public function prevcc()
+    {
+        $user = Users::authE();
+        if (Base::isError($user)) {
+            return $user;
+        } else {
+            $user = $user['data'];
+        }
+        //
+        $rid = DB::table('report_ccuser')
+            ->join('report_lists', 'report_lists.id', '=', 'report_ccuser.rid')
+            ->where('report_lists.username', $user['username'])
+            ->orderByDesc('report_ccuser.id')
+            ->value('rid');
+        if (empty($rid)) {
+            return Base::retError('没有相关数据!');
+        }
+        $lists = Base::DBC2A(DB::table('report_ccuser')->select(['username'])->where('rid', $rid)->pluck('username'));
+        if (empty($lists)) {
+            return Base::retError('没有相关数据!');
+        }
+        return Base::retSuccess('success', [
+            'lists' => $lists
+        ]);
+    }
+}

+ 289 - 0
app/Http/Controllers/Api/SystemController.php

@@ -0,0 +1,289 @@
+<?php
+
+namespace App\Http\Controllers\Api;
+
+use App\Http\Controllers\Controller;
+use App\Module\Base;
+use App\Module\Users;
+use Request;
+
+/**
+ * @apiDefine system
+ *
+ * 系统
+ */
+class SystemController extends Controller
+{
+    public function __invoke($method, $action = '')
+    {
+        $app = $method ? $method : 'main';
+        if ($action) {
+            $app .= "__" . $action;
+        }
+        return (method_exists($this, $app)) ? $this->$app() : Base::ajaxError("404 not found (" . str_replace("__", "/", $app) . ").");
+    }
+
+    /**
+     * 获取设置、保存设置
+     *
+     * @apiParam {String} type
+     * - get: 获取(默认)
+     * - save: 保存设置(参数:logo、github、reg、callav、autoArchived、archivedDay)
+     */
+    public function setting()
+    {
+        $type = trim(Request::input('type'));
+        if ($type == 'save') {
+            if (env("SYSTEM_SETTING") == 'disabled') {
+                return Base::retError('当前环境禁止修改!');
+            }
+            $user = Users::authE();
+            if (Base::isError($user)) {
+                return $user;
+            } else {
+                $user = $user['data'];
+            }
+            if (Base::isError(Users::identity('admin'))) {
+                return Base::retError('权限不足!', [], -1);
+            }
+            $all = Request::input();
+            foreach ($all AS $key => $value) {
+                if (!in_array($key, ['logo', 'github', 'reg', 'callav', 'autoArchived', 'archivedDay'])) {
+                    unset($all[$key]);
+                }
+            }
+            $all['logo'] = is_array($all['logo']) ? $all['logo'][0]['path'] : $all['logo'];
+            $all['archivedDay'] = intval($all['archivedDay']);
+            if ($all['autoArchived'] == 'open') {
+                if ($all['archivedDay'] <= 0) {
+                    return Base::retError(['自动归档时间不可小于%天!', 1]);
+                } elseif ($all['archivedDay'] > 100) {
+                    return Base::retError(['自动归档时间不可大于%天!', 100]);
+                }
+            }
+            $setting = Base::setting('system', Base::newTrim($all));
+        } else {
+            $setting = Base::setting('system');
+        }
+        $setting['logo'] = Base::fillUrl($setting['logo']);
+        $setting['enterprise'] = env('ENTERPRISE_SHOW') ? 'show': '';
+        return Base::retSuccess('success', $setting ? $setting : json_decode('{}'));
+    }
+
+    /**
+     * 获取终端详细信息
+     */
+    public function get__info()
+    {
+        if (Request::input("key") !== env('APP_KEY')) {
+            return [];
+        }
+        return Base::retSuccess('success', [
+            'ip' => Base::getIp(),
+            'ip-info' => Base::getIpInfo(Base::getIp()),
+            'ip-iscn' => Base::isCnIp(Base::getIp()),
+            'header' => Request::header(),
+            'token' => Base::getToken(),
+            'url' => url('') . Base::getUrl(),
+        ]);
+    }
+
+    /**
+     * 获取IP地址
+     */
+    public function get__ip() {
+        return Base::getIp();
+    }
+
+    /**
+     * 是否中国IP地址
+     */
+    public function get__cnip() {
+        return Base::isCnIp(Request::input('ip'));
+    }
+
+    /**
+     * 获取IP地址详细信息
+     */
+    public function get__ipinfo() {
+        return Base::getIpInfo(Request::input("ip"));
+    }
+
+    /**
+     * 获取websocket地址
+     */
+    public function get__wsurl() {
+        $wsurl = env('LARAVELS_PROXY_URL');
+        if (!$wsurl) {
+            $wsurl = url('');
+            $wsurl = str_replace('https://', 'wss://', $wsurl);
+            $wsurl = str_replace('http://', 'ws://', $wsurl);
+            $wsurl.= '/ws';
+        }
+        return Base::retSuccess('success', [
+            'wsurl' => $wsurl,
+        ]);
+    }
+
+    /**
+     * 上传图片
+     */
+    public function imgupload()
+    {
+        if (Users::token2userid() === 0) {
+            return Base::retError('身份失效,等重新登录!');
+        }
+        $scale = [intval(Request::input('width')), intval(Request::input('height'))];
+        if (!$scale[0] && !$scale[1]) {
+            $scale = [2160, 4160, -1];
+        }
+        $path = "uploads/picture/" . Users::token2userid() . "/" . date("Ym") . "/";
+        $image64 = trim(Base::getPostValue('image64'));
+        $fileName = trim(Base::getPostValue('filename'));
+        if ($image64) {
+            $data = Base::image64save([
+                "image64" => $image64,
+                "path" => $path,
+                "fileName" => $fileName,
+                "scale" => $scale
+            ]);
+        } else {
+            $data = Base::upload([
+                "file" => Request::file('image'),
+                "type" => 'image',
+                "path" => $path,
+                "fileName" => $fileName,
+                "scale" => $scale
+            ]);
+        }
+        if (Base::isError($data)) {
+            return Base::retError($data['msg']);
+        } else {
+            return Base::retSuccess('success', $data['data']);
+        }
+    }
+
+    /**
+     * 浏览图片空间
+     */
+    public function imgview()
+    {
+        if (Users::token2userid() === 0) {
+            return Base::retError('身份失效,等重新登录!');
+        }
+        $publicPath = "uploads/picture/" . Users::token2userid() . "/";
+        $dirPath = public_path($publicPath);
+        $dirs = $files = [];
+        //
+        $path = Request::input('path');
+        if ($path && is_string($path)) {
+            $path = str_replace(array('||', '|'), '/', $path);
+            $path = trim($path, '/');
+            $path = str_replace('..', '', $path);
+            $path = Base::leftDelete($path, $publicPath);
+            if ($path) {
+                $path = $path . '/';
+                $dirPath .= $path;
+                //
+                $dirs[] = [
+                    'type' => 'dir',
+                    'title' => '...',
+                    'path' => substr(substr($path, 0, -1), 0, strripos(substr($path, 0, -1), '/')),
+                    'url' => '',
+                    'thumb' => Base::fillUrl('images/other/dir.png'),
+                    'inode' => 0,
+                ];
+            }
+        } else {
+            $path = '';
+        }
+        $list = glob($dirPath . '*', GLOB_BRACE);
+        foreach ($list as $v) {
+            $filename = basename($v);
+            $pathTemp = $publicPath . $path . $filename;
+            if (is_dir($v)) {
+                $dirs[] = [
+                    'type' => 'dir',
+                    'title' => $filename,
+                    'path' => $pathTemp,
+                    'url' => Base::fillUrl($pathTemp),
+                    'thumb' => Base::fillUrl('images/other/dir.png'),
+                    'inode' => fileatime($v),
+                ];
+            } elseif (substr($filename, -10) != "_thumb.jpg") {
+                $array = [
+                    'type' => 'file',
+                    'title' => $filename,
+                    'path' => $pathTemp,
+                    'url' => Base::fillUrl($pathTemp),
+                    'thumb' => $pathTemp,
+                    'inode' => fileatime($v),
+                ];
+                //
+                $extension = pathinfo($dirPath . $filename, PATHINFO_EXTENSION);
+                if (in_array($extension, array('gif', 'jpg', 'jpeg', 'png', 'bmp'))) {
+                    if (file_exists($dirPath . $filename . '_thumb.jpg')) {
+                        $array['thumb'] .= '_thumb.jpg';
+                    }
+                    $array['thumb'] = Base::fillUrl($array['thumb']);
+                    $files[] = $array;
+                }
+            }
+        }
+        if ($dirs) {
+            $inOrder = [];
+            foreach ($dirs as $key => $item) {
+                $inOrder[$key] = $item['title'];
+            }
+            array_multisort($inOrder, SORT_DESC, $dirs);
+        }
+        if ($files) {
+            $inOrder = [];
+            foreach ($files as $key => $item) {
+                $inOrder[$key] = $item['inode'];
+            }
+            array_multisort($inOrder, SORT_DESC, $files);
+        }
+        //
+        return Base::retSuccess('success', ['dirs' => $dirs, 'files' => $files]);
+    }
+
+    /**
+     * 上传文件
+     */
+    public function fileupload()
+    {
+        if (Users::token2userid() === 0) {
+            return Base::retError('身份失效,等重新登录!');
+        }
+        $path = "uploads/files/" . Users::token2userid() . "/" . date("Ym") . "/";
+        $image64 = trim(Base::getPostValue('image64'));
+        $fileName = trim(Base::getPostValue('filename'));
+        if ($image64) {
+            $data = Base::image64save([
+                "image64" => $image64,
+                "path" => $path,
+                "fileName" => $fileName,
+            ]);
+        } else {
+            $data = Base::upload([
+                "file" => Request::file('files'),
+                "type" => 'file',
+                "path" => $path,
+                "fileName" => $fileName,
+            ]);
+        }
+        //
+        return $data;
+    }
+
+    /**
+     * 清理opcache数据
+     * @return int
+     */
+    public function opcache()
+    {
+        opcache_reset();
+        return Base::time();
+    }
+}

+ 586 - 0
app/Http/Controllers/Api/UsersController.php

@@ -0,0 +1,586 @@
+<?php
+
+namespace App\Http\Controllers\Api;
+
+use App\Http\Controllers\Controller;
+use App\Model\DBCache;
+use App\Module\Base;
+use App\Module\Users;
+use DB;
+use Request;
+use Session;
+
+/**
+ * @apiDefine users
+ *
+ * 会员
+ */
+class UsersController extends Controller
+{
+    public function __invoke($method, $action = '')
+    {
+        $app = $method ? $method : 'main';
+        if ($action) {
+            $app .= "__" . $action;
+        }
+        return (method_exists($this, $app)) ? $this->$app() : Base::ajaxError("404 not found (" . str_replace("__", "/", $app) . ").");
+    }
+
+    /**
+     * 登陆、注册
+     *
+     * @apiParam {String} type           类型
+     * - login:登录(默认)
+     * - reg:注册
+     * @apiParam {String} username       用户名
+     * @apiParam {String} userpass       密码
+     */
+    public function login()
+    {
+        $type = trim(Request::input('type'));
+        $username = trim(Request::input('username'));
+        $userpass = trim(Request::input('userpass'));
+        if ($type == 'reg') {
+            $setting = Base::setting('system');
+            if ($setting['reg'] == 'close') {
+                return Base::retError('未开放注册。');
+            }
+            $user = Users::reg($username, $userpass);
+            if (Base::isError($user)) {
+                return $user;
+            } else {
+                $user = $user['data'];
+            }
+        } else {
+            $user = Base::DBC2A(DB::table('users')->where('username', $username)->first());
+            if (empty($user)) {
+                return Base::retError('账号或密码错误。');
+            }
+            if ($user['userpass'] != Base::md52($userpass, $user['encrypt'])) {
+                return Base::retError('账号或密码错误!');
+            }
+            if (in_array($user['id'], [1, 2])) {
+                $user['setting'] = Base::string2array($user['setting']);
+                if (intval($user['setting']['version']) < 1) {
+                    $user['setting']['version'] = intval($user['setting']['version']) + 1;
+                    $user['identity'] = ',admin,';
+                    DB::table('users')->where('username', $username)->update([
+                        'setting' => Base::array2string($user['setting']),
+                        'identity' => $user['identity'],
+                    ]);
+                }
+            }
+        }
+        //
+        $array = [
+            'token' => Users::token($user),
+            'loginnum' => $user['loginnum'] + 1,
+            'lastip' => Base::getIp(),
+            'lastdate' => Base::time(),
+            'lineip' => Base::getIp(),
+            'linedate' => Base::time(),
+        ];
+        Base::array_over($user, $array);
+        DB::table('users')->where('id', $user['id'])->update($array);
+        //
+        return Base::retSuccess($type == 'reg' ? "注册成功!" : "登陆成功!", Users::retInfo($user));
+    }
+
+    /**
+     * 获取我的信息
+     *
+     * @apiParam {String} [callback]           jsonp返回字段
+     */
+    public function info()
+    {
+        $callback = Request::input('callback');
+        //
+        $user = Users::authE();
+        if (Base::isError($user)) {
+            if (strlen($callback) > 3) {
+                return $callback . '(' . json_encode($user) . ')';
+            }
+            return $user;
+        } else {
+            $user = $user['data'];
+        }
+        //
+        if (strlen($callback) > 3) {
+            return $callback . '(' . json_encode(Base::retSuccess('success', Users::retInfo($user))) . ')';
+        }
+        return Base::retSuccess('success', Users::retInfo($user));
+    }
+
+    /**
+     * 获取指定会员基本信息
+     *
+     * @apiParam {String|jsonArray} username          会员用户名(多个格式:jsonArray,一次最多30个)
+     */
+    public function basic()
+    {
+        $username = trim(Request::input('username'));
+        $array = Base::json2array($username);
+        if (empty($array)) {
+            $array[] = $username;
+        }
+        if (count($array) > 50) {
+            return Base::retError(['一次最多只能获取%条数据!', 50]);
+        }
+        $retArray = [];
+        foreach ($array AS $name) {
+            $basic = Users::username2basic($name);
+            if ($basic) {
+                $retArray[] = $basic;
+            }
+        }
+        return Base::retSuccess('success', $retArray);
+    }
+
+    /**
+     * 搜索会员列表
+     *
+     * @apiParam {Object} where            搜索条件
+     * - where.usernameequal
+     * - where.nousername
+     * - where.username
+     * - where.noidentity
+     * - where.identity
+     * - where.noprojectid
+     * - where.projectid
+     * - where.nobookid
+     * @apiParam {Number} [take]           获取数量,10-100
+     */
+    public function searchinfo()
+    {
+        $keys = Request::input('where');
+        $whereArr = [];
+        $whereRaw = null;
+        if ($keys['usernameequal'])     $whereArr[] = ['username', '=', $keys['usernameequal']];
+        if ($keys['identity'])          $whereArr[] = ['identity', 'like', '%,' . $keys['identity'] . ',%'];
+        if ($keys['noidentity'])        $whereArr[] = ['identity', 'not like', '%,' . $keys['noidentity'] . ',%'];
+        if ($keys['username']) {
+            $whereRaw.= $whereRaw ? ' AND ' : '';
+            $whereRaw.= "(`username` LIKE '%" . $keys['username'] . "%' OR `nickname` LIKE '%" . $keys['username'] . "%')";
+        }
+        if (intval($keys['projectid']) > 0) {
+            $whereRaw.= $whereRaw ? ' AND ' : '';
+            $whereRaw.= "`username` IN (SELECT username FROM `" . env('DB_PREFIX') . "project_users` WHERE `type`='成员' AND `projectid`=" . intval($keys['projectid']) .")";
+        }
+        if ($keys['nousername']) {
+            $nousername = [];
+            foreach (explode(",", $keys['nousername']) AS $name) {
+                $name = trim($name);
+                if ($name && !in_array($name, $nousername)) {
+                    $nousername[] = $name;
+                }
+            }
+            if ($nousername) {
+                $whereRaw.= $whereRaw ? ' AND ' : '';
+                $whereRaw.= "(`username` NOT IN ('" . implode("','", $nousername) . "'))";
+            }
+        }
+        if (intval($keys['noprojectid']) > 0) {
+            $whereRaw.= $whereRaw ? ' AND ' : '';
+            $whereRaw.= "`username` NOT IN (SELECT username FROM `" . env('DB_PREFIX') . "project_users` WHERE `type`='成员' AND `projectid`=" . intval($keys['noprojectid']) .")";
+        }
+        if (intval($keys['nobookid']) > 0) {
+            $whereRaw.= $whereRaw ? ' AND ' : '';
+            $whereRaw.= "`username` NOT IN (SELECT username FROM `" . env('DB_PREFIX') . "docs_users` WHERE `bookid`=" . intval($keys['nobookid']) .")";
+        }
+        //
+        $lists = DBCache::table('users')->select(['id', 'username', 'nickname', 'userimg', 'profession'])
+            ->where($whereArr)
+            ->whereRaw($whereRaw)
+            ->orderBy('id')
+            ->cacheMinutes(now()->addSeconds(10))
+            ->take(Base::getPaginate(100, 10, 'take'))
+            ->get();
+        foreach ($lists AS $key => $item) {
+            $lists[$key]['userimg'] = Users::userimg($item['userimg']);
+            $lists[$key]['identitys'] = explode(",", trim($item['identity'], ","));
+            $lists[$key]['setting'] = Base::string2array($item['setting']);
+        }
+        return Base::retSuccess('success', $lists);
+    }
+
+    /**
+     * 修改资料
+     *
+     * @apiParam {Object} [userimg]             会员头像
+     * @apiParam {String} [nickname]            昵称
+     * @apiParam {String} [profession]          职位/职称
+     * @apiParam {String} [bgid]                背景编号
+     */
+    public function editdata()
+    {
+        $user = Users::authE();
+        if (Base::isError($user)) {
+            return $user;
+        } else {
+            $user = $user['data'];
+        }
+        //
+        $array = [];
+        //头像
+        $userimg = Request::input('userimg');
+        if ($userimg) {
+            $userimg = is_array($userimg) ? $userimg[0]['path'] : $userimg;
+            $array['userimg'] = Base::unFillUrl($userimg);
+        }
+        //昵称
+        $nickname = trim(Request::input('nickname'));
+        if ($nickname) {
+            if (mb_strlen($nickname) < 2) {
+                return Base::retError('昵称不可以少于2个字!');
+            } elseif (mb_strlen($nickname) > 8) {
+                return Base::retError('昵称最多只能设置8个字!');
+            } else {
+                $array['nickname'] = $nickname;
+            }
+        }
+        //职位/职称
+        $profession = trim(Request::input('profession'));
+        if ($profession) {
+            if (mb_strlen($profession) < 2) {
+                return Base::retError('职位/职称不可以少于2个字!');
+            } elseif (mb_strlen($profession) > 20) {
+                return Base::retError('职位/职称最多只能设置20个字!');
+            } else {
+                $array['profession'] = $profession;
+            }
+        }
+        //背景
+        $bgid = intval(Request::input('bgid'));
+        if ($bgid > 0) {
+            $array['bgid'] = $bgid;
+        }
+        //
+        if ($array) {
+            DB::table('users')->where('id', $user['id'])->update($array);
+            Users::AZUpdate($user['id']);
+        } else {
+            return Base::retError('请设置要修改的内容!');
+        }
+        return Base::retSuccess('修改成功!');
+    }
+
+    /**
+     * 修改密码
+     *
+     * @apiParam {String} oldpass           旧密码
+     * @apiParam {String} newpass           新密码
+     */
+    public function editpass()
+    {
+        $user = Users::authE();
+        if (Base::isError($user)) {
+            return $user;
+        } else {
+            $user = $user['data'];
+        }
+        //
+        $oldpass = trim(Request::input('oldpass'));
+        $newpass = trim(Request::input('newpass'));
+        if (strlen($newpass) < 6) {
+            return Base::retError('密码设置不能小于6位数!');
+        } elseif (strlen($newpass) > 32) {
+            return Base::retError('密码最多只能设置32位数!');
+        }
+        if ($oldpass == $newpass) {
+            return Base::retError('新旧密码一致!');
+        }
+        //
+        if (env("PASSWORD_ADMIN") == 'disabled') {
+            if ($user['id'] == 1) {
+                return Base::retError('当前环境禁止修改密码!');
+            }
+        }
+        if (env("PASSWORD_OWNER") == 'disabled') {
+            return Base::retError('当前环境禁止修改密码!');
+        }
+        //
+        if ($user['setpass']) {
+            $verify = DB::table('users')->where(['id'=>$user['id'], 'userpass'=>Base::md52($oldpass, Users::token2encrypt())])->count();
+            if (empty($verify)) {
+                return Base::retError('请填写正确的旧密码!');
+            }
+        }
+        $encrypt = Base::generatePassword(6);
+        DB::table('users')->where('id', $user['id'])->update([
+            'encrypt' => $encrypt,
+            'userpass' => Base::md52($newpass, $encrypt),
+            'changepass' => 0
+        ]);
+        return Base::retSuccess('修改成功');
+    }
+
+    /**
+     * 团队列表
+     *
+     * @apiParam {Object} [sorts]               排序方式,格式:{key:'', order:''}
+     * - key: username|az|id(默认)
+     * - order: asc|desc
+     * @apiParam {String} [username]            指定获取某个成员(返回对象)
+     * @apiParam {Number} [page]                当前页,默认:1
+     * @apiParam {Number} [pagesize]            每页显示数量,默认:10,最大:100
+     */
+    public function team__lists()
+    {
+        $user = Users::authE();
+        if (Base::isError($user)) {
+            return $user;
+        } else {
+            $user = $user['data'];
+        }
+        //
+        $username = trim(Request::input('username'));
+        $whereArray = [];
+        if ($username) {
+            $whereArray[] = ['username', '=', $username];
+        }
+        //
+        $orderBy = '`id` DESC';
+        $sorts = Base::json2array(Request::input('sorts'));
+        if (in_array($sorts['order'], ['asc', 'desc'])) {
+            switch ($sorts['key']) {
+                case 'username':
+                    $orderBy = '`' . $sorts['key'] . '` ' . $sorts['order'] . ',`id` DESC';
+                    break;
+                case 'az':
+                    $orderBy = '`' . $sorts['key'] . '` ' . $sorts['order'] . ',`username` ' . $sorts['order'] . ',`id` DESC';
+                    break;
+            }
+        }
+        //
+        $lists = DB::table('users')->where($whereArray)->select(['id', 'identity', 'username', 'nickname', 'az', 'userimg', 'profession', 'regdate'])->orderByRaw($orderBy)->paginate(Base::getPaginate(100, 10));
+        $lists = Base::getPageList($lists);
+        if ($lists['total'] == 0) {
+            return Base::retError('未找到任何相关的团队成员');
+        }
+        foreach ($lists['lists'] AS $key => $item) {
+            $lists['lists'][$key]['identity'] = is_array($item['identity']) ? $item['identity'] : explode(",", trim($item['identity'], ","));
+            $lists['lists'][$key]['userimg'] = Users::userimg($item['userimg']);
+        }
+        if ($username) {
+            return Base::retSuccess('success', $lists['lists'][0]);
+        }
+        return Base::retSuccess('success', $lists);
+    }
+
+    /**
+     * 添加团队成员
+     *
+     * @apiParam {Number} [id]                  用户ID(留空为添加用户)
+     * @apiParam {String} username              用户名(修改时无效,多个用英文逗号分隔)
+     * @apiParam {String} userpass              密码
+     * @apiParam {Object} [userimg]             会员头像
+     * @apiParam {String} [nickname]            昵称
+     * @apiParam {String} [profession]          职位/职称
+     * @apiParam {Number} changepass            登陆是否需要修改密码
+     */
+    public function team__add()
+    {
+        $user = Users::authE();
+        if (Base::isError($user)) {
+            return $user;
+        } else {
+            $user = $user['data'];
+        }
+        //
+        if (Base::isError(Users::identity('admin'))) {
+            return Base::retError('权限不足!', [], -1);
+        }
+        //头像
+        $userimg = Request::input('userimg');
+        if ($userimg) {
+            $userimg = is_array($userimg) ? $userimg[0]['path'] : $userimg;
+        }
+        //昵称
+        $nickname = trim(Request::input('nickname'));
+        if ($nickname) {
+            if (mb_strlen($nickname) < 2) {
+                return Base::retError('昵称不可以少于2个字!');
+            } elseif (mb_strlen($nickname) > 8) {
+                return Base::retError('昵称最多只能设置8个字!');
+            }
+        }
+        //职位/职称
+        $profession = trim(Request::input('profession'));
+        if ($profession) {
+            if (mb_strlen($profession) < 2) {
+                return Base::retError('职位/职称不可以少于2个字!');
+            } elseif (mb_strlen($profession) > 20) {
+                return Base::retError('职位/职称最多只能设置20个字!');
+            }
+        }
+        //
+        $id = intval(Request::input('id'));
+        $userpass = trim(Request::input('userpass'));
+        $otherArray = [
+            'userimg' => $userimg ?: '',
+            'nickname' => $nickname ?: '',
+            'profession' => $profession ?: '',
+            'changepass' => intval(Request::input('changepass')),
+        ];
+        if ($id > 0) {
+            //开始修改
+            if ($userpass) {
+                if (strlen($userpass) < 6) {
+                    return Base::retError('密码设置不能小于6位数!');
+                } elseif (strlen($userpass) > 32) {
+                    return Base::retError('密码最多只能设置32位数!');
+                }
+                $encrypt = Base::generatePassword(6);
+                $otherArray['encrypt'] = $encrypt;
+                $otherArray['userpass'] = Base::md52($userpass, $encrypt);
+            }
+            DB::table('users')->where('id', $id)->update($otherArray);
+            Users::AZUpdate($id);
+            return Base::retSuccess('修改成功!');
+        } else {
+            //开始注册
+            if (strlen($userpass) < 6) {
+                return Base::retError('密码设置不能小于6位数!');
+            } elseif (strlen($userpass) > 32) {
+                return Base::retError('密码最多只能设置32位数!');
+            }
+            $username = trim(Request::input('username'));
+            $array = array_values(array_filter(array_unique(explode(",", $username))));
+            if (empty($array)) {
+                return Base::retError('请填写有效的用户名!');
+            }
+            if (count($array) > 500) {
+                return Base::retError(['一次最多只能添加%个账号!', 500]);
+            }
+            foreach ($array AS $item) {
+                $username = trim($item);
+                if ($username) {
+                    $user = Users::reg($username, $userpass, $otherArray);
+                    if (Base::isError($user)) {
+                        return $user;
+                    }
+                }
+            }
+            return Base::retSuccess('添加成功!');
+        }
+    }
+
+    /**
+     * 删除团队成员
+     *
+     * @apiParam {String} username           用户名
+     */
+    public function team__delete()
+    {
+        $user = Users::authE();
+        if (Base::isError($user)) {
+            return $user;
+        } else {
+            $user = $user['data'];
+        }
+        //
+        if (Base::isError(Users::identity('admin'))) {
+            return Base::retError('权限不足!', [], -1);
+        }
+        $username = trim(Request::input('username'));
+        if ($user['username'] == $username) {
+            return Base::retError('不能删除自己!');
+        }
+        //
+        if (DB::table('users')->where('username', $username)->delete()) {
+            return Base::retSuccess('删除成功!');
+        } else {
+            return Base::retError('删除失败!');
+        }
+    }
+
+    /**
+     * 设置、删除管理员
+     *
+     * @apiParam {String} act           操作
+     * - set: 设置管理员
+     * - del: 删除管理员
+     * @apiParam {String} username      用户名
+     */
+    public function team__admin()
+    {
+        $user = Users::authE();
+        if (Base::isError($user)) {
+            return $user;
+        } else {
+            $user = $user['data'];
+        }
+        //
+        if (Base::isError(Users::identity('admin'))) {
+            return Base::retError('权限不足!', [], -1);
+        }
+        //
+        $username = trim(Request::input('username'));
+        if ($user['username'] == $username) {
+            return Base::retError('不能操作自己!');
+        }
+        $userInfo = Base::DBC2A(DB::table('users')->where('username', $username)->first());
+        if (empty($userInfo)) {
+            return Base::retError('成员不存在!');
+        }
+        $identity = is_array($userInfo['identity']) ? $userInfo['identity'] : explode(",", trim($userInfo['identity'], ","));
+        $isUp = false;
+        if (trim(Request::input('act')) == 'del') {
+            if (Users::identityRaw('admin', $identity)) {
+                $identity = array_diff($identity, ['admin']);
+                $isUp = true;
+            }
+        } else {
+            if (!Users::identityRaw('admin', $identity)) {
+                $identity[] = 'admin';
+                $isUp = true;
+            }
+        }
+        if ($isUp) {
+            DB::table('users')->where('username', $username)->update([
+                'identity' => $identity ? (',' . implode(",", $identity) . ',') : ''
+            ]);
+        }
+        return Base::retSuccess('操作成功!', [
+            'up' => $isUp ? 1 : 0,
+            'identity' => $identity
+        ]);
+    }
+
+    /**
+     * 设置、删除友盟token
+     *
+     * @apiParam {String} act           操作
+     * - set: 设置token
+     * - del: 删除token
+     * @apiParam {String} token         友盟token
+     * @apiParam {String} platform      ios|android
+     */
+    public function umeng__token()
+    {
+        $act = trim(Request::input('act'));
+        $token = trim(Request::input('token'));
+        if (empty($token)) {
+            return Base::retError('token empty');
+        }
+        $platform = strtolower(trim(Request::input('platform')));
+        DB::table('umeng')->where('token', $token)->delete();
+        //
+        if ($act == 'set') {
+            $user = Users::authE();
+            if (Base::isError($user)) {
+                return $user;
+            } else {
+                $user = $user['data'];
+            }
+            DB::table('umeng')->insert([
+                'token' => $token,
+                'username' => $user['username'],
+                'platform' => $platform,
+                'update' => Base::time(),
+            ]);
+        }
+        //
+        return Base::retSuccess('success');
+    }
+}

+ 10 - 0
app/Http/Controllers/Api/apidoc.json

@@ -0,0 +1,10 @@
+{
+  "name": "API",
+  "title": "APP接口",
+  "version": "2.0.0",
+  "description": "APP接口文档",
+  "url": "https://你的域名/",
+  "template": {
+    "withGenerator": false
+  }
+}

+ 89 - 0
app/Http/Controllers/Api/apidoc.php

@@ -0,0 +1,89 @@
+<?php
+
+/**
+ * 给apidoc项目增加顺序编号
+ */
+
+error_reporting(E_ALL & ~E_NOTICE);
+
+$path = dirname(__FILE__). '/';
+$lists = scandir($path);
+//
+foreach ($lists AS $item) {
+    $fillPath = $path . $item;
+    if (substr($fillPath, -14) == 'Controller.php') {
+        $content = file_get_contents($fillPath);
+        preg_match_all("/\* @api \{(.+?)\} (.*?)\n/i", $content, $matchs);
+        $i = 1;
+        foreach ($matchs[2] AS $key=>$text) {
+            if (in_array(strtolower($matchs[1][$key]), array('get', 'post'))) {
+                $expl = explode(" ", __sRemove($text));
+                $end = $expl[1];
+                if ($expl[2]) {
+                    $end = '';
+                    foreach ($expl AS $k=>$v) { if ($k >= 2) { $end.= " ".$v; } }
+                }
+                $newtext = "* @api {".$matchs[1][$key]."} ".$expl[0]."          ".__zeroFill($i, 2).". ".trim($end);
+                $content = str_replace("* @api {".$matchs[1][$key]."} ".$text, $newtext, $content);
+                $i++;
+                //
+                echo $newtext;
+                echo "\r\n";
+            }
+        }
+        if ($i > 1) {
+            file_put_contents($fillPath, $content);
+        }
+    }
+}
+echo "Success \n";
+
+/** ************************************************************** */
+/** ************************************************************** */
+/** ************************************************************** */
+
+/**
+ * 替换所有空格
+ * @param $str
+ * @return mixed
+ */
+function __sRemove($str) {
+    $str = str_replace("  ", " ", $str);
+    if (__strExists($str, "  ")) {
+        return __sRemove($str);
+    }
+    return $str;
+}
+
+/**
+ * 是否包含字符
+ * @param $string
+ * @param $find
+ * @return bool
+ */
+function __strExists($string, $find)
+{
+    return !(strpos($string, $find) === FALSE);
+}
+
+/**
+ * @param string $str 补零
+ * @param int $length
+ * @param int $after
+ * @return bool|string
+ */
+function __zeroFill($str, $length = 0, $after = 1) {
+    if (strlen($str) >= $length) {
+        return $str;
+    }
+    $_str = '';
+    for ($i = 0; $i < $length; $i++) {
+        $_str .= '0';
+    }
+    if ($after) {
+        $_ret = substr($_str . $str, $length * -1);
+    } else {
+        $_ret = substr($str . $_str, 0, $length);
+    }
+    return $_ret;
+}

+ 13 - 0
app/Http/Controllers/Controller.php

@@ -0,0 +1,13 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
+use Illuminate\Foundation\Bus\DispatchesJobs;
+use Illuminate\Foundation\Validation\ValidatesRequests;
+use Illuminate\Routing\Controller as BaseController;
+
+class Controller extends BaseController
+{
+    use AuthorizesRequests, DispatchesJobs, ValidatesRequests;
+}

+ 46 - 0
app/Http/Controllers/IndexController.php

@@ -0,0 +1,46 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Module\Base;
+use Redirect;
+
+
+/**
+ * 页面
+ * Class IndexController
+ * @package App\Http\Controllers
+ */
+class IndexController extends Controller
+{
+
+    public function __invoke($method, $action = '', $child = '')
+    {
+        $app = $method ? $method : 'main';
+        if ($action) {
+            $app .= "__" . $action;
+        }
+        if (!method_exists($this, $app)) {
+            $app = 'main';
+        }
+        return $this->$app($child);
+    }
+
+    /**
+     * 首页
+     * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
+     */
+    public function main()
+    {
+        return view('main', ['version' => Base::getVersion()]);
+    }
+
+    /**
+     * 接口文档
+     * @return \Illuminate\Http\RedirectResponse
+     */
+    public function api()
+    {
+        return Redirect::to(Base::fillUrl('docs'), 301);
+    }
+}

+ 66 - 0
app/Http/Kernel.php

@@ -0,0 +1,66 @@
+<?php
+
+namespace App\Http;
+
+use Illuminate\Foundation\Http\Kernel as HttpKernel;
+
+class Kernel extends HttpKernel
+{
+    /**
+     * The application's global HTTP middleware stack.
+     *
+     * These middleware are run during every request to your application.
+     *
+     * @var array
+     */
+    protected $middleware = [
+        \App\Http\Middleware\TrustProxies::class,
+        \Fruitcake\Cors\HandleCors::class,
+        \App\Http\Middleware\CheckForMaintenanceMode::class,
+        \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
+        \App\Http\Middleware\TrimStrings::class,
+        \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
+    ];
+
+    /**
+     * The application's route middleware groups.
+     *
+     * @var array
+     */
+    protected $middlewareGroups = [
+        'web' => [
+            \App\Http\Middleware\EncryptCookies::class,
+            \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
+            \Illuminate\Session\Middleware\StartSession::class,
+            // \Illuminate\Session\Middleware\AuthenticateSession::class,
+            \Illuminate\View\Middleware\ShareErrorsFromSession::class,
+            \App\Http\Middleware\VerifyCsrfToken::class,
+            \Illuminate\Routing\Middleware\SubstituteBindings::class,
+        ],
+
+        'api' => [
+            'throttle:60,1',
+            \Illuminate\Routing\Middleware\SubstituteBindings::class,
+        ],
+    ];
+
+    /**
+     * The application's route middleware.
+     *
+     * These middleware may be assigned to groups or used individually.
+     *
+     * @var array
+     */
+    protected $routeMiddleware = [
+        'auth' => \App\Http\Middleware\Authenticate::class,
+        'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
+        'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
+        'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
+        'can' => \Illuminate\Auth\Middleware\Authorize::class,
+        'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
+        'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
+        'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
+        'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
+        'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
+    ];
+}

+ 32 - 0
app/Http/Middleware/ApiMiddleware.php

@@ -0,0 +1,32 @@
+<?php
+
+namespace App\Http\Middleware;
+
+use Closure;
+use Request;
+
+class ApiMiddleware
+{
+    /**
+     * Handle an incoming request.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @param  \Closure  $next
+     * @return mixed
+     */
+    public function handle($request, Closure $next)
+    {
+        global $_A;
+        $_A = [];
+
+        @error_reporting(E_ALL & ~E_NOTICE);
+
+        if (Request::input('__Access-Control-Allow-Origin') || Request::header('__Access-Control-Allow-Origin')) {
+            header('Access-Control-Allow-Origin:*');
+            header('Access-Control-Allow-Methods:GET,POST,PUT,DELETE,OPTIONS');
+            header('Access-Control-Allow-Headers:Content-Type, platform, platform-channel, token, release, Access-Control-Allow-Origin');
+        }
+
+        return $next($request);
+    }
+}

+ 21 - 0
app/Http/Middleware/Authenticate.php

@@ -0,0 +1,21 @@
+<?php
+
+namespace App\Http\Middleware;
+
+use Illuminate\Auth\Middleware\Authenticate as Middleware;
+
+class Authenticate extends Middleware
+{
+    /**
+     * Get the path the user should be redirected to when they are not authenticated.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @return string|null
+     */
+    protected function redirectTo($request)
+    {
+        if (! $request->expectsJson()) {
+            return route('login');
+        }
+    }
+}

+ 17 - 0
app/Http/Middleware/CheckForMaintenanceMode.php

@@ -0,0 +1,17 @@
+<?php
+
+namespace App\Http\Middleware;
+
+use Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode as Middleware;
+
+class CheckForMaintenanceMode extends Middleware
+{
+    /**
+     * The URIs that should be reachable while maintenance mode is enabled.
+     *
+     * @var array
+     */
+    protected $except = [
+        //
+    ];
+}

+ 17 - 0
app/Http/Middleware/EncryptCookies.php

@@ -0,0 +1,17 @@
+<?php
+
+namespace App\Http\Middleware;
+
+use Illuminate\Cookie\Middleware\EncryptCookies as Middleware;
+
+class EncryptCookies extends Middleware
+{
+    /**
+     * The names of the cookies that should not be encrypted.
+     *
+     * @var array
+     */
+    protected $except = [
+        //
+    ];
+}

+ 27 - 0
app/Http/Middleware/RedirectIfAuthenticated.php

@@ -0,0 +1,27 @@
+<?php
+
+namespace App\Http\Middleware;
+
+use App\Providers\RouteServiceProvider;
+use Closure;
+use Illuminate\Support\Facades\Auth;
+
+class RedirectIfAuthenticated
+{
+    /**
+     * Handle an incoming request.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @param  \Closure  $next
+     * @param  string|null  $guard
+     * @return mixed
+     */
+    public function handle($request, Closure $next, $guard = null)
+    {
+        if (Auth::guard($guard)->check()) {
+            return redirect(RouteServiceProvider::HOME);
+        }
+
+        return $next($request);
+    }
+}

+ 18 - 0
app/Http/Middleware/TrimStrings.php

@@ -0,0 +1,18 @@
+<?php
+
+namespace App\Http\Middleware;
+
+use Illuminate\Foundation\Http\Middleware\TrimStrings as Middleware;
+
+class TrimStrings extends Middleware
+{
+    /**
+     * The names of the attributes that should not be trimmed.
+     *
+     * @var array
+     */
+    protected $except = [
+        'password',
+        'password_confirmation',
+    ];
+}

+ 23 - 0
app/Http/Middleware/TrustProxies.php

@@ -0,0 +1,23 @@
+<?php
+
+namespace App\Http\Middleware;
+
+use Fideloper\Proxy\TrustProxies as Middleware;
+use Illuminate\Http\Request;
+
+class TrustProxies extends Middleware
+{
+    /**
+     * The trusted proxies for this application.
+     *
+     * @var array|string
+     */
+    protected $proxies;
+
+    /**
+     * The headers that should be used to detect proxies.
+     *
+     * @var int
+     */
+    protected $headers = Request::HEADER_X_FORWARDED_ALL;
+}

+ 36 - 0
app/Http/Middleware/VerifyCsrfToken.php

@@ -0,0 +1,36 @@
+<?php
+
+namespace App\Http\Middleware;
+
+use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;
+
+class VerifyCsrfToken extends Middleware
+{
+    /**
+     * The URIs that should be excluded from CSRF verification.
+     *
+     * @var array
+     */
+    protected $except = [
+        //上传图片
+        'api/system/imgupload/',
+
+        //上传文件
+        'api/system/fileupload/',
+
+        //上传项目文件
+        'api/project/files/upload/',
+
+        //上传聊天文件
+        'api/chat/files/upload/',
+
+        //修改项目任务
+        'api/project/task/edit/',
+
+        //汇报提交
+        'api/report/template/',
+
+        //保存文档
+        'api/docs/section/save/',
+    ];
+}

+ 29 - 0
app/Jobs/Timer/SystemCronJob.php

@@ -0,0 +1,29 @@
+<?php
+
+namespace App\Jobs\Timer;
+
+use App\Tasks\AutoArchivedTask;
+use Hhxsv5\LaravelS\Swoole\Task\Task;
+use Hhxsv5\LaravelS\Swoole\Timer\CronJob;
+
+class SystemCronJob extends CronJob
+{
+    protected $i = 0;
+
+    public function interval()
+    {
+        return 60000;    // 每60秒运行一次
+    }
+
+    public function isImmediate()
+    {
+        return true;    // 是否立即执行第一次,false则等待间隔时间后执行第一次
+    }
+
+    public function run()
+    {
+        $this->i++;
+        $autoArchivedTask = new AutoArchivedTask();
+        Task::deliver($autoArchivedTask);
+    }
+}

+ 526 - 0
app/Model/DBCache.php

@@ -0,0 +1,526 @@
+<?php
+
+namespace App\Model;
+
+use Cache;
+use Closure;
+use DateTime;
+use Illuminate\Pagination\Paginator;
+
+/**
+ * 数据库模型 缓存读取
+ *
+ * Class DBCache
+ * @package App\Model
+ */
+class DBCache
+{
+    protected $table = null;
+
+    protected $attributes = [];
+
+    protected $builder = null;
+
+    private $__cache = true;               //启用缓存(默认启用)
+    private $__cacheKeyname = null;        //缓存名称(默认自动生成)
+    private $__cacheMinutes = 1;           //缓存时间(分钟, 默认1分钟)
+    private $__removeCache = false;        //删除缓存
+    private $__join = [];
+    private $__take = [];
+    private $__where = [];
+    private $__whereRaw = [];
+    private $__whereIn = [];
+    private $__select = [];
+    private $__selectRaw = [];
+    private $__orderBy = [];
+    private $__orderByRaw = [];
+    private $__inRandomOrder = false;
+
+    public function __construct($tabel = null, array $attributes = [])
+    {
+        $this->initParameter();
+        if ($tabel) {
+            $this->table = $tabel;
+        }
+        if ($attributes) {
+            $this->attributes = $attributes;
+        }
+    }
+
+    public function setTable($table)
+    {
+        $this->table = $table;
+        return $this;
+    }
+
+    public function setAttributes($attributes)
+    {
+        $this->attributes = $attributes;
+        return $this;
+    }
+
+    /**
+     * 是否缓存
+     *
+     * @param bool $isCache
+     * @return $this
+     */
+    public function cache($isCache = true)
+    {
+        $this->__cache = $isCache ? true : false;
+        return $this;
+    }
+
+    public function noCache()
+    {
+        $this->__cache = false;
+        return $this;
+    }
+
+    /**
+     * 缓存名称
+     *
+     * @param $name
+     * @return $this
+     */
+    public function cacheKeyname($name)
+    {
+        $this->__cacheKeyname = $name ?: null;
+        return $this;
+    }
+
+    /**
+     * 缓存时间(分钟)
+     *
+     * @param $Minutes
+     * @return $this
+     */
+    public function cacheMinutes($Minutes)
+    {
+        if ($Minutes === 'year') {
+            $Minutes = 365 * 24 * 60;
+        }elseif ($Minutes === 'month') {
+            $Minutes = 30 * 24 * 60;
+        }elseif ($Minutes === 'day') {
+            $Minutes = 24 * 60;
+        }elseif ($Minutes === 'hour') {
+            $Minutes = 60;
+        }
+        if ($Minutes instanceof DateTime) {
+            $this->__cacheMinutes = $Minutes;
+        }else{
+            $this->__cacheMinutes = max($Minutes, 1);
+        }
+        return $this;
+    }
+
+    /**
+     * 删除缓存
+     */
+    public function removeCache()
+    {
+        $this->__removeCache = true;
+        return $this;
+    }
+    public function forgetCache($cacheKey)
+    {
+        $this->__removeCache = false;
+        return Cache::forget($cacheKey);
+    }
+
+    /* ******************************************************************************** */
+    /* ******************************************************************************** */
+
+    public function join($table, $first, $operator = null, $second = null, $type = 'inner', $where = false)
+    {
+        $this->__join[] = [
+            $table,
+            $first,
+            $operator,
+            $second,
+            $type,
+            $where
+        ];
+        return $this;
+    }
+
+    public function take($var)
+    {
+        $this->__take[] = $var;
+        return $this;
+    }
+
+    public function where($column, $operator = null, $value = null, $boolean = 'and')
+    {
+        $this->__where[] = [
+            $column,
+            $operator,
+            $value,
+            $boolean
+        ];
+        return $this;
+    }
+
+    public function whereRaw($sql, $bindings = [], $boolean = 'and')
+    {
+        if ($sql === null) {
+            return $this;
+        }
+        $this->__whereRaw[] = [
+            $sql,
+            $bindings,
+            $boolean
+        ];
+        return $this;
+    }
+
+    public function whereIn($column, $values, $boolean = 'and', $not = false)
+    {
+        $this->__whereIn[] = [
+            $column,
+            $values,
+            $boolean,
+            $not
+        ];
+        return $this;
+    }
+    public function whereNotIn($column, $values, $boolean = 'and')
+    {
+        return $this->whereIn($column, $values, $boolean, true);
+    }
+
+    public function select($var = ['*'])
+    {
+        $this->__select[] = is_array($var) ? $var : func_get_args();
+        return $this;
+    }
+
+    public function selectRaw($expression, array $bindings = [])
+    {
+        $this->__selectRaw[] = [
+            $expression,
+            $bindings
+        ];
+        return $this;
+    }
+
+    public function orderBy($column, $direction = 'asc')
+    {
+        $this->__orderBy[] = [
+            $column,
+            $direction
+        ];
+        return $this;
+    }
+    public function orderByDesc($column)
+    {
+        return $this->orderBy($column, 'desc');
+    }
+
+    public function orderByRaw($sql, $bindings = [])
+    {
+        $this->__orderByRaw[] = [
+            $sql,
+            $bindings
+        ];
+        return $this;
+    }
+
+    public function inRandomOrder($isRand = true)
+    {
+        $this->__inRandomOrder = $isRand?true:false;
+        return $this;
+    }
+
+    /* ******************************************************************************** */
+    /* ******************************************************************************** */
+    /* ******************************************************************************** */
+
+    /**
+     * 初始化参数
+     */
+    private function initParameter() {
+        $this->__cache = true;
+        $this->__cacheKeyname = null;
+        $this->__cacheMinutes = 1;
+        $this->__removeCache = false;
+        //
+        $this->__join = [];
+        $this->__take = [];
+        $this->__where = [];
+        $this->__whereRaw = [];
+        $this->__whereIn = [];
+        $this->__select = [];
+        $this->__selectRaw = [];
+        $this->__orderBy = [];
+        $this->__orderByRaw = [];
+        $this->__inRandomOrder = false;
+    }
+
+    /**
+     * 获取缓存标识
+     * @param string $type      查询方式
+     * @param string $attach    附加标识
+     * @return string
+     */
+    private function identify($type, $attach = '') {
+        if ($this->__cacheKeyname) {
+            return $this->__cacheKeyname;
+        }
+        //
+        $identify = $this->addEncode($this->attributes);
+        $identify.= $this->addEncode($this->__join);
+        $identify.= $this->addEncode($this->__take);
+        $identify.= $this->addEncode($this->__where);
+        $identify.= $this->addEncode($this->__whereRaw);
+        $identify.= $this->addEncode($this->__whereIn);
+        $identify.= $this->addEncode($this->__select);
+        $identify.= $this->addEncode($this->__selectRaw);
+        $identify.= $this->addEncode($this->__orderBy);
+        $identify.= $this->addEncode($this->__orderByRaw);
+        $identify.= $this->addEncode($this->__inRandomOrder);
+        $identify.= $this->addEncode($attach);
+        //
+        return 'DBCache:' . $this->table . ':' . $type . '::' . md5($identify);
+    }
+
+    private function addEncode($value)
+    {
+        $encodeName = '';
+        if (is_array($value)) {
+            sort($value);
+            foreach ($value AS $key => $val) {
+                $encodeName .= ':' . $key . '=' . $this->addEncode($val) . ';';
+            }
+        } elseif ($value instanceof Closure) {
+            $join = new DBClause();
+            call_user_func($value, $join);
+            $encodeName .= ':' . $join->toString() . ';';
+        } elseif (is_string($value) || is_numeric($value)) {
+            $encodeName .= ':' . $value . ';';
+        } else {
+            $encodeName .= ':' . var_export($value, true) . ';';
+        }
+        return $encodeName;
+    }
+
+    /**
+     * 创建对象
+     * @return $this|DOModel|null
+     */
+    private function builder() {
+        if ($this->builder === null) {
+            $this->builder = new DOModel($this->table, $this->attributes);
+        }
+        $builder = $this->builder;
+        //
+        foreach ($this->__join AS $item) {
+            $builder = $builder->join($item[0], $item[1], $item[2], $item[3], $item[4], $item[5]);
+        }
+        foreach ($this->__take AS $item) {
+            $builder = $builder->take($item);
+        }
+        foreach ($this->__where AS $item) {
+            $builder = $builder->where($item[0], $item[1], $item[2], $item[3]);
+        }
+        foreach ($this->__whereRaw AS $item) {
+            $builder = $builder->whereRaw($item[0], $item[1], $item[2]);
+        }
+        foreach ($this->__whereIn AS $item) {
+            $builder = $builder->whereIn($item[0], $item[1], $item[2], $item[3]);
+        }
+        foreach ($this->__select AS $item) {
+            $builder = $builder->select($item);
+        }
+        foreach ($this->__selectRaw AS $item) {
+            $builder = $builder->selectRaw($item[0], $item[1]);
+        }
+        foreach ($this->__orderBy AS $item) {
+            $builder = $builder->orderBy($item[0], $item[1]);
+        }
+        foreach ($this->__orderByRaw AS $item) {
+            $builder = $builder->orderByRaw($item[0], $item[1]);
+        }
+        if ($this->__inRandomOrder) {
+            $builder = $builder->inRandomOrder();
+        }
+        //
+        $this->initParameter();
+        return $builder;
+    }
+
+    /* ******************************************************************************** */
+    /* ******************************************************************************** */
+    /* ******************************************************************************** */
+
+    /**
+     * 静态方法
+     *
+     * @param string $tabel
+     * @return DBCache
+     */
+    public static function table($tabel = '')
+    {
+        return new DBCache($tabel);
+    }
+
+    /**
+     * 获取一条数据
+     *
+     * @param array $columns
+     * @return mixed
+     */
+    public function first($columns = ['*'])
+    {
+        if ($this->__cache) {
+            $cacheKey = $this->identify('first', $columns);
+            if ($this->__removeCache) {
+                return $this->forgetCache($cacheKey);
+            }
+            $result = Cache::remember($cacheKey, $this->__cacheMinutes, function() use ($columns) {
+                return $this->builder()->first($columns);
+            });
+        }else{
+            $result = $this->builder()->first($columns);
+        }
+        return isset($result->original)?$result->original:null;
+    }
+
+    /**
+     * 获取所有数据
+     *
+     * @param array $columns
+     * @return array|bool
+     */
+    public function get($columns = ['*'])
+    {
+        if ($this->__cache) {
+            $cacheKey = $this->identify('get', $columns);
+            if ($this->__removeCache) {
+                return $this->forgetCache($cacheKey);
+            }
+            $result = Cache::remember($cacheKey, $this->__cacheMinutes, function() use ($columns) {
+                return $this->builder()->get($columns);
+            });
+        }else{
+            $result = $this->builder()->get($columns);
+        }
+        //
+        $array = [];
+        foreach ($result AS $key=>$item) {
+            $array[$key] = isset($item->original)?$item->original:null;
+        }
+        return $array;
+    }
+
+    /**
+     * 查询结果的计数
+     *
+     * @param string $columns
+     * @return int|bool
+     */
+    public function count($columns = '*')
+    {
+        if ($this->__cache) {
+            $cacheKey = $this->identify('count', $columns);
+            if ($this->__removeCache) {
+                return $this->forgetCache($cacheKey);
+            }
+            $result = Cache::remember($cacheKey, $this->__cacheMinutes, function() use ($columns) {
+                return $this->builder()->count($columns);
+            });
+        }else{
+            $result = $this->builder()->count($columns);
+        }
+        return intval($result);
+    }
+
+    /**
+     * 从查询的第一个结果中获得一个列的值
+     *
+     * @param $column
+     * @return mixed
+     */
+    public function value($column)
+    {
+        if ($this->__cache) {
+            $cacheKey = $this->identify('value', $column);
+            if ($this->__removeCache) {
+                return $this->forgetCache($cacheKey);
+            }
+            $result = Cache::remember($cacheKey, $this->__cacheMinutes, function() use ($column) {
+                $value = $this->builder()->value($column);
+                return $value ? $value : '';
+            });
+        }else{
+            $result = $this->builder()->value($column);
+        }
+        return $result;
+    }
+
+    /**
+     * 统计输出
+     *
+     * @param $column
+     * @return bool|mixed
+     */
+    public function sum($column)
+    {
+        if ($this->__cache) {
+            $cacheKey = $this->identify('sum', $column);
+            if ($this->__removeCache) {
+                return $this->forgetCache($cacheKey);
+            }
+            $result = Cache::remember($cacheKey, $this->__cacheMinutes, function() use ($column) {
+                $sum = $this->builder()->sum($column);
+                return $sum ? $sum : 0;
+            });
+        }else{
+            $result = $this->builder()->sum($column);
+        }
+        return $result;
+    }
+
+    /**
+     * 获取分页数据
+     * @param null $perPage
+     * @param array $columns
+     * @param string $pageName
+     * @param null $page
+     * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator|mixed
+     */
+    public function paginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null)
+    {
+        if ($this->__cache) {
+            $attach = $this->addEncode($perPage);
+            $attach.= $this->addEncode($columns);
+            $attach.= $this->addEncode($pageName);
+            $attach.= $this->addEncode(Paginator::resolveCurrentPage($pageName));
+            $cacheKey = $this->identify('paginate', $attach);
+            if ($this->__removeCache) {
+                return $this->forgetCache($cacheKey);
+            }
+            $result = Cache::remember($cacheKey, $this->__cacheMinutes, function() use ($perPage, $columns, $pageName, $page) {
+                return $this->builder()->paginate($perPage, $columns, $pageName, $page);
+            });
+        }else{
+            $result = $this->builder()->paginate($perPage, $columns, $pageName, $page);
+        }
+        $array = [];
+        foreach ($result AS $key=>$item) {
+            $array[$key] = isset($item->original)?$item->original:null;
+        }
+        return [
+            "currentPage" => $result->currentPage(),
+            "firstItem" => $result->firstItem(),
+            "hasMorePages" => $result->hasMorePages(),
+            "lastItem" => $result->lastItem(),
+            "lastPage" => $result->lastPage(),
+            "nextPageUrl" => $result->nextPageUrl(),
+            "previousPageUrl" => $result->previousPageUrl(),
+            "perPage" => $result->perPage(),
+            "total" => $result->total(),
+            "lists" => $array,
+        ];
+    }
+}

+ 55 - 0
app/Model/DBClause.php

@@ -0,0 +1,55 @@
+<?php
+
+namespace App\Model;
+
+
+use Closure;
+
+class DBClause
+{
+    private $identify;
+
+    public function __construct()
+    {
+        $this->identify = "";
+    }
+
+    public function on($first, $operator = null, $second = null, $boolean = 'and')
+    {
+        $identify = $this->addEncode($first);
+        $identify.= $this->addEncode($operator);
+        $identify.= $this->addEncode($second);
+        $identify.= $this->addEncode($boolean);
+        $this->identify.= $identify;
+    }
+
+    public function orOn($first, $operator = null, $second = null)
+    {
+        $this->on($first, $operator, $second, 'or');
+    }
+
+    public function toString()
+    {
+        return $this->identify;
+    }
+
+    private function addEncode($value)
+    {
+        $encodeName = '';
+        if (is_array($value)) {
+            ksort($value);
+            foreach ($value AS $key => $val) {
+                $encodeName .= ':' . $key . '=' . $this->addEncode($val) . ';';
+            }
+        } elseif ($value instanceof Closure) {
+            $join = new DBClause();
+            call_user_func($value, $join);
+            $encodeName .= ':' . $join->toString() . ';';
+        } elseif (is_string($value) || is_numeric($value)) {
+            $encodeName .= ':' . $value . ';';
+        } else {
+            $encodeName .= ':' . var_export($value, true) . ';';
+        }
+        return $encodeName;
+    }
+}

+ 32 - 0
app/Model/DOModel.php

@@ -0,0 +1,32 @@
+<?php
+
+namespace App\Model;
+
+use Illuminate\Database\Eloquent\Model;
+
+/**
+ * Class Model
+ * @package App\Model
+ */
+class DOModel extends Model
+{
+
+    public $original = [];
+
+    public $timestamps = false;
+
+    public function __construct($tabel = null, array $attributes = [])
+    {
+        parent::__construct($attributes);
+        //
+        if (is_string($tabel)) {
+            $this->setTable($tabel);
+        }
+    }
+
+    public function setTimestamps($timestamps)
+    {
+        $this->timestamps = $timestamps;
+    }
+
+}

File diff suppressed because it is too large
+ 2556 - 0
app/Module/Base.php


+ 144 - 0
app/Module/BillExport.php

@@ -0,0 +1,144 @@
+<?php
+
+namespace App\Module;
+
+use Excel;
+use Maatwebsite\Excel\Concerns\FromCollection;
+use Maatwebsite\Excel\Concerns\WithEvents;
+use Maatwebsite\Excel\Concerns\WithHeadings;
+use Maatwebsite\Excel\Concerns\WithStrictNullComparison;
+use Maatwebsite\Excel\Concerns\WithTitle;
+use Maatwebsite\Excel\Events\AfterSheet;
+use PhpOffice\PhpSpreadsheet\Cell\DataValidation;
+use PhpOffice\PhpSpreadsheet\Writer\Exception;
+
+class BillExport implements WithHeadings, WithEvents, FromCollection, WithTitle, WithStrictNullComparison
+{
+    public $title;
+    public $headings = [];
+    public $data = [];
+    public $typeLists = [];
+    public $typeNumber = 0;
+
+    public function __construct($title, array $data)
+    {
+        $this->title = $title;
+        $this->data = $data;
+    }
+
+    public static function create($data = [], $title = "Sheet1") {
+        if (is_string($data)) {
+            list($title, $data) = [$data, $title];
+        }
+        if (!is_array($data)) {
+            $data = [];
+        }
+        return new BillExport($title, $data);
+    }
+
+    public function setTitle($title) {
+        $this->title = $title;
+        return $this;
+    }
+
+    public function setHeadings(array $headings) {
+        $this->headings = $headings;
+        return $this;
+    }
+
+    public function setData(array $data) {
+        $this->data = $data;
+        return $this;
+    }
+
+    public function setTypeList(array $typeList, $number = 0) {
+        $this->typeLists = $typeList;
+        $this->typeNumber = $number;
+        return $this;
+    }
+
+    public function store($fileName = '') {
+        if (empty($fileName)) {
+            $fileName = date("YmdHis") . '.xls';
+        }
+        try {
+            return Excel::store($this, $fileName);
+        } catch (Exception $e) {
+            return "导出错误:" . $e->getMessage();
+        } catch (\PhpOffice\PhpSpreadsheet\Exception $e) {
+            return "导出错误:" . $e->getMessage();
+        }
+    }
+
+    public function download($fileName = '') {
+        if (empty($fileName)) {
+            $fileName = date("YmdHis") . '.xls';
+        }
+        try {
+            return Excel::download($this, $fileName);
+        } catch (Exception $e) {
+            return "导出错误:" . $e->getMessage();
+        } catch (\PhpOffice\PhpSpreadsheet\Exception $e) {
+            return "导出错误:" . $e->getMessage();
+        }
+    }
+
+    /**
+     * 导出的文件标题
+     * @return string
+     */
+    public function title(): string
+    {
+        return $this->title;
+    }
+
+    /**
+     * 标题行
+     * @return array
+     */
+    public function headings(): array
+    {
+        return $this->headings;
+    }
+
+    /**
+     * 导出的内容
+     * @return \Illuminate\Support\Collection
+     */
+    public function collection()
+    {
+        return collect($this->data);
+    }
+
+    /**
+     * 设置单元格事件
+     * @return array
+     */
+    public function registerEvents(): array
+    {
+        return [
+            AfterSheet::Class => function (AfterSheet $event) {
+                $count = count($this->data);
+                foreach ($this->typeLists AS $cell => $typeList) {
+                    if ($cell && $typeList) {
+                        $p = $this->headings ? 1 : 0;
+                        for ($i = 1 + $p; $i <= max($count, $this->typeNumber) + $p; $i++) {
+                            $validation = $event->sheet->getDelegate()->getCell($cell . $i)->getDataValidation();
+                            $validation->setType(DataValidation::TYPE_LIST);
+                            $validation->setErrorStyle(DataValidation::STYLE_WARNING);
+                            $validation->setAllowBlank(false);
+                            $validation->setShowDropDown(true);
+                            $validation->setShowInputMessage(true);
+                            $validation->setShowErrorMessage(true);
+                            $validation->setErrorTitle('输入的值不合法');
+                            $validation->setError('选择的值不在列表中,请选择列表中的值');
+                            $validation->setPromptTitle('从列表中选择');
+                            $validation->setPrompt('请选择下拉列表中的值');
+                            $validation->setFormula1('"' . implode(',', $typeList) . '"');
+                        }
+                    }
+                }
+            }
+        ];
+    }
+}

+ 16 - 0
app/Module/BillImport.php

@@ -0,0 +1,16 @@
+<?php
+
+
+namespace App\Module;
+
+
+use Maatwebsite\Excel\Concerns\ToArray;
+
+class BillImport implements ToArray
+{
+    public function Array(Array $tables)
+    {
+        return $tables;
+    }
+
+}

+ 245 - 0
app/Module/Chat.php

@@ -0,0 +1,245 @@
+<?php
+
+namespace App\Module;
+
+use Cache;
+use DB;
+
+/**
+ * Class Chat
+ * @package App\Module
+ */
+class Chat
+{
+    /**
+     * 打开对话(创建对话)
+     * @param string $username      发送者用户名
+     * @param string $receive       接受者用户名
+     * @param bool $forceRefresh    是否强制刷新缓存
+     * @return mixed
+     */
+    public static function openDialog($username, $receive, $forceRefresh = false)
+    {
+        if (!$username || !$receive) {
+            return Base::retError('参数错误!');
+        }
+        $cacheKey = $username . "@" . $receive;
+        if ($forceRefresh === true) {
+            Cache::forget($cacheKey);
+        }
+        $result = Cache::remember($cacheKey, now()->addMinutes(10), function() use ($receive, $username) {
+            $row = Base::DBC2A(DB::table('chat_dialog')->where([
+                'user1' => $username,
+                'user2' => $receive,
+            ])->first());
+            if ($row) {
+                $row['recField'] = 2;   //接受者的字段位置
+                return Base::retSuccess('already1', $row);
+            }
+            $row = Base::DBC2A(DB::table('chat_dialog')->where([
+                'user1' => $receive,
+                'user2' => $username,
+            ])->first());
+            if ($row) {
+                $row['recField'] = 1;
+                return Base::retSuccess('already2', $row);
+            }
+            //
+            DB::table('chat_dialog')->insert([
+                'user1' => $username,
+                'user2' => $receive,
+                'indate' => Base::time()
+            ]);
+            $row = Base::DBC2A(DB::table('chat_dialog')->where([
+                'user1' => $username,
+                'user2' => $receive,
+            ])->first());
+            if ($row) {
+                $row['recField'] = 2;
+                return Base::retSuccess('success', $row);
+            }
+            //
+            return Base::retError('系统繁忙,请稍后再试!');
+        });
+        if (Base::isError($result)) {
+            Cache::forget($cacheKey);
+        }
+        return $result;
+    }
+
+    /**
+     * 删除对话缓存
+     * @param $username
+     * @param $receive
+     * @return bool
+     */
+    public static function forgetDialog($username, $receive)
+    {
+        if (!$username || !$receive) {
+            return false;
+        }
+        Cache::forget($username . "@" . $receive);
+        Cache::forget($receive . "@" . $username);
+        return true;
+    }
+
+    /**
+     * 保存对话消息
+     * @param string $username      发送者用户名
+     * @param string $receive       接受者用户名
+     * @param array $message
+     * @return mixed
+     */
+    public static function saveMessage($username, $receive, $message)
+    {
+        $dialog = self::openDialog($username, $receive);
+        if (Base::isError($dialog)) {
+            return $dialog;
+        } else {
+            $dialog = $dialog['data'];
+        }
+        //
+        $indate = abs($message['indate'] - time()) > 60 ? time() : $message['indate'];
+        if (isset($message['id'])) unset($message['id']);
+        if (isset($message['username'])) unset($message['username']);
+        if (isset($message['userimg'])) unset($message['userimg']);
+        if (isset($message['indate'])) unset($message['indate']);
+        if (isset($message['replaceId'])) unset($message['replaceId']);
+        $inArray = [
+            'did' => $dialog['id'],
+            'username' => $username,
+            'receive' => $receive,
+            'message' => Base::array2string($message),
+            'indate' => $indate
+        ];
+        //
+        if (mb_strlen($message['text']) > 20000) {
+            return Base::retError("发送内容长度已超出最大限制!");
+        }
+        $field = ($dialog['recField'] == 1 ? 'unread1' : 'unread2');
+        $unread = intval(DB::table('chat_dialog')->where('id', $dialog['id'])->value($field));
+        $lastText = self::messageDesc($message);
+        if ($lastText) {
+            $upArray = [];
+            if ($username != $receive) {
+                $upArray = Base::DBUP([
+                    $field => 1,
+                ]);
+                $unread += 1;
+            }
+            $upArray['lasttext'] = mb_substr($lastText, 0, 100);
+            $upArray['lastdate'] = $indate;
+            if ($dialog['del1']) {
+                $upArray['del1'] = 0;
+            }
+            if ($dialog['del2']) {
+                $upArray['del2'] = 0;
+            }
+            DB::table('chat_dialog')->where('id', $dialog['id'])->update($upArray);
+            if ($dialog['del1'] || $dialog['del2']) {
+                Chat::forgetDialog($dialog['user1'], $dialog['user2']);
+            }
+        }
+        $inArray['id'] = DB::table('chat_msg')->insertGetId($inArray);
+        $inArray['message'] = $message;
+        $inArray['unread'] = $unread;
+        //
+        return Base::retSuccess('success', $inArray);
+    }
+
+    /**
+     * 格式化信息(来自接收)
+     * @param $data
+     * @return array
+     */
+    public static function formatMsgReceive($data) {
+        return self::formatMsgData(Base::json2array($data));
+    }
+
+    /**
+     * 格式化信息(用于发送)
+     * @param $array
+     * @return string
+     */
+    public static function formatMsgSend($array) {
+        return Base::array2json(self::formatMsgData($array));
+    }
+
+    /**
+     * 格式化信息
+     * @param array $array
+     * @return array
+     */
+    public static function formatMsgData($array = []) {
+        if (!is_array($array)) {
+            $array = [];
+        }
+        //messageType来自客户端(前端->后端):refresh/unread/read/roger/user/info/team/docs/appActivity
+        //messageType来自服务端(后端->前端):error/open/kick/user/back/unread/docs
+        if (!isset($array['messageType'])) $array['messageType'] = '';  //消息类型
+        if (!isset($array['messageId'])) $array['messageId'] = '';      //消息ID(用于back给客户端)
+        if (!isset($array['contentId'])) $array['contentId'] = 0;       //消息数据ID(用于roger给服务端)
+        if (!isset($array['channel'])) $array['channel'] = '';          //渠道(用于多端登录)
+        if (!isset($array['username'])) $array['username'] = '';        //发送者
+        if (!isset($array['target'])) $array['target'] = null;          //接受者
+        if (!isset($array['body'])) $array['body'] = [];                //正文内容
+        if (!isset($array['time'])) $array['time'] = time();            //时间
+        //
+        $array['contentId'] = intval($array['contentId']);
+        if (!is_array($array['body']) || empty($array['body'])) $array['body'] = ['_' => time()];
+        return $array;
+    }
+
+    /**
+     * 获取跟任务有关系的(在线)用户(关注的、在项目里的、负责人、创建者)
+     * @param $taskId
+     * @return array
+     */
+    public static function getTaskUsers($taskId)
+    {
+        $tmpLists = Project::taskSomeUsers($taskId);
+        if (empty($tmpLists)) {
+            return [];
+        }
+        //
+        return Base::DBC2A(DB::table('ws')->select(['fd', 'username', 'channel'])->where([
+            ['update', '>', time() - 600],
+        ])->whereIn('username', array_values(array_unique($tmpLists)))->get());
+    }
+
+    /**
+     * 获取消息简述
+     * @param array $message
+     * @return mixed|string
+     */
+    public static function messageDesc($message)
+    {
+        switch ($message['type']) {
+            case 'text':
+                $lastText = $message['text'];
+                break;
+            case 'image':
+                $lastText = '[图片]';
+                break;
+            case 'file':
+                $lastText = '[文件]';
+                break;
+            case 'taskB':
+                $lastText = $message['text'] . " [来自关注任务]";
+                break;
+            case 'report':
+                $lastText = $message['text'] . " [来自工作报告]";
+                break;
+            case 'video':
+                $lastText = '[视频通话]';
+                break;
+            case 'voice':
+                $lastText = '[语音通话]';
+                break;
+            default:
+                $lastText = '[未知类型]';
+                break;
+        }
+        return $lastText;
+    }
+}

+ 105 - 0
app/Module/Docs.php

@@ -0,0 +1,105 @@
+<?php
+
+namespace App\Module;
+
+use App\Tasks\PushTask;
+use Cache;
+use DB;
+use Hhxsv5\LaravelS\Swoole\Task\Task;
+
+/**
+ * Class Docs
+ * @package App\Module
+ */
+class Docs
+{
+    /**
+     * 检验是否有阅读或修改权限
+     * @param $bookid
+     * @param string $checkType     edit|view
+     * @return array|mixed
+     */
+    public static function checkRole($bookid, $checkType = 'edit')
+    {
+        $row = Base::DBC2A(DB::table('docs_book')->where('id', $bookid)->first());
+        if (empty($row)) {
+            return Base::retError('知识库不存在或已被删除!', -1000);
+        }
+        $userE = Users::authE();
+        if (Base::isError($userE)) {
+            $user = [];
+        } else {
+            $user = $userE['data'];
+        }
+        $checkType = $checkType == 'edit' ? 'edit' : 'view';
+        if ($checkType == 'edit') {
+            if (empty($user)) {
+                return $userE;
+            }
+        } else {
+            if ($row['role_view'] != 'all') {
+                if (empty($user)) {
+                    return Base::retError('知识库仅对会员开放,请登录后再试!', -1001);
+                }
+            }
+        }
+        if ($user['username'] == $row['username']) {
+            return Base::retSuccess('success');
+        }
+        //
+        if ($row['role_' . $checkType] == 'member') {
+            if (!DB::table('docs_users')->where('bookid', $bookid)->where('username', $user['username'])->exists()) {
+                return Base::retError('知识库仅对成员开放!', $checkType == 'edit' && $row['role_look'] == 'reg' ? 1002 : -1002);
+            }
+        } elseif ($row['role_' . $checkType] == 'private') {
+            if ($row['username'] != $user['username']) {
+                return Base::retError('知识库仅对作者开放!', $checkType == 'edit' && $row['role_look'] == 'reg' ? 1003 : -1003);
+            }
+        }
+        //
+        return Base::retSuccess('success');
+    }
+
+    /**
+     * 通知正在编辑的成员
+     *
+     * @param integer $sid      章节ID
+     * @param array $bodyArray  body参数
+     */
+    public static function notice($sid, $bodyArray = [])
+    {
+        $user = Users::auth();
+        $array = Base::json2array(Cache::get("docs::" . $sid));
+        if ($array) {
+            foreach ($array as $uname => $vbody) {
+                if (intval($vbody['indate']) + 20 < time()) {
+                    unset($array[$uname]);
+                }
+            }
+        }
+        $pushLists = [];
+        if ($array) {
+            foreach ($array AS $tuser) {
+                $uLists = Base::DBC2A(DB::table('ws')->select(['fd', 'username', 'channel'])->where('username', $tuser['username'])->get());
+                foreach ($uLists AS $item) {
+                    if ($item['username'] == $user['username']) {
+                        continue;
+                    }
+                    $pushLists[] = [
+                        'fd' => $item['fd'],
+                        'msg' => [
+                            'messageType' => 'docs',
+                            'body' => array_merge([
+                                'sid' => $sid,
+                                'nickname' => $user['nickname'] ?: $user['username'],
+                                'time' => time(),
+                            ], $bodyArray)
+                        ]
+                    ];
+                }
+            }
+        }
+        $pushTask = new PushTask($pushLists);
+        Task::deliver($pushTask);
+    }
+}

+ 235 - 0
app/Module/Ihttp.php

@@ -0,0 +1,235 @@
+<?php
+
+namespace App\Module;
+
+use Exception;
+
+@error_reporting(E_ALL & ~E_NOTICE);
+
+class Ihttp
+{
+
+    public static function ihttp_request($url, $post = [], $extra = [], $timeout = 60, $retRaw = false, $isGb2312 = false) {
+        $urlset = parse_url($url);
+        if(empty($urlset['path'])) {
+            $urlset['path'] = '/';
+        }
+        if(!empty($urlset['query'])) {
+            $urlset['query'] = "?{$urlset['query']}";
+        }
+        if(empty($urlset['port'])) {
+            $urlset['port'] = $urlset['scheme'] == 'https' ? '443' : '80';
+        }
+        if (Base::strExists($url, 'https://') && !extension_loaded('openssl')) {
+            if (!extension_loaded("openssl")) {
+                return Base::retError('请开启您PHP环境的openssl');
+            }
+        }
+        if(function_exists('curl_init') && function_exists('curl_exec')) {
+            $ch = curl_init();
+            curl_setopt($ch, CURLOPT_URL, $urlset['scheme']. '://' .$urlset['host'].($urlset['port'] == '80' ? '' : ':'.$urlset['port']).$urlset['path'].$urlset['query']);
+            curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
+            curl_setopt($ch, CURLOPT_HEADER, 1);
+            if($post) {
+                if (is_array($post)) {
+                    $filepost = false;
+                    foreach ($post as $name => $value) {
+                        if (is_string($value) && substr($value, 0, 1) == '@') {
+                            $filepost = true;
+                            break;
+                        }
+                    }
+                    if (!$filepost) {
+                        $post = http_build_query($post);
+                    }
+                }
+                curl_setopt($ch, CURLOPT_POST, 1);
+                curl_setopt($ch, CURLOPT_POSTFIELDS, $post);
+            }
+            curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $timeout);
+            curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
+            curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
+            curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
+            curl_setopt($ch, CURLOPT_SSLVERSION, 1);
+            if (defined('CURL_SSLVERSION_TLSv1')) {
+                curl_setopt($ch, CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1);
+            }
+            curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:9.0.1) Gecko/20100101 Firefox/9.0.1');
+            if (!empty($extra) && is_array($extra)) {
+                $headers = array();
+                foreach ($extra as $opt => $value) {
+                    if (Base::strExists($opt, 'CURLOPT_')) {
+                        curl_setopt($ch, constant($opt), $value);
+                    } elseif (is_numeric($opt)) {
+                        curl_setopt($ch, $opt, $value);
+                    } else {
+                        $headers[] = "{$opt}: {$value}";
+                    }
+                }
+                if(!empty($headers)) {
+                    curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
+                }
+            }
+            $data = curl_exec($ch);
+            //$status = curl_getinfo($ch);
+            $errno = curl_errno($ch);
+            $error = curl_error($ch);
+            curl_close($ch);
+            if($errno || empty($data)) {
+                return Base::retError($error);
+            } else {
+                if ($isGb2312) {
+                    try { $data = iconv('GB2312', 'UTF-8', $data); }catch (Exception $e) { }
+                }
+                $response = self::ihttp_response_parse($data);
+                if ($retRaw === true) {
+                    return Base::retSuccess($response['code'], $response);
+                }
+                return Base::retSuccess($response['code'], $response['content']);
+            }
+        }
+        $method = empty($post) ? 'GET' : 'POST';
+        $fdata = "{$method} {$urlset['path']}{$urlset['query']} HTTP/1.1\r\n";
+        $fdata .= "Host: {$urlset['host']}\r\n";
+        if(function_exists('gzdecode')) {
+            $fdata .= "Accept-Encoding: gzip, deflate\r\n";
+        }
+        $fdata .= "Connection: close\r\n";
+        if (!empty($extra) && is_array($extra)) {
+            foreach ($extra as $opt => $value) {
+                if (!Base::strExists($opt, 'CURLOPT_')) {
+                    $fdata .= "{$opt}: {$value}\r\n";
+                }
+            }
+        }
+        //$body = '';
+        if ($post) {
+            if (is_array($post)) {
+                $body = http_build_query($post);
+            } else {
+                $body = urlencode((string)$post);
+            }
+            $fdata .= 'Content-Length: ' . strlen($body) . "\r\n\r\n{$body}";
+        } else {
+            $fdata .= "\r\n";
+        }
+        if($urlset['scheme'] == 'https') {
+            $fp = fsockopen('ssl://' . $urlset['host'], $urlset['port'], $errno, $error);
+        } else {
+            $fp = fsockopen($urlset['host'], $urlset['port'], $errno, $error);
+        }
+        stream_set_blocking($fp, true);
+        stream_set_timeout($fp, $timeout);
+        if (!$fp) {
+            return Base::retError( $error);
+        } else {
+            fwrite($fp, $fdata);
+            $content = '';
+            while (!feof($fp))
+                $content .= fgets($fp, 512);
+            fclose($fp);
+            if ($isGb2312) {
+                try { $content = iconv('GB2312', 'UTF-8', $content); }catch (Exception $e) { }
+            }
+            $response = self::ihttp_response_parse($content, true);
+            if ($retRaw === true) {
+                return Base::retSuccess($response['code'], $response);
+            }
+            return Base::retSuccess($response['code'], $response['content']);
+        }
+    }
+
+    public static function ihttp_get($url) {
+        return self::ihttp_request($url);
+    }
+
+    public static function ihttp_post($url, $data, $timeout = 60) {
+        $headers = array('Content-Type' => 'application/x-www-form-urlencoded');
+        return self::ihttp_request($url, $data, $headers, $timeout);
+    }
+
+    public static function ihttp_proxy($url, $post = [], $timeout = 20, $extra = []) {
+        return self::ihttp_request($url, $post, $extra, $timeout);
+    }
+
+    private static function ihttp_response_parse($data, $chunked = false) {
+        $rlt = array();
+        $pos = strpos($data, "\r\n\r\n");
+        if (Base::strExists(substr($data, 0, $pos), "Proxy-agent:")) {
+            $data = substr($data, $pos + 4, strlen($data));
+            $pos = strpos($data, "\r\n\r\n");
+        }
+        $split1[0] = substr($data, 0, $pos);
+        $split1[1] = substr($data, $pos + 4, strlen($data));
+
+        $split2 = explode("\r\n", $split1[0], 2);
+        preg_match('/^(\S+) (\S+) (\S+)$/', $split2[0], $matches);
+        $rlt['code'] = $matches[2];
+        $rlt['status'] = $matches[3];
+        $rlt['responseline'] = $split2[0];
+        $header = explode("\r\n", $split2[1]);
+        $isgzip = false;
+        $ischunk = false;
+        foreach ($header as $v) {
+            $row = explode(':', $v);
+            $key = trim($row[0]);
+            $value = trim(substr($v, strlen($row[0]) + 1));
+            if (is_array($rlt['headers'][$key])) {
+                $rlt['headers'][$key][] = $value;
+            } elseif (!empty($rlt['headers'][$key])) {
+                $temp = $rlt['headers'][$key];
+                unset($rlt['headers'][$key]);
+                $rlt['headers'][$key][] = $temp;
+                $rlt['headers'][$key][] = $value;
+            } else {
+                $rlt['headers'][$key] = $value;
+            }
+            if(!$isgzip && strtolower($key) == 'content-encoding' && strtolower($value) == 'gzip') {
+                $isgzip = true;
+            }
+            if(!$ischunk && strtolower($key) == 'transfer-encoding' && strtolower($value) == 'chunked') {
+                $ischunk = true;
+            }
+        }
+        if($chunked && $ischunk) {
+            $rlt['content'] = self::ihttp_response_parse_unchunk($split1[1]);
+        } else {
+            $rlt['content'] = $split1[1];
+        }
+        if($isgzip && function_exists('gzdecode')) {
+            $rlt['content'] = gzdecode($rlt['content']);
+        }
+
+        $rlt['meta'] = $data;
+        if($rlt['code'] == '100') {
+            return self::ihttp_response_parse($rlt['content']);
+        }
+        return $rlt;
+    }
+
+    private static function ihttp_response_parse_unchunk($str = null) {
+        if(!is_string($str) or strlen($str) < 1) {
+            return false;
+        }
+        $eol = "\r\n";
+        $add = strlen($eol);
+        $tmp = $str;
+        $str = '';
+        do {
+            $tmp = ltrim($tmp);
+            $pos = strpos($tmp, $eol);
+            if($pos === false) {
+                return false;
+            }
+            $len = hexdec(substr($tmp, 0, $pos));
+            if(!is_numeric($len) or $len < 0) {
+                return false;
+            }
+            $str .= substr($tmp, ($pos + $add), $len);
+            $tmp  = substr($tmp, ($len + $pos + $add));
+            $check = trim($tmp);
+        } while(!empty($check));
+        unset($tmp);
+        return $str;
+    }
+}

+ 220 - 0
app/Module/Project.php

@@ -0,0 +1,220 @@
+<?php
+
+namespace App\Module;
+
+use DB;
+
+/**
+ * Class Project
+ * @package App\Module
+ */
+class Project
+{
+    /**
+     * 是否在项目里
+     * @param int $projectid
+     * @param string $username
+     * @param bool $isowner
+     * @return array
+     */
+    public static function inThe($projectid, $username, $isowner = false)
+    {
+        $whereArray = [
+            'type' => '成员',
+            'projectid' => $projectid,
+            'username' => $username,
+        ];
+        if ($isowner) {
+            $whereArray['isowner'] = 1;
+        }
+        $row = Base::DBC2A(DB::table('project_users')->select(['isowner', 'indate'])->where($whereArray)->first());
+        if (empty($row)) {
+            return Base::retError('你不在项目成员内!');
+        } else {
+            return Base::retSuccess('你在项目内', $row);
+        }
+    }
+
+    /**
+     * 更新项目(complete、unfinished)
+     * @param int $projectid
+     */
+    public static function updateNum($projectid)
+    {
+        if ($projectid > 0) {
+            DB::table('project_lists')->where('id', $projectid)->update([
+                'unfinished' => DB::table('project_task')->where('projectid', $projectid)->where('complete', 0)->where('delete', 0)->count(),
+                'complete' => DB::table('project_task')->where('projectid', $projectid)->where('complete', 1)->where('delete', 0)->count(),
+            ]);
+        }
+    }
+
+    /**
+     * 任务负责人组
+     * @param $task
+     * @return array
+     */
+    public static function taskPersons($task)
+    {
+        $array = [];
+        $array[] = Users::username2basic($task['username']);
+        $persons = [$task['username']];
+        $subtask = Base::string2array($task['subtask']);
+        foreach ($subtask AS $item) {
+            if ($item['uname'] && !in_array($item['uname'], $persons)) {
+                $persons[] = $item['uname'];
+                $basic = Users::username2basic($item['uname']);
+                if ($basic) {
+                    $array[] = $basic;
+                }
+            }
+        }
+        return $array;
+    }
+
+    /**
+     * 是否负责人(任务负责人、子任务负责人)
+     * @param $task
+     * @param $username
+     * @return bool
+     */
+    public static function isPersons($task, $username)
+    {
+        $persons = [$task['username']];
+        $subtask = Base::string2array($task['subtask']);
+        foreach ($subtask AS $item) {
+            if ($item['uname'] && !in_array($item['uname'], $persons)) {
+                $persons[] = $item['uname'];
+            }
+        }
+        return in_array($username, $persons) ? true : false;
+    }
+
+    /**
+     * 任务是否过期
+     * @param array $task
+     * @return int
+     */
+    public static function taskIsOverdue($task)
+    {
+        return $task['complete'] == 0 && $task['enddate'] > 0 && $task['enddate'] <= Base::time() ? 1 : 0;
+    }
+
+    /**
+     * 过期的排在前
+     * @param array $taskLists
+     * @return mixed
+     */
+    public static function sortTask($taskLists)
+    {
+        $inOrder = [];
+        foreach ($taskLists as $key => $oitem) {
+            $inOrder[$key] = $oitem['overdue'] ? -1 : $key;
+        }
+        array_multisort($inOrder, SORT_ASC, $taskLists);
+        return $taskLists;
+    }
+
+    /**
+     * 获取与任务有关系的用户(关注的、在项目里的、负责人、创建者)
+     * @param $taskId
+     * @return array
+     */
+    public static function taskSomeUsers($taskId)
+    {
+        $taskDeatil = Base::DBC2A(DB::table('project_task')->select(['follower', 'subtask', 'createuser', 'username', 'projectid'])->where('id', $taskId)->first());
+        if (empty($taskDeatil)) {
+            return [];
+        }
+        //关注的用户
+        $userArray = Base::string2array($taskDeatil['follower']);
+        //子任务负责人
+        $subtask = Base::string2array($taskDeatil['subtask']);
+        foreach ($subtask AS $item) {
+            $userArray[] = $item['uname'];
+        }
+        //创建者
+        $userArray[] = $taskDeatil['createuser'];
+        //负责人
+        $userArray[] = $taskDeatil['username'];
+        //在项目里的用户
+        if ($taskDeatil['projectid'] > 0) {
+            $tempLists = Base::DBC2A(DB::table('project_users')->select(['username'])->where(['projectid' => $taskDeatil['projectid'], 'type' => '成员' ])->get());
+            foreach ($tempLists AS $item) {
+                $userArray[] = $item['username'];
+            }
+        }
+        //
+        return array_values(array_filter(array_unique($userArray)));
+    }
+
+    /**
+     * 项目(任务)权限
+     * @param $type
+     * @param $projectid
+     * @param int $taskid
+     * @param string $username
+     * @return array|mixed
+     */
+    public static function role($type, $projectid, $taskid = 0, $username = '')
+    {
+        if (empty($username)) {
+            $user = Users::authE();
+            if (Base::isError($user)) {
+                return $user;
+            } else {
+                $user = $user['data'];
+            }
+            $username = $user['username'];
+        }
+        //
+        $project = Base::DBC2A(DB::table('project_lists')->select(['username', 'title', 'setting'])->where('id', $projectid)->where('delete', 0)->first());
+        if (empty($project)) {
+            return Base::retError('项目不存在或已被删除!');
+        }
+        // 项目负责人最高权限
+        if ($project['username'] == $username) {
+            unset($project['setting']);
+            return Base::retSuccess('success', $project);
+        }
+        //
+        $setting = Base::string2array($project['setting']);
+        foreach (['edit_role', 'complete_role', 'archived_role', 'del_role'] AS $key) {
+            $setting[$key] = is_array($setting[$key]) ? $setting[$key] : ['__', 'owner'];
+        }
+        $setting['add_role'] = is_array($setting['add_role']) ? $setting['add_role'] : ['__', 'member'];
+        //
+        $role = $setting[$type];
+        if (empty($role) || !is_array($role)) {
+            return Base::retError('操作权限不足!');
+        }
+        if (in_array('member', $role)) {
+            $inRes = Project::inThe($projectid, $username);
+            if (Base::isError($inRes)) {
+                return $inRes;
+            }
+        } elseif (in_array('owner', $role)) {
+            if (empty($taskid)) {
+                return Base::retError('任务不存在!');
+            }
+            $task = Base::DBC2A(DB::table('project_task')
+                ->select(['username'])
+                ->where([
+                    ['delete', '=', 0],
+                    ['id', '=', $taskid],
+                ])
+                ->first());
+            if (empty($task)) {
+                return Base::retError('任务不存在!');
+            }
+            if ($task['username'] != $username) {
+                return Base::retError('此操作只允许项目管理员或者任务负责人!');
+            }
+        } else {
+            return Base::retError('此操作仅限项目负责人!');
+        }
+        //
+        unset($project['setting']);
+        return Base::retSuccess('success', $project);
+    }
+}

+ 115 - 0
app/Module/Umeng.php

@@ -0,0 +1,115 @@
+<?php
+
+namespace App\Module;
+
+/**
+ * Class Umeng
+ * @package App\Module
+ */
+class Umeng
+{
+
+    /**
+     * 推送通知
+     * @param string $platform     ios|android
+     * @param string $token        umeng token
+     * @param string $title
+     * @param string $desc
+     * @param array $extra
+     * @return array
+     */
+    public static function notification($platform, $token, $title, $desc, $extra = [])
+    {
+        if ($platform == 'ios') {
+            $body = [
+                'appkey' => env('UMENG_PUSH_IOS_APPKEY'),
+                'timestamp' => Base::time(),
+                'type' => 'unicast',
+                'device_tokens' => $token,
+                'payload' => array_merge([
+                    'aps' => [
+                        'alert' => [
+                            'title' => $title,
+                            'subtitle' => '',
+                            'body' => $desc,
+                        ]
+                    ],
+                ], $extra),
+            ];
+        } else {
+            $body = [
+                'appkey' => env('UMENG_PUSH_ANDROID_APPKEY'),
+                'timestamp' => Base::time(),
+                'type' => 'unicast',
+                'device_tokens' => $token,
+                'payload' => [
+                    'display_type' => 'notification',
+                    'body' => [
+                        'ticker' => $title,
+                        'title' => $title,
+                        'text' => $desc,
+                    ],
+                    'extra' => $extra,
+                ],
+            ];
+        }
+        //
+        $res = self::curl($platform, 'https://msgapi.umeng.com/api/send', $body);
+        if (Base::isError($res)) {
+            return $res;
+        } else {
+            return Base::retSuccess('success');
+        }
+    }
+
+    /**
+     * 发送请求
+     * @param $platform
+     * @param $url
+     * @param $body
+     * @param string $method
+     * @return array
+     */
+    private static function curl($platform, $url, $body, $method = 'POST')
+    {
+        if ($platform == 'ios') {
+            $appkey = env('UMENG_PUSH_IOS_APPKEY');
+            $secret = env('UMENG_PUSH_IOS_APPMASTERSECRET');
+        } else {
+            $appkey = env('UMENG_PUSH_ANDROID_APPKEY');
+            $secret = env('UMENG_PUSH_ANDROID_APPMASTERSECRET');
+        }
+        if (empty($appkey)) {
+            return Base::retError('no appkey');
+        }
+        if (empty($secret)) {
+            return Base::retError('no secret');
+        }
+        //
+        $postBody = json_encode($body);
+        $mysign = md5($method . $url . $postBody . $secret);
+        $url.= "?sign=" . $mysign;
+        //
+        $res = Ihttp::ihttp_request($url, $postBody);
+        if (Base::isError($res)) {
+            return $res;
+        }
+        $array = json_decode($res['data'], true);
+        $debug = env('UMENG_PUSH_DEBUG');
+        if ($debug === true || $debug === 'info' || ($debug === 'error' && $array['ret'] !== 'SUCCESS')) {
+            $logFile = storage_path('logs/umeng-push-' . date('Y-m') . '.log');
+            file_put_contents($logFile, "[" . date("Y-m-d H:i:s") . "]\n" . Base::array2string_discard([
+                    'platform' => $platform,
+                    'url' => $url,
+                    'method' => $method,
+                    'body' => $body,
+                    'request' => $res['data'],
+                ]) . "\n", FILE_APPEND);
+        }
+        if ($array['ret'] == 'SUCCESS') {
+            return Base::retSuccess('success', $array['data']);
+        } else {
+            return Base::retError('error', $array['data']);
+        }
+    }
+}

+ 350 - 0
app/Module/Users.php

@@ -0,0 +1,350 @@
+<?php
+
+namespace App\Module;
+
+use App\Model\DBCache;
+use DB;
+use Request;
+use Session;
+
+/**
+ * Class Users
+ * @package App\Module
+ */
+class Users
+{
+    /**
+     * 注册会员
+     * @param $username
+     * @param $userpass
+     * @param array $other
+     * @return array
+     */
+    public static function reg($username, $userpass, $other = [])
+    {
+        //用户名
+        if (strlen($username) < 2) {
+            return Base::retError('用户名不可以少于2个字符!');
+        } elseif (strlen($username) > 16) {
+            return Base::retError('用户名最多只能设置16个字符!');
+        }
+        if (!preg_match('/^[A-Za-z0-9_\x{4e00}-\x{9fa5}]+$/u', $username)) {
+            return Base::retError('用户名由2-16位数字或字母、汉字、下划线组成!');
+        }
+        if (Users::username2id($username) > 0) {
+            return Base::retError('用户名已存在!');
+        }
+        //密码
+        if (strlen($userpass) < 6) {
+            return Base::retError('密码设置不能小于6位数!');
+        } elseif (strlen($userpass) > 32) {
+            return Base::retError('密码最多只能设置32位数!');
+        }
+        //开始注册
+        $encrypt = Base::generatePassword(6);
+        $inArray = [
+            'encrypt' => $encrypt,
+            'username' => $username,
+            'userpass' => Base::md52($userpass, $encrypt),
+            'regip' => Base::getIp(),
+            'regdate' => Base::time()
+        ];
+        if ($other) {
+            $inArray = array_merge($inArray, $other);
+        }
+        DB::table('users')->insert($inArray);
+        $user = Base::DBC2A(DB::table('users')->where('username', $username)->first());
+        if (empty($user)) {
+            return Base::retError('注册失败,请稍后再试。');
+        }
+        Users::AZUpdate($user['id']);
+        return Base::retSuccess('success', $user);
+    }
+
+    /**
+     * 临时身份标识
+     * @param int $length
+     * @return mixed|string
+     */
+    public static function tmpID($length = 8)
+    {
+        if (strlen(Request::input("tmpid")) == $length) {
+            return Request::input("tmpid");
+        }
+        $tmpID = Session::get('user::tmpID');
+        if (strlen($tmpID) != $length) {
+            $tmpID = Base::generatePassword($length);
+            Session::put('user::tmpID', $tmpID);
+        }
+        return $tmpID;
+    }
+
+    /**
+     * id获取用户名
+     * @param $id
+     * @return mixed
+     */
+    public static function id2username($id) {
+        return DB::table('users')->where('id', intval($id))->value('username');
+    }
+
+    /**
+     * 用户名获取id
+     * @param $username
+     * @return mixed
+     */
+    public static function username2id($username) {
+        return intval(DB::table('users')->where('username', $username)->value('id'));
+    }
+
+    /**
+     * token获取会员ID
+     * @return int
+     */
+    public static function token2userid()
+    {
+        $authorization = Base::getToken();
+        $id = 0;
+        if ($authorization) {
+            list($id, $username, $encrypt, $timestamp) = explode("@", base64_decode($authorization) . "@@@@");
+        }
+        return intval($id);
+    }
+
+    /**
+     * token获取会员手机号
+     * @return int
+     */
+    public static function token2username()
+    {
+        $authorization = Base::getToken();
+        $username = '';
+        if ($authorization) {
+            list($id, $username, $encrypt, $timestamp) = explode("@", base64_decode($authorization) . "@@@@");
+        }
+        return Base::isMobile($username) ? $username : '';
+    }
+
+    /**
+     * token获取encrypt
+     * @return mixed|string
+     */
+    public static function token2encrypt()
+    {
+        $authorization = Base::getToken();
+        $encrypt = '';
+        if ($authorization) {
+            list($id, $username, $encrypt, $timestamp) = explode("@", base64_decode($authorization) . "@@@@");
+        }
+        return $encrypt ?: '';
+    }
+
+    /**
+     * 用户身份认证(获取用户信息)
+     * @return array|mixed
+     */
+    public static function auth()
+    {
+        global $_A;
+        if (isset($_A["__static_auth"])) {
+            return $_A["__static_auth"];
+        }
+        $authorization = Base::getToken();
+        if ($authorization) {
+            list($id, $username, $encrypt, $timestamp) = explode("@", base64_decode($authorization) . "@@@@");
+            if (intval($id) > 0 && intval($timestamp) + 2592000 > Base::time()) {
+                $userinfo = DB::table('users')->where(['id' => $id, 'username' => $username, 'encrypt' => $encrypt])->first();
+                Base::coll2array($userinfo);
+                if ($userinfo['token']) {
+                    $upArray = [];
+                    if (Base::getIp() && $userinfo['lineip'] != Base::getIp()) {
+                        $upArray['lineip'] = Base::getIp();
+                    }
+                    if ($userinfo['linedate'] + 30 < Base::time()) {
+                        $upArray['linedate'] = Base::time();
+                    }
+                    if ($upArray) {
+                        DB::table('users')->where('id', $userinfo['id'])->update($upArray);
+                    }
+                    return $_A["__static_auth"] = Users::retInfo($userinfo);
+                }
+            }
+        }
+        return $_A["__static_auth"] = false;
+    }
+
+    /**
+     * 用户身份认证(获取用户信息)
+     * @return array|mixed
+     */
+    public static function authE()
+    {
+        $user = Users::auth();
+        if (!$user) {
+            $authorization = Base::getToken();
+            if ($authorization) {
+                return Base::retError('身份已失效,请重新登录!', [], -1);
+            } else {
+                return Base::retError('请登录后继续...', [], -1);
+            }
+        }
+        return Base::retSuccess("auth", $user);
+    }
+
+    /**
+     * 生成token
+     * @param $userinfo
+     * @return bool|string
+     */
+    public static function token($userinfo)
+    {
+        return base64_encode($userinfo['id'] . '@' . $userinfo['username'] . '@' . $userinfo['encrypt'] . '@' . Base::time() . '@' . Base::generatePassword(6));
+    }
+
+    /**
+     * 判断用户权限(身份)
+     * @param $identity
+     * @return array
+     */
+    public static function identity($identity)
+    {
+        $user = Users::auth();
+        if (is_array($user['identity'])
+            && in_array($identity, $user['identity'])) {
+            return Base::retSuccess("权限通过。");
+        }
+        return Base::retError("权限不足!");
+    }
+
+    /**
+     * 判断用户权限(身份)
+     * @param $identity
+     * @param $userIdentity
+     * @return bool
+     */
+    public static function identityRaw($identity, $userIdentity)
+    {
+        $userIdentity = is_array($userIdentity) ? $userIdentity : explode(",", trim($userIdentity, ","));
+        return $identity && in_array($identity, $userIdentity);
+    }
+
+    /**
+     * 筛选用户信息
+     * @param $userinfo
+     * @return mixed
+     */
+    public static function retInfo($userinfo)
+    {
+        //是否设置密码
+        if (!isset($userinfo['setpass'])) {
+            $userinfo['setpass'] = $userinfo['userpass'] ? 1 : 0;
+        }
+        //
+        $userinfo['id'] = intval($userinfo['id']);
+        $userinfo['changepass'] = intval($userinfo['changepass']);
+        $userinfo['setting'] = Base::string2array($userinfo['setting']);
+        $userinfo['userimg'] = self::userimg($userinfo['userimg']);
+        $userinfo['identity'] = is_array($userinfo['identity']) ? $userinfo['identity'] : explode(",", trim($userinfo['identity'], ","));
+        unset($userinfo['encrypt']);
+        unset($userinfo['userpass']);
+        return $userinfo;
+    }
+
+    /**
+     * userid 获取 基本信息
+     * @param int $userid           会员ID
+     * @return array
+     */
+    public static function userid2basic($userid)
+    {
+        if (empty($userid)) {
+            return [];
+        }
+        $fields = ['id AS userid', 'username', 'nickname', 'userimg', 'profession'];
+        $userInfo = DBCache::table('users')->where('id', $userid)->select($fields)->cacheMinutes(1)->first();
+        if ($userInfo) {
+            $userInfo['userimg'] = Users::userimg($userInfo['userimg']);
+        }
+        return $userInfo ?: [];
+    }
+
+    /**
+     * username 获取 基本信息
+     * @param string $username           用户名
+     * @param bool $clearCache           清理缓存
+     * @return array
+     */
+    public static function username2basic($username, $clearCache = false)
+    {
+        if (empty($username)) {
+            return [];
+        }
+        $fields = ['id AS userid', 'username', 'nickname', 'userimg', 'profession'];
+        $builder = DBCache::table('users')->where('username', $username)->select($fields)->cacheMinutes(1);
+        if ($clearCache) {
+            $builder->removeCache()->first();
+            return [];
+        } else {
+            $userInfo = $builder->first();
+            if ($userInfo) {
+                $userInfo['userimg'] = Users::userimg($userInfo['userimg']);
+            }
+            return $userInfo ?: [];
+        }
+    }
+
+    /**
+     * 获取会员昵称
+     * @param $username
+     * @return mixed
+     */
+    public static function nickname($username)
+    {
+        $info = self::username2basic($username);
+        if (empty($info)) {
+            return $username;
+        }
+        return $info['nickname'] ?: $info['username'];
+    }
+
+    /**
+     * 用户头像,不存在时返回默认
+     * @param string|array $params 头像地址 或 会员用户名
+     * @return \Illuminate\Contracts\Routing\UrlGenerator|string
+     */
+    public static function userimg($params) {
+        if (is_array($params)) {
+            foreach ($params as $key => $val) {
+                if (is_array($val)) {
+                    $val['userimg'] = self::userimg($val['userimg'] ?: $val['username']);
+                } else {
+                    $val = self::userimg($val);
+                }
+                $params[$key] = $val;
+            }
+            return $params;
+        }
+        if (!Base::strExists($params, '.')) {
+            if (empty($params)) {
+                $params = "";
+            } else {
+                $userInfo = self::username2basic($params);
+                $params = $userInfo['userimg'];
+            }
+        }
+        return $params ? Base::fillUrl($params) : url('images/other/avatar.png');
+    }
+
+    /**
+     * 更新首字母
+     * @param $userid
+     */
+    public static function AZUpdate($userid) {
+        $row = Base::DBC2A(DB::table('users')->where('id', $userid)->select(['username', 'nickname'])->first());
+        if ($row) {
+            DB::table('users')->where('id', $userid)->update([
+                'az' => Base::getFirstCharter($row['nickname'] ?: $row['username'])
+            ]);
+        }
+    }
+}

File diff suppressed because it is too large
+ 8469 - 0
app/Module/ip/all_cn.txt


+ 28 - 0
app/Providers/AppServiceProvider.php

@@ -0,0 +1,28 @@
+<?php
+
+namespace App\Providers;
+
+use Illuminate\Support\ServiceProvider;
+
+class AppServiceProvider extends ServiceProvider
+{
+    /**
+     * Register any application services.
+     *
+     * @return void
+     */
+    public function register()
+    {
+        //
+    }
+
+    /**
+     * Bootstrap any application services.
+     *
+     * @return void
+     */
+    public function boot()
+    {
+        //
+    }
+}

+ 30 - 0
app/Providers/AuthServiceProvider.php

@@ -0,0 +1,30 @@
+<?php
+
+namespace App\Providers;
+
+use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
+use Illuminate\Support\Facades\Gate;
+
+class AuthServiceProvider extends ServiceProvider
+{
+    /**
+     * The policy mappings for the application.
+     *
+     * @var array
+     */
+    protected $policies = [
+        // 'App\Model' => 'App\Policies\ModelPolicy',
+    ];
+
+    /**
+     * Register any authentication / authorization services.
+     *
+     * @return void
+     */
+    public function boot()
+    {
+        $this->registerPolicies();
+
+        //
+    }
+}

+ 21 - 0
app/Providers/BroadcastServiceProvider.php

@@ -0,0 +1,21 @@
+<?php
+
+namespace App\Providers;
+
+use Illuminate\Support\Facades\Broadcast;
+use Illuminate\Support\ServiceProvider;
+
+class BroadcastServiceProvider extends ServiceProvider
+{
+    /**
+     * Bootstrap any application services.
+     *
+     * @return void
+     */
+    public function boot()
+    {
+        Broadcast::routes();
+
+        require base_path('routes/channels.php');
+    }
+}

+ 34 - 0
app/Providers/EventServiceProvider.php

@@ -0,0 +1,34 @@
+<?php
+
+namespace App\Providers;
+
+use Illuminate\Auth\Events\Registered;
+use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
+use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
+use Illuminate\Support\Facades\Event;
+
+class EventServiceProvider extends ServiceProvider
+{
+    /**
+     * The event listener mappings for the application.
+     *
+     * @var array
+     */
+    protected $listen = [
+        Registered::class => [
+            SendEmailVerificationNotification::class,
+        ],
+    ];
+
+    /**
+     * Register any events for your application.
+     *
+     * @return void
+     */
+    public function boot()
+    {
+        parent::boot();
+
+        //
+    }
+}

+ 80 - 0
app/Providers/RouteServiceProvider.php

@@ -0,0 +1,80 @@
+<?php
+
+namespace App\Providers;
+
+use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
+use Illuminate\Support\Facades\Route;
+
+class RouteServiceProvider extends ServiceProvider
+{
+    /**
+     * This namespace is applied to your controller routes.
+     *
+     * In addition, it is set as the URL generator's root namespace.
+     *
+     * @var string
+     */
+    protected $namespace = 'App\Http\Controllers';
+
+    /**
+     * The path to the "home" route for your application.
+     *
+     * @var string
+     */
+    public const HOME = '/home';
+
+    /**
+     * Define your route model bindings, pattern filters, etc.
+     *
+     * @return void
+     */
+    public function boot()
+    {
+        //
+
+        parent::boot();
+    }
+
+    /**
+     * Define the routes for the application.
+     *
+     * @return void
+     */
+    public function map()
+    {
+        $this->mapApiRoutes();
+
+        $this->mapWebRoutes();
+
+        //
+    }
+
+    /**
+     * Define the "web" routes for the application.
+     *
+     * These routes all receive session state, CSRF protection, etc.
+     *
+     * @return void
+     */
+    protected function mapWebRoutes()
+    {
+        Route::middleware('web')
+            ->namespace($this->namespace)
+            ->group(base_path('routes/web.php'));
+    }
+
+    /**
+     * Define the "api" routes for the application.
+     *
+     * These routes are typically stateless.
+     *
+     * @return void
+     */
+    protected function mapApiRoutes()
+    {
+        Route::prefix('api')
+            ->middleware('api')
+            ->namespace($this->namespace)
+            ->group(base_path('routes/api.php'));
+    }
+}

+ 468 - 0
app/Services/WebSocketService.php

@@ -0,0 +1,468 @@
+<?php
+
+namespace App\Services;
+
+@error_reporting(E_ALL & ~E_NOTICE);
+
+use App\Module\Base;
+use App\Module\Chat;
+use App\Module\Users;
+use App\Tasks\ChromeExtendTask;
+use App\Tasks\NotificationTask;
+use App\Tasks\PushTask;
+use Cache;
+use DB;
+use Hhxsv5\LaravelS\Swoole\Task\Task;
+use Hhxsv5\LaravelS\Swoole\WebSocketHandlerInterface;
+use Swoole\Http\Request;
+use Swoole\WebSocket\Frame;
+use Swoole\WebSocket\Server;
+
+/**
+ * @see https://wiki.swoole.com/#/start/start_ws_server
+ */
+class WebSocketService implements WebSocketHandlerInterface
+{
+    /**
+     * 声明没有参数的构造函数
+     * WebSocketService constructor.
+     */
+    public function __construct()
+    {
+
+    }
+
+    /**
+     * 连接建立时触发
+     * @param Server $server
+     * @param Request $request
+     */
+    public function onOpen(Server $server, Request $request)
+    {
+        global $_A;
+        $_A = [
+            '__static_langdata' => [],
+        ];
+        //判断参数
+        $fd = $request->fd;
+        if (!isset($request->get['token'])) {
+            $server->push($fd, Chat::formatMsgSend([
+                'messageType' => 'error',
+                'body' => [
+                    'error' => '参数错误'
+                ],
+            ]));
+            $server->close($fd);
+            $this->deleteUser($fd);
+            return;
+        }
+        //判断token
+        $token = $request->get['token'];
+        $channel = $request->get['channel'] ?: '';
+        $cacheKey = "ws::token:" . md5($token);
+        $username = Cache::remember($cacheKey, now()->addSeconds(1), function () use ($token) {
+            list($id, $username, $encrypt, $timestamp) = explode("@", base64_decode($token) . "@@@@");
+            if (intval($id) > 0 && intval($timestamp) + 2592000 > time()) {
+                if (DB::table('users')->where(['id' => $id, 'username' => $username, 'encrypt' => $encrypt])->exists()) {
+                    return $username;
+                }
+            }
+            return null;
+        });
+        if (empty($username)) {
+            Cache::forget($cacheKey);
+            $server->push($fd, Chat::formatMsgSend([
+                'messageType' => 'error',
+                'channel' => $channel,
+                'body' => [
+                    'error' => '会员不存在',
+                ],
+            ]));
+            $server->close($fd);
+            $this->deleteUser($fd);
+            return;
+        }
+        //踢下线
+        if (in_array($channel, ['ios', 'android'])) {
+            $userLists = $this->getUser('', $channel, $username);
+            foreach ($userLists AS $user) {
+                $server->push($user['fd'], Chat::formatMsgSend([
+                    'messageType' => 'kick',
+                    'channel' => $channel,
+                    'body' => [
+                        'ip' => Base::getIp(),
+                        'time' => time(),
+                        'newfd' => $fd,
+                    ],
+                ]));
+                $this->deleteUser($user['fd']);
+            }
+        }
+        //保存用户、发送open事件
+        Cache::forever("ws::immediatelyNotify-" . $username, "no");
+        $this->saveUser($fd, $channel, $username);
+        $server->push($fd, Chat::formatMsgSend([
+            'messageType' => 'open',
+            'channel' => $channel,
+            'body' => [
+                'fd' => $fd,
+            ],
+        ]));
+        //发送最后一条未发送的信息
+        $lastMsg = Base::DBC2A(DB::table('chat_msg')->where('receive', $username)->orderByDesc('indate')->first());
+        if ($lastMsg && $lastMsg['roger'] === 0) {
+            $dialog = Chat::openDialog($lastMsg['username'], $lastMsg['receive']);
+            if (!Base::isError($dialog)) {
+                $dialog = $dialog['data'];
+                $unread = intval(DB::table('chat_dialog')->where('id', $dialog['id'])->value(($dialog['recField'] == 1 ? 'unread1' : 'unread2')));
+                $body = Base::string2array($lastMsg['message']);
+                $body['id'] = $lastMsg['id'];
+                $body['resend'] = 1;
+                $body['unread'] = $unread;
+                $body['username'] = $lastMsg['username'];
+                $body['userimg'] = Users::userimg($lastMsg['username']);
+                $body['indate'] = $lastMsg['indate'];
+                //
+                $basic = Users::username2basic($lastMsg['username']);
+                $body['userid'] = $basic ? $basic['userid'] : 0;
+                $body['nickname'] = $basic ? $basic['nickname'] : ($body['nickname'] || '');
+                $body['userimg'] = $basic ? $basic['userimg'] : ($body['userimg'] || '');
+                //
+                $server->push($fd, Chat::formatMsgSend([
+                    'messageType' => 'user',
+                    'contentId' => $lastMsg['id'],
+                    'channel' => $channel,
+                    'username' => $lastMsg['username'],
+                    'target' => $lastMsg['receive'],
+                    'body' => $body,
+                    'time' => $lastMsg['indate'],
+                ]));
+            }
+        }
+    }
+
+    /**
+     * 收到消息时触发
+     * @param Server $server
+     * @param Frame $frame
+     */
+    public function onMessage(Server $server, Frame $frame)
+    {
+        global $_A;
+        $_A = [
+            '__static_langdata' => [],
+        ];
+        //
+        $data = Chat::formatMsgReceive($frame->data);
+        $back = [
+            'status' => 1,
+            'message' => '',
+        ];
+        //
+        switch ($data['messageType']) {
+            /**
+             * APP激活进入前台
+             */
+            case 'appActivity':
+                Cache::forever("ws::immediatelyNotify-" . $data['username'], "no");
+                break;
+
+            /**
+             * 刷新
+             */
+            case 'refresh':
+                DB::table('ws')->where([
+                    'fd' => $frame->fd,
+                    'channel' => $data['channel'],
+                ])->update(['update' => time()]);
+                break;
+
+            /**
+             * 总未读消息数
+             */
+            case 'unread':
+                $username = $this->getUsername($frame->fd, $data['channel']);
+                if ($username) {
+                    $num = intval(DB::table('chat_dialog')->where('user1', $username)->sum('unread1'));
+                    $num+= intval(DB::table('chat_dialog')->where('user2', $username)->sum('unread2'));
+                    $back['message'] = $num;
+                } else {
+                    $back['message'] = 0;
+                }
+                break;
+
+            /**
+             * 已读会员消息
+             */
+            case 'read':
+                $username = $this->getUsername($frame->fd, $data['channel']);
+                $dialog = Chat::openDialog($username, $data['target']);
+                if (!Base::isError($dialog)) {
+                    $dialog = $dialog['data'];
+                    $upArray = [];
+                    if ($dialog['user1'] == $dialog['user2']) {
+                        $upArray['unread1'] = 0;
+                        $upArray['unread2'] = 0;
+                    } else {
+                        $upArray[($dialog['recField'] == 1 ? 'unread2' : 'unread1')] = 0;
+                    }
+                    DB::table('chat_dialog')->where('id', $dialog['id'])->update($upArray);
+                }
+                $chromeExtendTask = new ChromeExtendTask($username);
+                Task::deliver($chromeExtendTask);
+                break;
+
+            /**
+             * 收到信息回执
+             */
+            case 'roger':
+                $contentIds = Base::explodeInt(',', $data['contentId']);
+                if ($contentIds) {
+                    $username = $this->getUsername($frame->fd, $data['channel']);
+                    if ($username) {
+                        DB::table('chat_msg')->where('receive', $username)->whereIn('id', $contentIds)->update([
+                            'roger' => 1,
+                        ]);
+                    }
+                }
+                break;
+
+            /**
+             * 发给用户
+             */
+            case 'user':
+                $username = $this->getUsername($frame->fd, $data['channel']);
+                $res = Chat::saveMessage($username, $data['target'], $data['body']);
+                if (Base::isError($res)) {
+                    $back = [
+                        'status' => 0,
+                        'message' => $res['msg'],
+                    ];
+                } else {
+                    $resData = $res['data'];
+                    $back['message'] = $resData['id'];
+                    $data['contentId'] = $resData['id'];
+                    $data['body']['id'] = $resData['id'];
+                    $data['body']['unread'] = $resData['unread'];
+                    //
+                    $basic = Users::username2basic($username);
+                    $data['body']['userid'] = $basic ? $basic['userid'] : 0;
+                    $data['body']['nickname'] = $basic ? $basic['nickname'] : ($data['body']['nickname'] || '');
+                    $data['body']['userimg'] = $basic ? $basic['userimg'] : ($data['body']['userimg'] || '');
+                    //
+                    $pushLists = [];
+                    foreach ($this->getUserOfName($data['target']) AS $item) {
+                        $pushLists[] = [
+                            'fd' => $item['fd'],
+                            'msg' => $data
+                        ];
+                    }
+                    $pushTask = new PushTask($pushLists);
+                    Task::deliver($pushTask);
+                    //
+                    $notificationTask = new NotificationTask($resData['id']);
+                    $notificationTask->delay(Cache::get("ws::immediatelyNotify-" . $data['target']) == "yes" ? 2 : 10);
+                    Task::deliver($notificationTask);
+                }
+                break;
+
+            /**
+             * 发给用户(不保存记录)
+             */
+            case 'info':
+                $pushLists = [];
+                foreach ($this->getUserOfName($data['target']) AS $item) {
+                    $pushLists[] = [
+                        'fd' => $item['fd'],
+                        'msg' => $data
+                    ];
+                }
+                $pushTask = new PushTask($pushLists);
+                Task::deliver($pushTask);
+                break;
+
+            /**
+             * 发给整个团队
+             */
+            case 'team':
+                if ($data['body']['type'] === 'taskA') {
+                    $taskId = intval(Base::val($data['body'], 'taskDetail.id'));
+                    if ($taskId > 0) {
+                        $userLists = Chat::getTaskUsers($taskId);
+                    } else {
+                        $userLists = $this->getTeamUsers();
+                    }
+                    //
+                    $pushLists = [];
+                    foreach ($userLists as $user) {
+                        $data['messageType'] = 'user';
+                        $data['target'] = $user['username'];
+                        $pushLists[] = [
+                            'fd' => $user['fd'],
+                            'msg' => $data
+                        ];
+                    }
+                    $pushTask = new PushTask($pushLists);
+                    Task::deliver($pushTask);
+                }
+                break;
+
+            /**
+             * 知识库协作
+             */
+            case 'docs':
+                $back['message'] = [];
+                $body = $data['body'];
+                $type = $body['type'];
+                $sid = intval($body['sid']);
+                if ($sid <= 0) {
+                    return;
+                }
+                $array = Base::json2array(Cache::get("docs::" . $sid));
+                if ($array) {
+                    foreach ($array as $uname => $vbody) {
+                        if (intval($vbody['indate']) + 20 < time()) {
+                            unset($array[$uname]);
+                        }
+                    }
+                }
+                if ($type == 'enter' || $type == 'refresh') {
+                    $array[$body['username']] = $body;
+                } elseif ($type == 'quit') {
+                    unset($array[$body['username']]);
+                }
+                //
+                Cache::put("docs::" . $sid, Base::array2json($array), 30);
+                if ($array) {
+                    ksort($array);
+                }
+                $back['message'] = array_values($array);
+                //
+                if ($type == 'enter' || $type == 'quit') {
+                    $pushLists = [];
+                    foreach ($back['message'] AS $tuser) {
+                        foreach ($this->getUserOfName($tuser['username']) AS $item) {
+                            $pushLists[] = [
+                                'fd' => $item['fd'],
+                                'msg' => [
+                                    'messageType' => 'docs',
+                                    'body' => [
+                                        'type' => 'users',
+                                        'sid' => $sid,
+                                        'lists' => $back['message']
+                                    ]
+                                ]
+                            ];
+                        }
+                    }
+                    $pushTask = new PushTask($pushLists);
+                    Task::deliver($pushTask);
+                }
+                break;
+        }
+        if ($data['messageId']) {
+            $pushLists = [];
+            $pushLists[] = [
+                'fd' => $frame->fd,
+                'msg' => [
+                    'messageType' => 'back',
+                    'messageId' => $data['messageId'],
+                    'body' => $back,
+                ]
+            ];
+            $pushTask = new PushTask($pushLists);
+            Task::deliver($pushTask);
+        }
+    }
+
+    /**
+     * 关闭连接时触发
+     * @param Server $server
+     * @param $fd
+     * @param $reactorId
+     */
+    public function onClose(Server $server, $fd, $reactorId)
+    {
+        $this->deleteUser($fd);
+    }
+
+    /** ****************************************************************************** */
+    /** ****************************************************************************** */
+    /** ****************************************************************************** */
+
+    /**
+     * 保存用户
+     * @param $fd
+     * @param $channel
+     * @param $username
+     */
+    private function saveUser($fd, $channel, $username)
+    {
+        try {
+            DB::transaction(function () use ($username, $channel, $fd) {
+                $this->deleteUser($fd);
+                DB::table('ws')->updateOrInsert([
+                    'key' => md5($fd . '@' . $channel . '@' . $username)
+                ], [
+                    'fd' => $fd,
+                    'username' => $username,
+                    'channel' => $channel,
+                    'update' => time()
+                ]);
+            });
+        } catch (\Throwable $e) {
+
+        }
+    }
+
+    /**
+     * 清除用户
+     * @param $fd
+     */
+    private function deleteUser($fd)
+    {
+        DB::table('ws')->where('fd', $fd)->delete();
+    }
+
+    /**
+     * 获取用户
+     * @param string $fd
+     * @param string $channel
+     * @param string $username
+     * @return array
+     */
+    private function getUser($fd  = '', $channel = '', $username = '')
+    {
+        $array = [];
+        if ($fd) $array['fd'] = $fd;
+        if ($channel) $array['channel'] = $channel;
+        if ($username) $array['username'] = $username;
+        if (empty($array)) {
+            return [];
+        }
+        return Base::DBC2A(DB::table('ws')->select(['fd', 'username', 'channel'])->where($array)->get());
+    }
+
+    private function getUserOfFd($fd, $channel = '') {
+        return $this->getUser($fd, $channel);
+    }
+
+    private function getUserOfName($username, $channel = '') {
+        return $this->getUser('', $channel, $username);
+    }
+
+    private function getUsername($fd, $channel) {
+        return DB::table('ws')->where(['fd' => $fd, 'channel' => $channel ])->value('username');
+    }
+
+    /**
+     * 获取团队所有在线用户
+     * @return array|string
+     */
+    private function getTeamUsers()
+    {
+        return Base::DBC2A(DB::table('ws')->select(['fd', 'username', 'channel'])->where([
+            ['update', '>', time() - 600],
+        ])->get());
+    }
+}

+ 102 - 0
app/Tasks/AutoArchivedTask.php

@@ -0,0 +1,102 @@
+<?php
+namespace App\Tasks;
+
+@error_reporting(E_ALL & ~E_NOTICE);
+
+use App\Module\Base;
+use App\Module\Chat;
+use DB;
+use Hhxsv5\LaravelS\Swoole\Task\Task;
+
+/**
+ * 完成的任务自动归档
+ * Class AutoArchivedTask
+ * @package App\Tasks
+ */
+class AutoArchivedTask extends Task
+{
+
+    public function __construct()
+    {
+        //
+    }
+
+    public function handle()
+    {
+        $setting = Base::setting('system');
+        if ($setting['autoArchived'] === 'open') {
+            $archivedDay = intval($setting['archivedDay']);
+            if ($archivedDay > 0) {
+                $time = time();
+                $archivedDay = min(100, $archivedDay);
+                $archivedTime = $time - ($archivedDay * 86400);
+                //获取已完成未归档的任务
+                DB::transaction(function () use ($time, $archivedTime) {
+                    $taskLists = Base::DBC2A(DB::table('project_task')->where([
+                        ['delete', '=', 0],
+                        ['archiveddate', '=', 0],
+                        ['complete', '=', 1],
+                        ['completedate', '<=', $archivedTime],
+                    ])->take(100)->get());
+                    if ($taskLists) {
+                        $idArray = [];
+                        $logArray = [];
+                        $pushLists = [];
+                        $upArray = [
+                            'archived' => 1,
+                            'archiveddate' => $time,
+                        ];
+                        foreach ($taskLists AS $taskDetail) {
+                            $idArray[] = $taskDetail['id'];
+                            $logArray[] = [
+                                'type' => '日志',
+                                'projectid' => $taskDetail['projectid'],
+                                'taskid' => $taskDetail['id'],
+                                'username' => $taskDetail['username'],
+                                'detail' => '任务归档【自动】',
+                                'indate' => $time,
+                                'other' => Base::array2string([
+                                    'type' => 'task',
+                                    'id' => $taskDetail['id'],
+                                    'title' => $taskDetail['title'],
+                                ])
+                            ];
+                            $userLists = Chat::getTaskUsers($taskDetail['id']);
+                            if ($userLists) {
+                                foreach ($userLists as $user) {
+                                    $pushLists[] = [
+                                        'fd' => $user['fd'],
+                                        'msg' => [
+                                            'messageType' => 'user',
+                                            'username' => '::system',
+                                            'target' => $user['username'],
+                                            'time' => $time,
+                                            'body' => [
+                                                'act' => 'archived',
+                                                'type' => 'taskA',
+                                                'taskDetail' => array_merge($taskDetail, $upArray),
+                                            ]
+                                        ]
+                                    ];
+                                }
+                            }
+                        }
+                        if ($idArray) {
+                            DB::table('project_task')->whereIn('id', $idArray)->where([
+                                ['archiveddate', '=', 0],
+                                ['complete', '=', 1],
+                            ])->update($upArray);
+                        }
+                        if ($logArray) {
+                            DB::table('project_log')->insert($logArray);
+                        }
+                        if ($pushLists) {
+                            $pushTask = new PushTask($pushLists);
+                            Task::deliver($pushTask);
+                        }
+                    }
+                });
+            }
+        }
+    }
+}

+ 47 - 0
app/Tasks/ChromeExtendTask.php

@@ -0,0 +1,47 @@
+<?php
+namespace App\Tasks;
+
+@error_reporting(E_ALL & ~E_NOTICE);
+
+use App\Module\Base;
+use App\Module\Chat;
+use DB;
+use Hhxsv5\LaravelS\Swoole\Task\Task;
+
+class ChromeExtendTask extends Task
+{
+    private $username;
+
+    /**
+     * ChromeExtendTask constructor.
+     * @param $username
+     */
+    public function __construct($username)
+    {
+        $this->username = $username;
+    }
+
+    public function handle()
+    {
+        $lists = Base::DBC2A(DB::table('ws')->select(['fd', 'username', 'channel'])->where([
+            'username' => $this->username,
+            'channel' => 'chromeExtend',
+        ])->where([
+            ['update', '>', time() - 600],
+        ])->get());
+        if (count($lists) > 0) {
+            $unread = intval(DB::table('chat_dialog')->where('user1', $this->username)->sum('unread1'));
+            $unread+= intval(DB::table('chat_dialog')->where('user2', $this->username)->sum('unread2'));
+            //
+            $swoole = app('swoole');
+            foreach ($lists AS $item) {
+                $swoole->push($item['fd'], Chat::formatMsgSend([
+                    'messageType' => 'unread',
+                    'body' => [
+                        'unread' => $unread
+                    ],
+                ]));
+            }
+        }
+    }
+}

+ 50 - 0
app/Tasks/NotificationTask.php

@@ -0,0 +1,50 @@
+<?php
+namespace App\Tasks;
+
+@error_reporting(E_ALL & ~E_NOTICE);
+
+use App\Module\Base;
+use App\Module\Chat;
+use App\Module\Umeng;
+use App\Module\Users;
+use Cache;
+use DB;
+use Hhxsv5\LaravelS\Swoole\Task\Task;
+
+class NotificationTask extends Task
+{
+    private $contentId;
+
+    /**
+     * NotificationTask constructor.
+     * @param int $contentId
+     */
+    public function __construct($contentId)
+    {
+        $this->contentId = intval($contentId);
+    }
+
+    public function handle()
+    {
+        $row = Base::DBC2A(DB::table('chat_msg')->where('id', $this->contentId)->first());
+        if (empty($row)) {
+            return;
+        }
+        if ($row['roger']) {
+            return;
+        }
+        //
+        $receive = $row['receive'];
+        $username = $row['username'];
+        $message = Base::string2array($row['message']);
+        $lists = Base::DBC2A(DB::table('umeng')->where('username', $receive)->get());
+        foreach ($lists AS $item) {
+            Umeng::notification($item['platform'], $item['token'], Users::nickname($username), Chat::messageDesc($message), [
+                'notifyType' => 'userMsg',
+                'contentId' => $this->contentId,
+                'username' => $username,
+            ]);
+            Cache::forever("ws::immediatelyNotify-" . $receive, "yes");
+        }
+    }
+}

+ 27 - 0
app/Tasks/PushTask.php

@@ -0,0 +1,27 @@
+<?php
+namespace App\Tasks;
+
+use App\Module\Chat;
+use Hhxsv5\LaravelS\Swoole\Task\Task;
+
+class PushTask extends Task
+{
+    private $lists;
+
+    /**
+     * PushTask constructor.
+     * @param array $lists      [fd, msg]
+     */
+    public function __construct($lists)
+    {
+        $this->lists = $lists;
+    }
+
+    public function handle()
+    {
+        $swoole = app('swoole');
+        foreach ($this->lists AS $item) {
+            $swoole->push($item['fd'], Chat::formatMsgSend($item['msg']));
+        }
+    }
+}

+ 39 - 0
app/User.php

@@ -0,0 +1,39 @@
+<?php
+
+namespace App;
+
+use Illuminate\Contracts\Auth\MustVerifyEmail;
+use Illuminate\Foundation\Auth\User as Authenticatable;
+use Illuminate\Notifications\Notifiable;
+
+class User extends Authenticatable
+{
+    use Notifiable;
+
+    /**
+     * The attributes that are mass assignable.
+     *
+     * @var array
+     */
+    protected $fillable = [
+        'name', 'email', 'password',
+    ];
+
+    /**
+     * The attributes that should be hidden for arrays.
+     *
+     * @var array
+     */
+    protected $hidden = [
+        'password', 'remember_token',
+    ];
+
+    /**
+     * The attributes that should be cast to native types.
+     *
+     * @var array
+     */
+    protected $casts = [
+        'email_verified_at' => 'datetime',
+    ];
+}

+ 53 - 0
artisan

@@ -0,0 +1,53 @@
+#!/usr/bin/env php
+<?php
+
+define('LARAVEL_START', microtime(true));
+
+/*
+|--------------------------------------------------------------------------
+| Register The Auto Loader
+|--------------------------------------------------------------------------
+|
+| Composer provides a convenient, automatically generated class loader
+| for our application. We just need to utilize it! We'll require it
+| into the script here so that we do not have to worry about the
+| loading of any our classes "manually". Feels great to relax.
+|
+*/
+
+require __DIR__.'/vendor/autoload.php';
+
+$app = require_once __DIR__.'/bootstrap/app.php';
+
+/*
+|--------------------------------------------------------------------------
+| Run The Artisan Application
+|--------------------------------------------------------------------------
+|
+| When we run the console application, the current CLI command will be
+| executed in this console and the response sent back to a terminal
+| or another output device for the developers. Here goes nothing!
+|
+*/
+
+$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);
+
+$status = $kernel->handle(
+    $input = new Symfony\Component\Console\Input\ArgvInput,
+    new Symfony\Component\Console\Output\ConsoleOutput
+);
+
+/*
+|--------------------------------------------------------------------------
+| Shutdown The Application
+|--------------------------------------------------------------------------
+|
+| Once Artisan has finished running, we will fire off the shutdown events
+| so that any final work may be done by the application before we shut
+| down the process. This is the last thing to happen to the request.
+|
+*/
+
+$kernel->terminate($input, $status);
+
+exit($status);

+ 26 - 0
bin/fswatch

@@ -0,0 +1,26 @@
+#!/usr/bin/env bash
+WORK_DIR=$1
+if [ ! -n "${WORK_DIR}" ] ;then
+    WORK_DIR="."
+fi
+
+echo "Restarting LaravelS..."
+./bin/laravels restart -d -i
+
+echo "Starting fswatch..."
+LOCKING=0
+fswatch -e ".*" -i "\\.php$" -r ${WORK_DIR} | while read file
+do
+    if [[ ! ${file} =~ .php$ ]] ;then
+        continue
+    fi
+    if [ ${LOCKING} -eq 1 ] ;then
+        echo "Reloading, skipped."
+        continue
+    fi
+    echo "File ${file} has been modified."
+    LOCKING=1
+    ./bin/laravels reload
+    LOCKING=0
+done
+exit 0

+ 28 - 0
bin/inotify

@@ -0,0 +1,28 @@
+#!/usr/bin/env bash
+WORK_DIR=$1
+if [ ! -n "${WORK_DIR}" ] ;then
+    WORK_DIR="."
+fi
+
+echo "Restarting LaravelS..."
+./bin/laravels restart -d -i
+
+echo "Starting inotifywait..."
+LOCKING=0
+
+inotifywait --event modify --event create --event move --event delete -mrq   ${WORK_DIR}  | while read file
+
+do
+    if [[ ! ${file} =~ .php$ ]] ;then
+        continue
+    fi
+    if [ ${LOCKING} -eq 1 ] ;then
+        echo "Reloading, skipped."
+        continue
+    fi
+    echo "File ${file} has been modified."
+    LOCKING=1
+    ./bin/laravels reload
+    LOCKING=0
+done
+exit 0

+ 165 - 0
bin/laravels

@@ -0,0 +1,165 @@
+#!/usr/bin/env php
+<?php
+
+/**
+ * This autoloader is only used to pull laravel-s.
+ * Class Psr4Autoloader
+ */
+class Psr4Autoloader
+{
+    /**
+     * An associative array where the key is a namespace prefix and the value
+     * is an array of base directories for classes in that namespace.
+     *
+     * @var array
+     */
+    protected $prefixes = array();
+
+    /**
+     * Register loader with SPL autoloader stack.
+     *
+     * @return void
+     */
+    public function register()
+    {
+        spl_autoload_register(array($this, 'loadClass'));
+    }
+
+    /**
+     * Adds a base directory for a namespace prefix.
+     *
+     * @param string $prefix The namespace prefix.
+     * @param string $base_dir A base directory for class files in the
+     * namespace.
+     * @param bool $prepend If true, prepend the base directory to the stack
+     * instead of appending it; this causes it to be searched first rather
+     * than last.
+     * @return void
+     */
+    public function addNamespace($prefix, $base_dir, $prepend = false)
+    {
+        // normalize namespace prefix
+        $prefix = trim($prefix, '\\') . '\\';
+
+        // normalize the base directory with a trailing separator
+        $base_dir = rtrim($base_dir, DIRECTORY_SEPARATOR) . '/';
+
+        // initialize the namespace prefix array
+        if (isset($this->prefixes[$prefix]) === false) {
+            $this->prefixes[$prefix] = array();
+        }
+
+        // retain the base directory for the namespace prefix
+        if ($prepend) {
+            array_unshift($this->prefixes[$prefix], $base_dir);
+        } else {
+            array_push($this->prefixes[$prefix], $base_dir);
+        }
+    }
+
+    /**
+     * Loads the class file for a given class name.
+     *
+     * @param string $class The fully-qualified class name.
+     * @return mixed The mapped file name on success, or boolean false on
+     * failure.
+     */
+    public function loadClass($class)
+    {
+        // the current namespace prefix
+        $prefix = $class;
+
+        // work backwards through the namespace names of the fully-qualified
+        // class name to find a mapped file name
+        while (false !== $pos = strrpos($prefix, '\\')) {
+
+            // retain the trailing namespace separator in the prefix
+            $prefix = substr($class, 0, $pos + 1);
+
+            // the rest is the relative class name
+            $relative_class = substr($class, $pos + 1);
+
+            // try to load a mapped file for the prefix and relative class
+            $mapped_file = $this->loadMappedFile($prefix, $relative_class);
+            if ($mapped_file) {
+                return $mapped_file;
+            }
+
+            // remove the trailing namespace separator for the next iteration
+            // of strrpos()
+            $prefix = rtrim($prefix, '\\');
+        }
+
+        // never found a mapped file
+        return false;
+    }
+
+    /**
+     * Load the mapped file for a namespace prefix and relative class.
+     *
+     * @param string $prefix The namespace prefix.
+     * @param string $relative_class The relative class name.
+     * @return mixed Boolean false if no mapped file can be loaded, or the
+     * name of the mapped file that was loaded.
+     */
+    protected function loadMappedFile($prefix, $relative_class)
+    {
+        // are there any base directories for this namespace prefix?
+        if (isset($this->prefixes[$prefix]) === false) {
+            return false;
+        }
+
+        // look through base directories for this namespace prefix
+        foreach ($this->prefixes[$prefix] as $base_dir) {
+
+            // replace the namespace prefix with the base directory,
+            // replace namespace separators with directory separators
+            // in the relative class name, append with .php
+            $file = $base_dir
+                . str_replace('\\', '/', $relative_class)
+                . '.php';
+
+            // if the mapped file exists, require it
+            if ($this->requireFile($file)) {
+                // yes, we're done
+                return $file;
+            }
+        }
+
+        // never found it
+        return false;
+    }
+
+    /**
+     * If a file exists, require it from the file system.
+     *
+     * @param string $file The file to require.
+     * @return bool True if the file exists, false if not.
+     */
+    protected function requireFile($file)
+    {
+        if (file_exists($file)) {
+            require $file;
+            return true;
+        }
+        return false;
+    }
+}
+
+$basePath = realpath(__DIR__ . '/../');
+$loader = new Psr4Autoloader();
+$loader->register();
+
+// Register laravel-s
+$loader->addNamespace('Hhxsv5\LaravelS', $basePath . '/vendor/hhxsv5/laravel-s/src');
+
+// Register laravel-s dependencies
+$loader->addNamespace('Symfony\Component\Console', $basePath . '/vendor/symfony/console');
+$loader->addNamespace('Symfony\Contracts\Service', $basePath . '/vendor/symfony/service-contracts');
+$loader->addNamespace('Symfony\Contracts', $basePath . '/vendor/symfony/contracts');
+
+$command = new Hhxsv5\LaravelS\Console\Portal($basePath);
+$input = new Symfony\Component\Console\Input\ArgvInput();
+$output = new Symfony\Component\Console\Output\ConsoleOutput();
+$code = $command->run($input, $output);
+exit($code);

+ 61 - 0
bin/wookteam

@@ -0,0 +1,61 @@
+#!/usr/bin/env php
+<?php
+
+/**
+ * Class wookteamLoader
+ */
+class wookteamLoader
+{
+    function modifyEnv(array $data)
+    {
+        if (empty($data) || !is_array($data)) {
+            return false;
+        }
+        $envPath = realpath(__DIR__ . '/../') . DIRECTORY_SEPARATOR . '.env';
+        if (!file_exists($envPath)) {
+            return false;
+        }
+        $envContent = file_get_contents($envPath);
+        foreach ($data as $key => $val) {
+            $envContent = preg_replace("/^" . $key . "\s*=\s*(.*?)$/m", $key . "=" . $val, $envContent);
+        }
+        file_put_contents($envPath, $envContent);
+        return true;
+    }
+
+    function modifyWookteam($type)
+    {
+        $envPath = realpath(__DIR__ . '/../') . DIRECTORY_SEPARATOR . '/docker/php.conf';
+        if (!file_exists($envPath)) {
+            return false;
+        }
+        $envContent = file_get_contents($envPath);
+        $envContent = str_replace("#command=php bin/laravels start -i", "command=php bin/laravels start -i", $envContent);
+        $envContent = str_replace("#command=./bin/inotify ./app", "command=./bin/inotify ./app", $envContent);
+        if ($type == "dev") {
+            $envContent = str_replace("command=php bin/laravels start -i", "#command=php bin/laravels start -i", $envContent);
+        } else {
+            $envContent = str_replace("command=./bin/inotify ./app", "#command=./bin/inotify ./app", $envContent);
+        }
+        file_put_contents($envPath, $envContent);
+        return true;
+    }
+}
+
+$array = getopt('', ['port:', 'ssl:', 'wookteam:']);
+$loader = new wookteamLoader();
+if (isset($array['wookteam'])) {
+    $loader->modifyWookteam($array['wookteam']);
+} else {
+    $data = [];
+    if (isset($array['port'])) {
+        $data['APP_PORT'] = $array['port'] ?: '8000';
+    }
+    if (isset($array['ssl'])) {
+        $data['APP_PORT_SSL'] = $array['ssl'] ?: '44300';
+    }
+    if ($data) {
+        $loader->modifyEnv($data);
+    }
+}
+

+ 55 - 0
bootstrap/app.php

@@ -0,0 +1,55 @@
+<?php
+
+/*
+|--------------------------------------------------------------------------
+| Create The Application
+|--------------------------------------------------------------------------
+|
+| The first thing we will do is create a new Laravel application instance
+| which serves as the "glue" for all the components of Laravel, and is
+| the IoC container for the system binding all of the various parts.
+|
+*/
+
+$app = new Illuminate\Foundation\Application(
+    $_ENV['APP_BASE_PATH'] ?? dirname(__DIR__)
+);
+
+/*
+|--------------------------------------------------------------------------
+| Bind Important Interfaces
+|--------------------------------------------------------------------------
+|
+| Next, we need to bind some important interfaces into the container so
+| we will be able to resolve them when needed. The kernels serve the
+| incoming requests to this application from both the web and CLI.
+|
+*/
+
+$app->singleton(
+    Illuminate\Contracts\Http\Kernel::class,
+    App\Http\Kernel::class
+);
+
+$app->singleton(
+    Illuminate\Contracts\Console\Kernel::class,
+    App\Console\Kernel::class
+);
+
+$app->singleton(
+    Illuminate\Contracts\Debug\ExceptionHandler::class,
+    App\Exceptions\Handler::class
+);
+
+/*
+|--------------------------------------------------------------------------
+| Return The Application
+|--------------------------------------------------------------------------
+|
+| This script returns the application instance. The instance is given to
+| the calling script so we can separate the building of the instances
+| from the actual running of the application and sending responses.
+|
+*/
+
+return $app;

+ 2 - 0
bootstrap/cache/.gitignore

@@ -0,0 +1,2 @@
+*
+!.gitignore

+ 101 - 0
cmd

@@ -0,0 +1,101 @@
+#!/usr/bin/env bash
+
+#fonts color
+Green="\033[32m"
+Red="\033[31m"
+GreenBG="\033[42;37m"
+RedBG="\033[41;37m"
+Font="\033[0m"
+
+#notification information
+OK="${Green}[OK]${Font}"
+Error="${Red}[错误]${Font}"
+
+cur_path="$(pwd)"
+
+supervisorctl_restart() {
+    RES=`docker-compose exec php /bin/bash -c "supervisorctl update $1"`
+    if [ -z "$RES" ];then
+        docker-compose exec php /bin/bash -c "supervisorctl restart $1"
+    else
+        echo -e "$RES"
+    fi
+}
+
+####################################################################################
+####################################################################################
+####################################################################################
+
+COMPOSE="docker-compose"
+
+if [ $# -gt 0 ];then
+    if [[ "$1" == "init" ]] || [[ "$1" == "install" ]]; then
+        shift 1
+        if [ ! -f ".env" ];then
+            cp .env.docker .env
+        fi
+        rm -rf composer.lock
+        rm -rf package-lock.json
+        $COMPOSE build php
+        $COMPOSE up -d
+        $COMPOSE restart php
+        $COMPOSE exec php /bin/bash -c "composer install"
+        $COMPOSE exec php /bin/bash -c "php artisan key:generate"
+        $COMPOSE exec php /bin/bash -c "php artisan migrate --seed"
+        $COMPOSE exec php /bin/bash -c "php bin/wookteam --port=8000 --ssl=44300"
+        $COMPOSE exec php /bin/bash -c "php bin/wookteam --wookteam=prod"
+        $COMPOSE stop
+        $COMPOSE start
+    elif [[ "$1" == "update" ]]; then
+        shift 1
+        git fetch --all
+        git reset --hard origin/master
+        git pull
+        $COMPOSE exec php /bin/bash -c "composer install"
+        $COMPOSE exec php /bin/bash -c "php artisan migrate"
+        $COMPOSE stop
+        $COMPOSE start
+    elif [[ "$1" == "dev" ]]; then
+        shift 1
+        $COMPOSE exec php /bin/bash -c "php bin/wookteam --wookteam=dev"
+        supervisorctl_restart php
+        npm run hot
+    elif [[ "$1" == "prod" ]]; then
+        shift 1
+        $COMPOSE exec php /bin/bash -c "php bin/wookteam --wookteam=prod"
+        supervisorctl_restart php
+        npm run prod
+    elif [[ "$1" == "artisan" ]]; then
+        shift 1
+        e="php artisan $@" && $COMPOSE exec php /bin/bash -c "$e"
+    elif [[ "$1" == "php" ]]; then
+        shift 1
+        e="php $@" && $COMPOSE exec php /bin/bash -c "$e"
+    elif [[ "$1" == "composer" ]]; then
+        shift 1
+        e="composer $@" && $COMPOSE exec php /bin/bash -c "$e"
+    elif [[ "$1" == "supervisorctl" ]]; then
+        shift 1
+        e="supervisorctl $@" && $COMPOSE exec php /bin/bash -c "$e"
+    elif [[ "$1" == "test" ]]; then
+        shift 1
+        e="./vendor/bin/phpunit $@" && $COMPOSE exec php /bin/bash -c "$e"
+    elif [[ "$1" == "npm" ]]; then
+        shift 1
+        e="npm $@" && $COMPOSE exec php /bin/bash -c "$e"
+    elif [[ "$1" == "yarn" ]]; then
+        shift 1
+        e="yarn $@" && $COMPOSE exec php /bin/bash -c "$e"
+    elif [[ "$1" == "mysql" ]]; then
+        shift 1
+        e="mysql $@" && $COMPOSE exec mariadb /bin/sh -c "$e"
+    elif [[ "$1" == "restart" ]]; then
+        shift 1
+        $COMPOSE stop "$@"
+        $COMPOSE start "$@"
+    else
+        $COMPOSE "$@"
+    fi
+else
+    $COMPOSE ps
+fi

+ 76 - 0
composer.json

@@ -0,0 +1,76 @@
+{
+    "name": "laravel/laravel",
+    "type": "project",
+    "description": "The Laravel Framework.",
+    "keywords": [
+        "framework",
+        "laravel"
+    ],
+    "license": "MIT",
+    "require": {
+        "php": "^7.2.5",
+        "fideloper/proxy": "^4.2",
+        "fruitcake/laravel-cors": "^1.0",
+        "guzzlehttp/guzzle": "^6.3",
+        "hhxsv5/laravel-s": "^3.7",
+        "laravel/framework": "^7.10.3",
+        "laravel/tinker": "^2.0",
+        "maatwebsite/excel": "^3.1",
+        "madnest/madzipper": "^v1.0.4",
+        "predis/predis": "^1.1.1"
+    },
+    "require-dev": {
+        "barryvdh/laravel-ide-helper": "^2.7",
+        "facade/ignition": "^2.0",
+        "fzaninotto/faker": "^1.9.1",
+        "kitloong/laravel-migrations-generator": "^4.3",
+        "mockery/mockery": "^1.3.1",
+        "nunomaduro/collision": "^4.1",
+        "orangehill/iseed": "^2.6",
+        "phpunit/phpunit": "^8.5"
+    },
+    "config": {
+        "optimize-autoloader": true,
+        "preferred-install": "dist",
+        "sort-packages": true
+    },
+    "extra": {
+        "laravel": {
+            "dont-discover": []
+        }
+    },
+    "autoload": {
+        "psr-4": {
+            "App\\": "app/"
+        },
+        "classmap": [
+            "database/seeds",
+            "database/factories"
+        ]
+    },
+    "autoload-dev": {
+        "psr-4": {
+            "Tests\\": "tests/"
+        }
+    },
+    "minimum-stability": "dev",
+    "prefer-stable": true,
+    "scripts": {
+        "post-autoload-dump": [
+            "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
+            "@php artisan package:discover --ansi"
+        ],
+        "post-root-package-install": [
+            "@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
+        ],
+        "post-create-project-cmd": [
+            "@php artisan key:generate --ansi"
+        ]
+    },
+    "repositories": {
+        "packagist": {
+            "type": "composer",
+            "url": "https://mirrors.aliyun.com/composer/"
+        }
+    }
+}

+ 232 - 0
config/app.php

@@ -0,0 +1,232 @@
+<?php
+
+return [
+
+    /*
+    |--------------------------------------------------------------------------
+    | Application Name
+    |--------------------------------------------------------------------------
+    |
+    | This value is the name of your application. This value is used when the
+    | framework needs to place the application's name in a notification or
+    | any other location as required by the application or its packages.
+    |
+    */
+
+    'name' => env('APP_NAME', 'Laravel'),
+
+    /*
+    |--------------------------------------------------------------------------
+    | Application Environment
+    |--------------------------------------------------------------------------
+    |
+    | This value determines the "environment" your application is currently
+    | running in. This may determine how you prefer to configure various
+    | services the application utilizes. Set this in your ".env" file.
+    |
+    */
+
+    'env' => env('APP_ENV', 'production'),
+
+    /*
+    |--------------------------------------------------------------------------
+    | Application Debug Mode
+    |--------------------------------------------------------------------------
+    |
+    | When your application is in debug mode, detailed error messages with
+    | stack traces will be shown on every error that occurs within your
+    | application. If disabled, a simple generic error page is shown.
+    |
+    */
+
+    'debug' => (bool) env('APP_DEBUG', false),
+
+    /*
+    |--------------------------------------------------------------------------
+    | Application URL
+    |--------------------------------------------------------------------------
+    |
+    | This URL is used by the console to properly generate URLs when using
+    | the Artisan command line tool. You should set this to the root of
+    | your application so that it is used when running Artisan tasks.
+    |
+    */
+
+    'url' => env('APP_URL', 'http://localhost'),
+
+    'asset_url' => env('ASSET_URL', null),
+
+    /*
+    |--------------------------------------------------------------------------
+    | Application Timezone
+    |--------------------------------------------------------------------------
+    |
+    | Here you may specify the default timezone for your application, which
+    | will be used by the PHP date and date-time functions. We have gone
+    | ahead and set this to a sensible default for you out of the box.
+    |
+    */
+
+    'timezone' => 'PRC',
+
+    /*
+    |--------------------------------------------------------------------------
+    | Application Locale Configuration
+    |--------------------------------------------------------------------------
+    |
+    | The application locale determines the default locale that will be used
+    | by the translation service provider. You are free to set this value
+    | to any of the locales which will be supported by the application.
+    |
+    */
+
+    'locale' => 'en',
+
+    /*
+    |--------------------------------------------------------------------------
+    | Application Fallback Locale
+    |--------------------------------------------------------------------------
+    |
+    | The fallback locale determines the locale to use when the current one
+    | is not available. You may change the value to correspond to any of
+    | the language folders that are provided through your application.
+    |
+    */
+
+    'fallback_locale' => 'en',
+
+    /*
+    |--------------------------------------------------------------------------
+    | Faker Locale
+    |--------------------------------------------------------------------------
+    |
+    | This locale will be used by the Faker PHP library when generating fake
+    | data for your database seeds. For example, this will be used to get
+    | localized telephone numbers, street address information and more.
+    |
+    */
+
+    'faker_locale' => 'en_US',
+
+    /*
+    |--------------------------------------------------------------------------
+    | Encryption Key
+    |--------------------------------------------------------------------------
+    |
+    | This key is used by the Illuminate encrypter service and should be set
+    | to a random, 32 character string, otherwise these encrypted strings
+    | will not be safe. Please do this before deploying an application!
+    |
+    */
+
+    'key' => env('APP_KEY'),
+
+    'cipher' => 'AES-256-CBC',
+
+    /*
+    |--------------------------------------------------------------------------
+    | Autoloaded Service Providers
+    |--------------------------------------------------------------------------
+    |
+    | The service providers listed here will be automatically loaded on the
+    | request to your application. Feel free to add your own services to
+    | this array to grant expanded functionality to your applications.
+    |
+    */
+
+    'providers' => [
+
+        /*
+         * Laravel Framework Service Providers...
+         */
+        Illuminate\Auth\AuthServiceProvider::class,
+        Illuminate\Broadcasting\BroadcastServiceProvider::class,
+        Illuminate\Bus\BusServiceProvider::class,
+        Illuminate\Cache\CacheServiceProvider::class,
+        Illuminate\Foundation\Providers\ConsoleSupportServiceProvider::class,
+        Illuminate\Cookie\CookieServiceProvider::class,
+        Illuminate\Database\DatabaseServiceProvider::class,
+        Illuminate\Encryption\EncryptionServiceProvider::class,
+        Illuminate\Filesystem\FilesystemServiceProvider::class,
+        Illuminate\Foundation\Providers\FoundationServiceProvider::class,
+        Illuminate\Hashing\HashServiceProvider::class,
+        Illuminate\Mail\MailServiceProvider::class,
+        Illuminate\Notifications\NotificationServiceProvider::class,
+        Illuminate\Pagination\PaginationServiceProvider::class,
+        Illuminate\Pipeline\PipelineServiceProvider::class,
+        Illuminate\Queue\QueueServiceProvider::class,
+        Illuminate\Redis\RedisServiceProvider::class,
+        Illuminate\Auth\Passwords\PasswordResetServiceProvider::class,
+        Illuminate\Session\SessionServiceProvider::class,
+        Illuminate\Translation\TranslationServiceProvider::class,
+        Illuminate\Validation\ValidationServiceProvider::class,
+        Illuminate\View\ViewServiceProvider::class,
+
+        /*
+         * Package Service Providers...
+         */
+
+        /*
+         * Application Service Providers...
+         */
+        App\Providers\AppServiceProvider::class,
+        App\Providers\AuthServiceProvider::class,
+        // App\Providers\BroadcastServiceProvider::class,
+        App\Providers\EventServiceProvider::class,
+        App\Providers\RouteServiceProvider::class,
+        Madnest\Madzipper\MadzipperServiceProvider::class
+    ],
+
+    /*
+    |--------------------------------------------------------------------------
+    | Class Aliases
+    |--------------------------------------------------------------------------
+    |
+    | This array of class aliases will be registered when this application
+    | is started. However, feel free to register as many as you wish as
+    | the aliases are "lazy" loaded so they don't hinder performance.
+    |
+    */
+
+    'aliases' => [
+
+        'App' => Illuminate\Support\Facades\App::class,
+        'Arr' => Illuminate\Support\Arr::class,
+        'Artisan' => Illuminate\Support\Facades\Artisan::class,
+        'Auth' => Illuminate\Support\Facades\Auth::class,
+        'Blade' => Illuminate\Support\Facades\Blade::class,
+        'Broadcast' => Illuminate\Support\Facades\Broadcast::class,
+        'Bus' => Illuminate\Support\Facades\Bus::class,
+        'Cache' => Illuminate\Support\Facades\Cache::class,
+        'Config' => Illuminate\Support\Facades\Config::class,
+        'Cookie' => Illuminate\Support\Facades\Cookie::class,
+        'Crypt' => Illuminate\Support\Facades\Crypt::class,
+        'DB' => Illuminate\Support\Facades\DB::class,
+        'Eloquent' => Illuminate\Database\Eloquent\Model::class,
+        'Event' => Illuminate\Support\Facades\Event::class,
+        'File' => Illuminate\Support\Facades\File::class,
+        'Gate' => Illuminate\Support\Facades\Gate::class,
+        'Hash' => Illuminate\Support\Facades\Hash::class,
+        'Http' => Illuminate\Support\Facades\Http::class,
+        'Lang' => Illuminate\Support\Facades\Lang::class,
+        'Log' => Illuminate\Support\Facades\Log::class,
+        'Mail' => Illuminate\Support\Facades\Mail::class,
+        'Notification' => Illuminate\Support\Facades\Notification::class,
+        'Password' => Illuminate\Support\Facades\Password::class,
+        'Queue' => Illuminate\Support\Facades\Queue::class,
+        'Redirect' => Illuminate\Support\Facades\Redirect::class,
+        'Redis' => Illuminate\Support\Facades\Redis::class,
+        'Request' => Illuminate\Support\Facades\Request::class,
+        'Response' => Illuminate\Support\Facades\Response::class,
+        'Route' => Illuminate\Support\Facades\Route::class,
+        'Schema' => Illuminate\Support\Facades\Schema::class,
+        'Session' => Illuminate\Support\Facades\Session::class,
+        'Storage' => Illuminate\Support\Facades\Storage::class,
+        'Str' => Illuminate\Support\Str::class,
+        'URL' => Illuminate\Support\Facades\URL::class,
+        'Validator' => Illuminate\Support\Facades\Validator::class,
+        'View' => Illuminate\Support\Facades\View::class,
+        'Madzipper' => Madnest\Madzipper\Madzipper::class
+    ],
+
+];

+ 117 - 0
config/auth.php

@@ -0,0 +1,117 @@
+<?php
+
+return [
+
+    /*
+    |--------------------------------------------------------------------------
+    | Authentication Defaults
+    |--------------------------------------------------------------------------
+    |
+    | This option controls the default authentication "guard" and password
+    | reset options for your application. You may change these defaults
+    | as required, but they're a perfect start for most applications.
+    |
+    */
+
+    'defaults' => [
+        'guard' => 'web',
+        'passwords' => 'users',
+    ],
+
+    /*
+    |--------------------------------------------------------------------------
+    | Authentication Guards
+    |--------------------------------------------------------------------------
+    |
+    | Next, you may define every authentication guard for your application.
+    | Of course, a great default configuration has been defined for you
+    | here which uses session storage and the Eloquent user provider.
+    |
+    | All authentication drivers have a user provider. This defines how the
+    | users are actually retrieved out of your database or other storage
+    | mechanisms used by this application to persist your user's data.
+    |
+    | Supported: "session", "token"
+    |
+    */
+
+    'guards' => [
+        'web' => [
+            'driver' => 'session',
+            'provider' => 'users',
+        ],
+
+        'api' => [
+            'driver' => 'token',
+            'provider' => 'users',
+            'hash' => false,
+        ],
+    ],
+
+    /*
+    |--------------------------------------------------------------------------
+    | User Providers
+    |--------------------------------------------------------------------------
+    |
+    | All authentication drivers have a user provider. This defines how the
+    | users are actually retrieved out of your database or other storage
+    | mechanisms used by this application to persist your user's data.
+    |
+    | If you have multiple user tables or models you may configure multiple
+    | sources which represent each model / table. These sources may then
+    | be assigned to any extra authentication guards you have defined.
+    |
+    | Supported: "database", "eloquent"
+    |
+    */
+
+    'providers' => [
+        'users' => [
+            'driver' => 'eloquent',
+            'model' => App\User::class,
+        ],
+
+        // 'users' => [
+        //     'driver' => 'database',
+        //     'table' => 'users',
+        // ],
+    ],
+
+    /*
+    |--------------------------------------------------------------------------
+    | Resetting Passwords
+    |--------------------------------------------------------------------------
+    |
+    | You may specify multiple password reset configurations if you have more
+    | than one user table or model in the application and you want to have
+    | separate password reset settings based on the specific user types.
+    |
+    | The expire time is the number of minutes that the reset token should be
+    | considered valid. This security feature keeps tokens short-lived so
+    | they have less time to be guessed. You may change this as needed.
+    |
+    */
+
+    'passwords' => [
+        'users' => [
+            'provider' => 'users',
+            'table' => 'password_resets',
+            'expire' => 60,
+            'throttle' => 60,
+        ],
+    ],
+
+    /*
+    |--------------------------------------------------------------------------
+    | Password Confirmation Timeout
+    |--------------------------------------------------------------------------
+    |
+    | Here you may define the amount of seconds before a password confirmation
+    | times out and the user is prompted to re-enter their password via the
+    | confirmation screen. By default, the timeout lasts for three hours.
+    |
+    */
+
+    'password_timeout' => 10800,
+
+];

+ 59 - 0
config/broadcasting.php

@@ -0,0 +1,59 @@
+<?php
+
+return [
+
+    /*
+    |--------------------------------------------------------------------------
+    | Default Broadcaster
+    |--------------------------------------------------------------------------
+    |
+    | This option controls the default broadcaster that will be used by the
+    | framework when an event needs to be broadcast. You may set this to
+    | any of the connections defined in the "connections" array below.
+    |
+    | Supported: "pusher", "redis", "log", "null"
+    |
+    */
+
+    'default' => env('BROADCAST_DRIVER', 'null'),
+
+    /*
+    |--------------------------------------------------------------------------
+    | Broadcast Connections
+    |--------------------------------------------------------------------------
+    |
+    | Here you may define all of the broadcast connections that will be used
+    | to broadcast events to other systems or over websockets. Samples of
+    | each available type of connection are provided inside this array.
+    |
+    */
+
+    'connections' => [
+
+        'pusher' => [
+            'driver' => 'pusher',
+            'key' => env('PUSHER_APP_KEY'),
+            'secret' => env('PUSHER_APP_SECRET'),
+            'app_id' => env('PUSHER_APP_ID'),
+            'options' => [
+                'cluster' => env('PUSHER_APP_CLUSTER'),
+                'useTLS' => true,
+            ],
+        ],
+
+        'redis' => [
+            'driver' => 'redis',
+            'connection' => 'default',
+        ],
+
+        'log' => [
+            'driver' => 'log',
+        ],
+
+        'null' => [
+            'driver' => 'null',
+        ],
+
+    ],
+
+];

+ 104 - 0
config/cache.php

@@ -0,0 +1,104 @@
+<?php
+
+use Illuminate\Support\Str;
+
+return [
+
+    /*
+    |--------------------------------------------------------------------------
+    | Default Cache Store
+    |--------------------------------------------------------------------------
+    |
+    | This option controls the default cache connection that gets used while
+    | using this caching library. This connection is used when another is
+    | not explicitly specified when executing a given caching function.
+    |
+    | Supported: "apc", "array", "database", "file",
+    |            "memcached", "redis", "dynamodb"
+    |
+    */
+
+    'default' => env('CACHE_DRIVER', 'file'),
+
+    /*
+    |--------------------------------------------------------------------------
+    | Cache Stores
+    |--------------------------------------------------------------------------
+    |
+    | Here you may define all of the cache "stores" for your application as
+    | well as their drivers. You may even define multiple stores for the
+    | same cache driver to group types of items stored in your caches.
+    |
+    */
+
+    'stores' => [
+
+        'apc' => [
+            'driver' => 'apc',
+        ],
+
+        'array' => [
+            'driver' => 'array',
+            'serialize' => false,
+        ],
+
+        'database' => [
+            'driver' => 'database',
+            'table' => 'cache',
+            'connection' => null,
+        ],
+
+        'file' => [
+            'driver' => 'file',
+            'path' => storage_path('framework/cache/data'),
+        ],
+
+        'memcached' => [
+            'driver' => 'memcached',
+            'persistent_id' => env('MEMCACHED_PERSISTENT_ID'),
+            'sasl' => [
+                env('MEMCACHED_USERNAME'),
+                env('MEMCACHED_PASSWORD'),
+            ],
+            'options' => [
+                // Memcached::OPT_CONNECT_TIMEOUT => 2000,
+            ],
+            'servers' => [
+                [
+                    'host' => env('MEMCACHED_HOST', '127.0.0.1'),
+                    'port' => env('MEMCACHED_PORT', 11211),
+                    'weight' => 100,
+                ],
+            ],
+        ],
+
+        'redis' => [
+            'driver' => 'redis',
+            'connection' => 'cache',
+        ],
+
+        'dynamodb' => [
+            'driver' => 'dynamodb',
+            'key' => env('AWS_ACCESS_KEY_ID'),
+            'secret' => env('AWS_SECRET_ACCESS_KEY'),
+            'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
+            'table' => env('DYNAMODB_CACHE_TABLE', 'cache'),
+            'endpoint' => env('DYNAMODB_ENDPOINT'),
+        ],
+
+    ],
+
+    /*
+    |--------------------------------------------------------------------------
+    | Cache Key Prefix
+    |--------------------------------------------------------------------------
+    |
+    | When utilizing a RAM based store such as APC or Memcached, there might
+    | be other applications utilizing the same cache. So, we'll specify a
+    | value to get prefixed to all our keys so we can avoid collisions.
+    |
+    */
+
+    'prefix' => env('CACHE_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_cache'),
+
+];

+ 34 - 0
config/cors.php

@@ -0,0 +1,34 @@
+<?php
+
+return [
+
+    /*
+    |--------------------------------------------------------------------------
+    | Cross-Origin Resource Sharing (CORS) Configuration
+    |--------------------------------------------------------------------------
+    |
+    | Here you may configure your settings for cross-origin resource sharing
+    | or "CORS". This determines what cross-origin operations may execute
+    | in web browsers. You are free to adjust these settings as needed.
+    |
+    | To learn more: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
+    |
+    */
+
+    'paths' => ['api/*'],
+
+    'allowed_methods' => ['*'],
+
+    'allowed_origins' => ['*'],
+
+    'allowed_origins_patterns' => [],
+
+    'allowed_headers' => ['*'],
+
+    'exposed_headers' => [],
+
+    'max_age' => 0,
+
+    'supports_credentials' => false,
+
+];

+ 147 - 0
config/database.php

@@ -0,0 +1,147 @@
+<?php
+
+use Illuminate\Support\Str;
+
+return [
+
+    /*
+    |--------------------------------------------------------------------------
+    | Default Database Connection Name
+    |--------------------------------------------------------------------------
+    |
+    | Here you may specify which of the database connections below you wish
+    | to use as your default connection for all database work. Of course
+    | you may use many connections at once using the Database library.
+    |
+    */
+
+    'default' => env('DB_CONNECTION', 'mysql'),
+
+    /*
+    |--------------------------------------------------------------------------
+    | Database Connections
+    |--------------------------------------------------------------------------
+    |
+    | Here are each of the database connections setup for your application.
+    | Of course, examples of configuring each database platform that is
+    | supported by Laravel is shown below to make development simple.
+    |
+    |
+    | All database work in Laravel is done through the PHP PDO facilities
+    | so make sure you have the driver for your particular database of
+    | choice installed on your machine before you begin development.
+    |
+    */
+
+    'connections' => [
+
+        'sqlite' => [
+            'driver' => 'sqlite',
+            'url' => env('DATABASE_URL'),
+            'database' => env('DB_DATABASE', database_path('database.sqlite')),
+            'prefix' => env('DB_PREFIX', ''),
+            'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
+        ],
+
+        'mysql' => [
+            'driver' => 'mysql',
+            'url' => env('DATABASE_URL'),
+            'host' => env('DB_HOST', '127.0.0.1'),
+            'port' => env('DB_PORT', '3306'),
+            'database' => env('DB_DATABASE', 'forge'),
+            'username' => env('DB_USERNAME', 'forge'),
+            'password' => env('DB_PASSWORD', ''),
+            'unix_socket' => env('DB_SOCKET', ''),
+            'charset' => 'utf8mb4',
+            'collation' => 'utf8mb4_unicode_ci',
+            'prefix' => env('DB_PREFIX', ''),
+            'prefix_indexes' => true,
+            'strict' => true,
+            'engine' => null,
+            'options' => extension_loaded('pdo_mysql') ? array_filter([
+                PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
+            ]) : [],
+        ],
+
+        'pgsql' => [
+            'driver' => 'pgsql',
+            'url' => env('DATABASE_URL'),
+            'host' => env('DB_HOST', '127.0.0.1'),
+            'port' => env('DB_PORT', '5432'),
+            'database' => env('DB_DATABASE', 'forge'),
+            'username' => env('DB_USERNAME', 'forge'),
+            'password' => env('DB_PASSWORD', ''),
+            'charset' => 'utf8',
+            'prefix' => env('DB_PREFIX', ''),
+            'prefix_indexes' => true,
+            'schema' => 'public',
+            'sslmode' => 'prefer',
+        ],
+
+        'sqlsrv' => [
+            'driver' => 'sqlsrv',
+            'url' => env('DATABASE_URL'),
+            'host' => env('DB_HOST', 'localhost'),
+            'port' => env('DB_PORT', '1433'),
+            'database' => env('DB_DATABASE', 'forge'),
+            'username' => env('DB_USERNAME', 'forge'),
+            'password' => env('DB_PASSWORD', ''),
+            'charset' => 'utf8',
+            'prefix' => env('DB_PREFIX', ''),
+            'prefix_indexes' => true,
+        ],
+
+    ],
+
+    /*
+    |--------------------------------------------------------------------------
+    | Migration Repository Table
+    |--------------------------------------------------------------------------
+    |
+    | This table keeps track of all the migrations that have already run for
+    | your application. Using this information, we can determine which of
+    | the migrations on disk haven't actually been run in the database.
+    |
+    */
+
+    'migrations' => 'migrations',
+
+    /*
+    |--------------------------------------------------------------------------
+    | Redis Databases
+    |--------------------------------------------------------------------------
+    |
+    | Redis is an open source, fast, and advanced key-value store that also
+    | provides a richer body of commands than a typical key-value system
+    | such as APC or Memcached. Laravel makes it easy to dig right in.
+    |
+    */
+
+    'redis' => [
+
+        'client' => env('REDIS_CLIENT', 'phpredis'),
+
+        'options' => [
+            'cluster' => env('REDIS_CLUSTER', 'redis'),
+            'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'),
+        ],
+
+        'default' => [
+            'url' => env('REDIS_URL'),
+            'host' => env('REDIS_HOST', '127.0.0.1'),
+            'password' => env('REDIS_PASSWORD', null),
+            'port' => env('REDIS_PORT', '6379'),
+            'database' => env('REDIS_DB', '0'),
+        ],
+
+        'cache' => [
+            'url' => env('REDIS_URL'),
+            'host' => env('REDIS_HOST', '127.0.0.1'),
+            'password' => env('REDIS_PASSWORD', null),
+            'port' => env('REDIS_PORT', '6379'),
+            'database' => env('REDIS_CACHE_DB', '1'),
+        ],
+
+    ],
+
+];

+ 85 - 0
config/filesystems.php

@@ -0,0 +1,85 @@
+<?php
+
+return [
+
+    /*
+    |--------------------------------------------------------------------------
+    | Default Filesystem Disk
+    |--------------------------------------------------------------------------
+    |
+    | Here you may specify the default filesystem disk that should be used
+    | by the framework. The "local" disk, as well as a variety of cloud
+    | based disks are available to your application. Just store away!
+    |
+    */
+
+    'default' => env('FILESYSTEM_DRIVER', 'local'),
+
+    /*
+    |--------------------------------------------------------------------------
+    | Default Cloud Filesystem Disk
+    |--------------------------------------------------------------------------
+    |
+    | Many applications store files both locally and in the cloud. For this
+    | reason, you may specify a default "cloud" driver here. This driver
+    | will be bound as the Cloud disk implementation in the container.
+    |
+    */
+
+    'cloud' => env('FILESYSTEM_CLOUD', 's3'),
+
+    /*
+    |--------------------------------------------------------------------------
+    | Filesystem Disks
+    |--------------------------------------------------------------------------
+    |
+    | Here you may configure as many filesystem "disks" as you wish, and you
+    | may even configure multiple disks of the same driver. Defaults have
+    | been setup for each driver as an example of the required options.
+    |
+    | Supported Drivers: "local", "ftp", "sftp", "s3"
+    |
+    */
+
+    'disks' => [
+
+        'local' => [
+            'driver' => 'local',
+            'root' => storage_path('app'),
+        ],
+
+        'public' => [
+            'driver' => 'local',
+            'root' => storage_path('app/public'),
+            'url' => env('APP_URL').'/storage',
+            'visibility' => 'public',
+        ],
+
+        's3' => [
+            'driver' => 's3',
+            'key' => env('AWS_ACCESS_KEY_ID'),
+            'secret' => env('AWS_SECRET_ACCESS_KEY'),
+            'region' => env('AWS_DEFAULT_REGION'),
+            'bucket' => env('AWS_BUCKET'),
+            'url' => env('AWS_URL'),
+            'endpoint' => env('AWS_ENDPOINT'),
+        ],
+
+    ],
+
+    /*
+    |--------------------------------------------------------------------------
+    | Symbolic Links
+    |--------------------------------------------------------------------------
+    |
+    | Here you may configure the symbolic links that will be created when the
+    | `storage:link` Artisan command is executed. The array keys should be
+    | the locations of the links and the values should be their targets.
+    |
+    */
+
+    'links' => [
+        public_path('storage') => storage_path('app/public'),
+    ],
+
+];

+ 52 - 0
config/hashing.php

@@ -0,0 +1,52 @@
+<?php
+
+return [
+
+    /*
+    |--------------------------------------------------------------------------
+    | Default Hash Driver
+    |--------------------------------------------------------------------------
+    |
+    | This option controls the default hash driver that will be used to hash
+    | passwords for your application. By default, the bcrypt algorithm is
+    | used; however, you remain free to modify this option if you wish.
+    |
+    | Supported: "bcrypt", "argon", "argon2id"
+    |
+    */
+
+    'driver' => 'bcrypt',
+
+    /*
+    |--------------------------------------------------------------------------
+    | Bcrypt Options
+    |--------------------------------------------------------------------------
+    |
+    | Here you may specify the configuration options that should be used when
+    | passwords are hashed using the Bcrypt algorithm. This will allow you
+    | to control the amount of time it takes to hash the given password.
+    |
+    */
+
+    'bcrypt' => [
+        'rounds' => env('BCRYPT_ROUNDS', 10),
+    ],
+
+    /*
+    |--------------------------------------------------------------------------
+    | Argon Options
+    |--------------------------------------------------------------------------
+    |
+    | Here you may specify the configuration options that should be used when
+    | passwords are hashed using the Argon algorithm. These will allow you
+    | to control the amount of time it takes to hash the given password.
+    |
+    */
+
+    'argon' => [
+        'memory' => 1024,
+        'threads' => 2,
+        'time' => 2,
+    ],
+
+];

+ 99 - 0
config/laravels.php

@@ -0,0 +1,99 @@
+<?php
+/**
+ * @see https://github.com/hhxsv5/laravel-s/blob/master/Settings-CN.md  Chinese
+ * @see https://github.com/hhxsv5/laravel-s/blob/master/Settings.md  English
+ */
+return [
+    'listen_ip'                => env('LARAVELS_LISTEN_IP', '127.0.0.1'),
+    'listen_port'              => env('LARAVELS_LISTEN_PORT', 5200),
+    'socket_type'              => defined('SWOOLE_SOCK_TCP') ? SWOOLE_SOCK_TCP : 1,
+    'enable_coroutine_runtime' => false,
+    'server'                   => env('LARAVELS_SERVER', 'LaravelS'),
+    'handle_static'            => env('LARAVELS_HANDLE_STATIC', false),
+    'laravel_base_path'        => env('LARAVEL_BASE_PATH', base_path()),
+    'inotify_reload'           => [
+        'enable'        => env('LARAVELS_INOTIFY_RELOAD', false),
+        'watch_path'    => base_path(),
+        'file_types'    => ['.php'],
+        'excluded_dirs' => [],
+        'log'           => true,
+    ],
+    'event_handlers'           => [
+        'ServerStart' => \App\Events\ServerStartEvent::class,
+        'WorkerStart' => \App\Events\WorkerStartEvent::class,
+    ],
+    'websocket'                => [
+        'enable' => true,
+        'handler' => \App\Services\WebSocketService::class,
+    ],
+    'sockets'                  => [],
+    'processes'                => [
+        //[
+        //    'class'    => \App\Processes\TestProcess::class,
+        //    'redirect' => false, // Whether redirect stdin/stdout, true or false
+        //    'pipe'     => 0 // The type of pipeline, 0: no pipeline 1: SOCK_STREAM 2: SOCK_DGRAM
+        //    'enable'   => true // Whether to enable, default true
+        //],
+    ],
+    'timer'                    => [
+        'enable'        => env('LARAVELS_TIMER', true),
+        'jobs'          => [
+            // Enable LaravelScheduleJob to run `php artisan schedule:run` every 1 minute, replace Linux Crontab
+            //\Hhxsv5\LaravelS\Illuminate\LaravelScheduleJob::class,
+            // Two ways to configure parameters:
+            // [\App\Jobs\XxxCronJob::class, [1000, true]], // Pass in parameters when registering
+            // \App\Jobs\XxxCronJob::class, // Override the corresponding method to return the configuration
+            \App\Jobs\Timer\SystemCronJob::class
+        ],
+        'max_wait_time' => 5,
+    ],
+    'swoole_tables'            => [],
+    'register_providers'       => [],
+    'cleaners'                 => [
+        // See LaravelS's built-in cleaners: https://github.com/hhxsv5/laravel-s/blob/master/Settings.md#cleaners
+    ],
+    'destroy_controllers'      => [
+        'enable'        => false,
+        'excluded_list' => [
+            //\App\Http\Controllers\TestController::class,
+        ],
+    ],
+    'swoole'                   => [
+        'daemonize'          => env('LARAVELS_DAEMONIZE', false),
+        'dispatch_mode'      => 2,
+        'reactor_num'        => env('LARAVELS_REACTOR_NUM', function_exists('swoole_cpu_num') ? swoole_cpu_num() * 2 : 4),
+        'worker_num'         => env('LARAVELS_WORKER_NUM', function_exists('swoole_cpu_num') ? swoole_cpu_num() * 2 : 8),
+        'task_worker_num'    => env('LARAVELS_TASK_WORKER_NUM', function_exists('swoole_cpu_num') ? swoole_cpu_num() * 2 : 8),
+        'task_ipc_mode'      => 1,
+        'task_max_request'   => env('LARAVELS_TASK_MAX_REQUEST', 8000),
+        'task_tmpdir'        => @is_writable('/dev/shm/') ? '/dev/shm' : '/tmp',
+        'max_request'        => env('LARAVELS_MAX_REQUEST', 8000),
+        'open_tcp_nodelay'   => true,
+        'pid_file'           => storage_path('laravels.pid'),
+        'log_file'           => storage_path(sprintf('logs/swoole-%s.log', date('Y-m'))),
+        'log_level'          => 4,
+        'document_root'      => base_path('public'),
+        'buffer_output_size' => 200 * 1024 * 1024,
+        'socket_buffer_size' => 256 * 1024 * 1024,
+        'package_max_length' => 200 * 1024 * 1024,
+        'reload_async'       => true,
+        'max_wait_time'      => 60,
+        'enable_reuse_port'  => true,
+        'enable_coroutine'   => false,
+        'http_compression'   => false,
+
+        // Slow log
+        // 'request_slowlog_timeout' => 2,
+        // 'request_slowlog_file'    => storage_path(sprintf('logs/slow-%s.log', date('Y-m'))),
+        // 'trace_event_worker'      => true,
+
+        'heartbeat_idle_time'      => 600,
+        'heartbeat_check_interval' => 60,
+
+        /**
+         * More settings of Swoole
+         * @see https://wiki.swoole.com/#/server/setting  Chinese
+         * @see https://www.swoole.co.uk/docs/modules/swoole-server/configuration  English
+         */
+    ],
+];

+ 104 - 0
config/logging.php

@@ -0,0 +1,104 @@
+<?php
+
+use Monolog\Handler\NullHandler;
+use Monolog\Handler\StreamHandler;
+use Monolog\Handler\SyslogUdpHandler;
+
+return [
+
+    /*
+    |--------------------------------------------------------------------------
+    | Default Log Channel
+    |--------------------------------------------------------------------------
+    |
+    | This option defines the default log channel that gets used when writing
+    | messages to the logs. The name specified in this option should match
+    | one of the channels defined in the "channels" configuration array.
+    |
+    */
+
+    'default' => env('LOG_CHANNEL', 'stack'),
+
+    /*
+    |--------------------------------------------------------------------------
+    | Log Channels
+    |--------------------------------------------------------------------------
+    |
+    | Here you may configure the log channels for your application. Out of
+    | the box, Laravel uses the Monolog PHP logging library. This gives
+    | you a variety of powerful log handlers / formatters to utilize.
+    |
+    | Available Drivers: "single", "daily", "slack", "syslog",
+    |                    "errorlog", "monolog",
+    |                    "custom", "stack"
+    |
+    */
+
+    'channels' => [
+        'stack' => [
+            'driver' => 'stack',
+            'channels' => ['single'],
+            'ignore_exceptions' => false,
+        ],
+
+        'single' => [
+            'driver' => 'single',
+            'path' => storage_path('logs/laravel.log'),
+            'level' => 'debug',
+        ],
+
+        'daily' => [
+            'driver' => 'daily',
+            'path' => storage_path('logs/laravel.log'),
+            'level' => 'debug',
+            'days' => 14,
+        ],
+
+        'slack' => [
+            'driver' => 'slack',
+            'url' => env('LOG_SLACK_WEBHOOK_URL'),
+            'username' => 'Laravel Log',
+            'emoji' => ':boom:',
+            'level' => 'critical',
+        ],
+
+        'papertrail' => [
+            'driver' => 'monolog',
+            'level' => 'debug',
+            'handler' => SyslogUdpHandler::class,
+            'handler_with' => [
+                'host' => env('PAPERTRAIL_URL'),
+                'port' => env('PAPERTRAIL_PORT'),
+            ],
+        ],
+
+        'stderr' => [
+            'driver' => 'monolog',
+            'handler' => StreamHandler::class,
+            'formatter' => env('LOG_STDERR_FORMATTER'),
+            'with' => [
+                'stream' => 'php://stderr',
+            ],
+        ],
+
+        'syslog' => [
+            'driver' => 'syslog',
+            'level' => 'debug',
+        ],
+
+        'errorlog' => [
+            'driver' => 'errorlog',
+            'level' => 'debug',
+        ],
+
+        'null' => [
+            'driver' => 'monolog',
+            'handler' => NullHandler::class,
+        ],
+
+        'emergency' => [
+            'path' => storage_path('logs/laravel.log'),
+        ],
+    ],
+
+];

+ 109 - 0
config/mail.php

@@ -0,0 +1,109 @@
+<?php
+
+return [
+
+    /*
+    |--------------------------------------------------------------------------
+    | Default Mailer
+    |--------------------------------------------------------------------------
+    |
+    | This option controls the default mailer that is used to send any email
+    | messages sent by your application. Alternative mailers may be setup
+    | and used as needed; however, this mailer will be used by default.
+    |
+    */
+
+    'default' => env('MAIL_MAILER', 'smtp'),
+
+    /*
+    |--------------------------------------------------------------------------
+    | Mailer Configurations
+    |--------------------------------------------------------------------------
+    |
+    | Here you may configure all of the mailers used by your application plus
+    | their respective settings. Several examples have been configured for
+    | you and you are free to add your own as your application requires.
+    |
+    | Laravel supports a variety of mail "transport" drivers to be used while
+    | sending an e-mail. You will specify which one you are using for your
+    | mailers below. You are free to add additional mailers as required.
+    |
+    | Supported: "smtp", "sendmail", "mailgun", "ses",
+    |            "postmark", "log", "array"
+    |
+    */
+
+    'mailers' => [
+        'smtp' => [
+            'transport' => 'smtp',
+            'host' => env('MAIL_HOST', 'smtp.mailgun.org'),
+            'port' => env('MAIL_PORT', 587),
+            'encryption' => env('MAIL_ENCRYPTION', 'tls'),
+            'username' => env('MAIL_USERNAME'),
+            'password' => env('MAIL_PASSWORD'),
+            'timeout' => null,
+        ],
+
+        'ses' => [
+            'transport' => 'ses',
+        ],
+
+        'mailgun' => [
+            'transport' => 'mailgun',
+        ],
+
+        'postmark' => [
+            'transport' => 'postmark',
+        ],
+
+        'sendmail' => [
+            'transport' => 'sendmail',
+            'path' => '/usr/sbin/sendmail -bs',
+        ],
+
+        'log' => [
+            'transport' => 'log',
+            'channel' => env('MAIL_LOG_CHANNEL'),
+        ],
+
+        'array' => [
+            'transport' => 'array',
+        ],
+    ],
+
+    /*
+    |--------------------------------------------------------------------------
+    | Global "From" Address
+    |--------------------------------------------------------------------------
+    |
+    | You may wish for all e-mails sent by your application to be sent from
+    | the same address. Here, you may specify a name and address that is
+    | used globally for all e-mails that are sent by your application.
+    |
+    */
+
+    'from' => [
+        'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),
+        'name' => env('MAIL_FROM_NAME', 'Example'),
+    ],
+
+    /*
+    |--------------------------------------------------------------------------
+    | Markdown Mail Settings
+    |--------------------------------------------------------------------------
+    |
+    | If you are using Markdown based email rendering, you may configure your
+    | theme and component paths here, allowing you to customize the design
+    | of the emails. Or, you may simply stick with the Laravel defaults!
+    |
+    */
+
+    'markdown' => [
+        'theme' => 'default',
+
+        'paths' => [
+            resource_path('views/vendor/mail'),
+        ],
+    ],
+
+];

+ 89 - 0
config/queue.php

@@ -0,0 +1,89 @@
+<?php
+
+return [
+
+    /*
+    |--------------------------------------------------------------------------
+    | Default Queue Connection Name
+    |--------------------------------------------------------------------------
+    |
+    | Laravel's queue API supports an assortment of back-ends via a single
+    | API, giving you convenient access to each back-end using the same
+    | syntax for every one. Here you may define a default connection.
+    |
+    */
+
+    'default' => env('QUEUE_CONNECTION', 'sync'),
+
+    /*
+    |--------------------------------------------------------------------------
+    | Queue Connections
+    |--------------------------------------------------------------------------
+    |
+    | Here you may configure the connection information for each server that
+    | is used by your application. A default configuration has been added
+    | for each back-end shipped with Laravel. You are free to add more.
+    |
+    | Drivers: "sync", "database", "beanstalkd", "sqs", "redis", "null"
+    |
+    */
+
+    'connections' => [
+
+        'sync' => [
+            'driver' => 'sync',
+        ],
+
+        'database' => [
+            'driver' => 'database',
+            'table' => 'jobs',
+            'queue' => 'default',
+            'retry_after' => 90,
+        ],
+
+        'beanstalkd' => [
+            'driver' => 'beanstalkd',
+            'host' => 'localhost',
+            'queue' => 'default',
+            'retry_after' => 90,
+            'block_for' => 0,
+        ],
+
+        'sqs' => [
+            'driver' => 'sqs',
+            'key' => env('AWS_ACCESS_KEY_ID'),
+            'secret' => env('AWS_SECRET_ACCESS_KEY'),
+            'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'),
+            'queue' => env('SQS_QUEUE', 'your-queue-name'),
+            'suffix' => env('SQS_SUFFIX'),
+            'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
+        ],
+
+        'redis' => [
+            'driver' => 'redis',
+            'connection' => 'default',
+            'queue' => env('REDIS_QUEUE', 'default'),
+            'retry_after' => 90,
+            'block_for' => null,
+        ],
+
+    ],
+
+    /*
+    |--------------------------------------------------------------------------
+    | Failed Queue Jobs
+    |--------------------------------------------------------------------------
+    |
+    | These options configure the behavior of failed queue job logging so you
+    | can control which database and table are used to store the jobs that
+    | have failed. You may change them to any database / table you wish.
+    |
+    */
+
+    'failed' => [
+        'driver' => env('QUEUE_FAILED_DRIVER', 'database'),
+        'database' => env('DB_CONNECTION', 'mysql'),
+        'table' => 'failed_jobs',
+    ],
+
+];

+ 33 - 0
config/services.php

@@ -0,0 +1,33 @@
+<?php
+
+return [
+
+    /*
+    |--------------------------------------------------------------------------
+    | Third Party Services
+    |--------------------------------------------------------------------------
+    |
+    | This file is for storing the credentials for third party services such
+    | as Mailgun, Postmark, AWS and more. This file provides the de facto
+    | location for this type of information, allowing packages to have
+    | a conventional file to locate the various service credentials.
+    |
+    */
+
+    'mailgun' => [
+        'domain' => env('MAILGUN_DOMAIN'),
+        'secret' => env('MAILGUN_SECRET'),
+        'endpoint' => env('MAILGUN_ENDPOINT', 'api.mailgun.net'),
+    ],
+
+    'postmark' => [
+        'token' => env('POSTMARK_TOKEN'),
+    ],
+
+    'ses' => [
+        'key' => env('AWS_ACCESS_KEY_ID'),
+        'secret' => env('AWS_SECRET_ACCESS_KEY'),
+        'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
+    ],
+
+];

+ 199 - 0
config/session.php

@@ -0,0 +1,199 @@
+<?php
+
+use Illuminate\Support\Str;
+
+return [
+
+    /*
+    |--------------------------------------------------------------------------
+    | Default Session Driver
+    |--------------------------------------------------------------------------
+    |
+    | This option controls the default session "driver" that will be used on
+    | requests. By default, we will use the lightweight native driver but
+    | you may specify any of the other wonderful drivers provided here.
+    |
+    | Supported: "file", "cookie", "database", "apc",
+    |            "memcached", "redis", "dynamodb", "array"
+    |
+    */
+
+    'driver' => env('SESSION_DRIVER', 'file'),
+
+    /*
+    |--------------------------------------------------------------------------
+    | Session Lifetime
+    |--------------------------------------------------------------------------
+    |
+    | Here you may specify the number of minutes that you wish the session
+    | to be allowed to remain idle before it expires. If you want them
+    | to immediately expire on the browser closing, set that option.
+    |
+    */
+
+    'lifetime' => env('SESSION_LIFETIME', 120),
+
+    'expire_on_close' => false,
+
+    /*
+    |--------------------------------------------------------------------------
+    | Session Encryption
+    |--------------------------------------------------------------------------
+    |
+    | This option allows you to easily specify that all of your session data
+    | should be encrypted before it is stored. All encryption will be run
+    | automatically by Laravel and you can use the Session like normal.
+    |
+    */
+
+    'encrypt' => false,
+
+    /*
+    |--------------------------------------------------------------------------
+    | Session File Location
+    |--------------------------------------------------------------------------
+    |
+    | When using the native session driver, we need a location where session
+    | files may be stored. A default has been set for you but a different
+    | location may be specified. This is only needed for file sessions.
+    |
+    */
+
+    'files' => storage_path('framework/sessions'),
+
+    /*
+    |--------------------------------------------------------------------------
+    | Session Database Connection
+    |--------------------------------------------------------------------------
+    |
+    | When using the "database" or "redis" session drivers, you may specify a
+    | connection that should be used to manage these sessions. This should
+    | correspond to a connection in your database configuration options.
+    |
+    */
+
+    'connection' => env('SESSION_CONNECTION', null),
+
+    /*
+    |--------------------------------------------------------------------------
+    | Session Database Table
+    |--------------------------------------------------------------------------
+    |
+    | When using the "database" session driver, you may specify the table we
+    | should use to manage the sessions. Of course, a sensible default is
+    | provided for you; however, you are free to change this as needed.
+    |
+    */
+
+    'table' => 'sessions',
+
+    /*
+    |--------------------------------------------------------------------------
+    | Session Cache Store
+    |--------------------------------------------------------------------------
+    |
+    | When using the "apc", "memcached", or "dynamodb" session drivers you may
+    | list a cache store that should be used for these sessions. This value
+    | must match with one of the application's configured cache "stores".
+    |
+    */
+
+    'store' => env('SESSION_STORE', null),
+
+    /*
+    |--------------------------------------------------------------------------
+    | Session Sweeping Lottery
+    |--------------------------------------------------------------------------
+    |
+    | Some session drivers must manually sweep their storage location to get
+    | rid of old sessions from storage. Here are the chances that it will
+    | happen on a given request. By default, the odds are 2 out of 100.
+    |
+    */
+
+    'lottery' => [2, 100],
+
+    /*
+    |--------------------------------------------------------------------------
+    | Session Cookie Name
+    |--------------------------------------------------------------------------
+    |
+    | Here you may change the name of the cookie used to identify a session
+    | instance by ID. The name specified here will get used every time a
+    | new session cookie is created by the framework for every driver.
+    |
+    */
+
+    'cookie' => env(
+        'SESSION_COOKIE',
+        Str::slug(env('APP_NAME', 'laravel'), '_').'_session'
+    ),
+
+    /*
+    |--------------------------------------------------------------------------
+    | Session Cookie Path
+    |--------------------------------------------------------------------------
+    |
+    | The session cookie path determines the path for which the cookie will
+    | be regarded as available. Typically, this will be the root path of
+    | your application but you are free to change this when necessary.
+    |
+    */
+
+    'path' => '/',
+
+    /*
+    |--------------------------------------------------------------------------
+    | Session Cookie Domain
+    |--------------------------------------------------------------------------
+    |
+    | Here you may change the domain of the cookie used to identify a session
+    | in your application. This will determine which domains the cookie is
+    | available to in your application. A sensible default has been set.
+    |
+    */
+
+    'domain' => env('SESSION_DOMAIN', null),
+
+    /*
+    |--------------------------------------------------------------------------
+    | HTTPS Only Cookies
+    |--------------------------------------------------------------------------
+    |
+    | By setting this option to true, session cookies will only be sent back
+    | to the server if the browser has a HTTPS connection. This will keep
+    | the cookie from being sent to you if it can not be done securely.
+    |
+    */
+
+    'secure' => env('SESSION_SECURE_COOKIE'),
+
+    /*
+    |--------------------------------------------------------------------------
+    | HTTP Access Only
+    |--------------------------------------------------------------------------
+    |
+    | Setting this value to true will prevent JavaScript from accessing the
+    | value of the cookie and the cookie will only be accessible through
+    | the HTTP protocol. You are free to modify this option if needed.
+    |
+    */
+
+    'http_only' => true,
+
+    /*
+    |--------------------------------------------------------------------------
+    | Same-Site Cookies
+    |--------------------------------------------------------------------------
+    |
+    | This option determines how your cookies behave when cross-site requests
+    | take place, and can be used to mitigate CSRF attacks. By default, we
+    | will set this value to "lax" since this is a secure default value.
+    |
+    | Supported: "lax", "strict", "none", null
+    |
+    */
+
+    'same_site' => 'lax',
+
+];

+ 36 - 0
config/view.php

@@ -0,0 +1,36 @@
+<?php
+
+return [
+
+    /*
+    |--------------------------------------------------------------------------
+    | View Storage Paths
+    |--------------------------------------------------------------------------
+    |
+    | Most templating systems load templates from disk. Here you may specify
+    | an array of paths that should be checked for your views. Of course
+    | the usual Laravel view path has already been registered for you.
+    |
+    */
+
+    'paths' => [
+        resource_path('views'),
+    ],
+
+    /*
+    |--------------------------------------------------------------------------
+    | Compiled View Path
+    |--------------------------------------------------------------------------
+    |
+    | This option determines where all the compiled Blade templates will be
+    | stored for your application. Typically, this is within the storage
+    | directory. However, as usual, you are free to change this value.
+    |
+    */
+
+    'compiled' => env(
+        'VIEW_COMPILED_PATH',
+        realpath(storage_path('framework/views'))
+    ),
+
+];

+ 2 - 0
database/.gitignore

@@ -0,0 +1,2 @@
+*.sqlite
+*.sqlite-journal

+ 28 - 0
database/factories/UserFactory.php

@@ -0,0 +1,28 @@
+<?php
+
+/** @var \Illuminate\Database\Eloquent\Factory $factory */
+
+use App\User;
+use Faker\Generator as Faker;
+use Illuminate\Support\Str;
+
+/*
+|--------------------------------------------------------------------------
+| Model Factories
+|--------------------------------------------------------------------------
+|
+| This directory should contain each of the model factory definitions for
+| your application. Factories provide a convenient way to generate new
+| model instances for testing / seeding your application's database.
+|
+*/
+
+$factory->define(User::class, function (Faker $faker) {
+    return [
+        'name' => $faker->name,
+        'email' => $faker->unique()->safeEmail,
+        'email_verified_at' => now(),
+        'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
+        'remember_token' => Str::random(10),
+    ];
+});

+ 43 - 0
database/migrations/2020_06_05_165357_create_pre_chat_dialog_table.php

@@ -0,0 +1,43 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+class CreatePreChatDialogTable extends Migration
+{
+	/**
+	 * Run the migrations.
+	 *
+	 * @return void
+	 */
+	public function up()
+	{
+		Schema::create('chat_dialog', function(Blueprint $table)
+		{
+			$table->bigIncrements('id');
+			$table->string('user1', 100)->nullable()->default('')->index('IDEX_user1')->comment('用户名1(发起对话者)');
+			$table->string('user2', 100)->nullable()->default('')->index('IDEX_user2')->comment('用户名2');
+			$table->integer('unread1')->nullable()->default(0)->comment('用户1未读信息数');
+			$table->integer('unread2')->nullable()->default(0)->comment('用户2未读信息数');
+			$table->boolean('del1')->nullable()->default(0)->comment('用户1删除');
+			$table->boolean('del2')->nullable()->default(0)->comment('用户2删除');
+			$table->bigInteger('lastid1')->nullable()->default(0)->comment('用户1删除到的聊天ID');
+			$table->bigInteger('lastid2')->nullable()->default(0)->comment('用户2删除到的聊天ID');
+			$table->text('lasttext')->nullable()->comment('最后消息');
+			$table->bigInteger('lastdate')->nullable()->default(0)->comment('最后消息时间');
+			$table->bigInteger('indate')->nullable()->default(0)->comment('数据生成时间');
+			$table->unique(['user1','user2'], 'IDEX_user1_user2');
+		});
+	}
+
+	/**
+	 * Reverse the migrations.
+	 *
+	 * @return void
+	 */
+	public function down()
+	{
+		Schema::drop('chat_dialog');
+	}
+}

+ 37 - 0
database/migrations/2020_06_05_165357_create_pre_chat_msg_table.php

@@ -0,0 +1,37 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+class CreatePreChatMsgTable extends Migration
+{
+	/**
+	 * Run the migrations.
+	 *
+	 * @return void
+	 */
+	public function up()
+	{
+		Schema::create('chat_msg', function(Blueprint $table)
+		{
+			$table->bigIncrements('id');
+			$table->integer('did')->nullable()->default(0)->index('IDEX_did')->comment('对话ID');
+			$table->string('username', 100)->nullable()->default('')->index('IDEX_username')->comment('发送者');
+			$table->string('receive', 100)->nullable()->default('')->index('IDEX_receive')->comment('接受者');
+			$table->text('message')->nullable()->comment('详细内容');
+			$table->boolean('roger')->nullable()->default(0)->comment('是否已读');
+			$table->bigInteger('indate')->nullable()->default(0);
+		});
+	}
+
+	/**
+	 * Reverse the migrations.
+	 *
+	 * @return void
+	 */
+	public function down()
+	{
+		Schema::drop('chat_msg');
+	}
+}

+ 34 - 0
database/migrations/2020_06_05_165357_create_pre_docs_book_table.php

@@ -0,0 +1,34 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+class CreatePreDocsBookTable extends Migration
+{
+	/**
+	 * Run the migrations.
+	 *
+	 * @return void
+	 */
+	public function up()
+	{
+		Schema::create('docs_book', function(Blueprint $table)
+		{
+			$table->increments('id');
+			$table->string('username', 100)->nullable()->default('');
+			$table->string('title', 100)->nullable()->default('');
+			$table->bigInteger('indate')->nullable()->default(0);
+		});
+	}
+
+	/**
+	 * Reverse the migrations.
+	 *
+	 * @return void
+	 */
+	public function down()
+	{
+		Schema::drop('docs_book');
+	}
+}

+ 36 - 0
database/migrations/2020_06_05_165357_create_pre_docs_content_table.php

@@ -0,0 +1,36 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+class CreatePreDocsContentTable extends Migration
+{
+	/**
+	 * Run the migrations.
+	 *
+	 * @return void
+	 */
+	public function up()
+	{
+		Schema::create('docs_content', function(Blueprint $table)
+		{
+			$table->increments('id');
+			$table->integer('bookid')->nullable()->default(0)->comment('知识库ID');
+			$table->integer('sid')->nullable()->default(0)->index('IDEX_sid')->comment('章节ID');
+			$table->longtext('content')->nullable()->comment('内容');
+			$table->string('username', 100)->nullable()->default('');
+			$table->bigInteger('indate')->nullable()->default(0);
+		});
+	}
+
+	/**
+	 * Reverse the migrations.
+	 *
+	 * @return void
+	 */
+	public function down()
+	{
+		Schema::drop('docs_content');
+	}
+}

+ 38 - 0
database/migrations/2020_06_05_165357_create_pre_docs_section_table.php

@@ -0,0 +1,38 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+class CreatePreDocsSectionTable extends Migration
+{
+	/**
+	 * Run the migrations.
+	 *
+	 * @return void
+	 */
+	public function up()
+	{
+		Schema::create('docs_section', function(Blueprint $table)
+		{
+			$table->increments('id');
+			$table->integer('bookid')->nullable()->default(0)->comment('知识库数据ID');
+			$table->integer('parentid')->nullable()->default(0)->comment('上级数据ID');
+			$table->string('username', 100)->nullable()->default('');
+			$table->string('title', 100)->nullable()->default('');
+			$table->string('type', 100)->nullable()->default('');
+			$table->integer('inorder')->nullable()->default(0)->comment('排序(DESC)');
+			$table->bigInteger('indate')->nullable()->default(0);
+		});
+	}
+
+	/**
+	 * Reverse the migrations.
+	 *
+	 * @return void
+	 */
+	public function down()
+	{
+		Schema::drop('docs_section');
+	}
+}

+ 43 - 0
database/migrations/2020_06_05_165357_create_pre_project_files_table.php

@@ -0,0 +1,43 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+class CreatePreProjectFilesTable extends Migration
+{
+	/**
+	 * Run the migrations.
+	 *
+	 * @return void
+	 */
+	public function up()
+	{
+		Schema::create('project_files', function(Blueprint $table)
+		{
+			$table->increments('id');
+			$table->integer('projectid')->nullable()->default(0)->comment('项目ID');
+			$table->integer('taskid')->nullable()->default(0)->comment('任务ID');
+			$table->string('name', 100)->nullable()->default('')->comment('文件名称');
+			$table->integer('size')->nullable()->default(0)->comment('文件大小(B)');
+			$table->string('ext', 20)->nullable()->default('')->comment('文件格式');
+			$table->string('path')->nullable()->default('')->comment('文件地址');
+			$table->string('thumb')->nullable()->default('')->comment('缩略图');
+			$table->string('username')->nullable()->default('')->comment('上传用户');
+			$table->integer('download')->nullable()->default(0)->comment('下载次数');
+			$table->boolean('delete')->nullable()->default(0)->comment('是否删除');
+			$table->bigInteger('deletedate')->nullable()->default(0)->comment('删除时间');
+			$table->bigInteger('indate')->nullable()->default(0);
+		});
+	}
+
+	/**
+	 * Reverse the migrations.
+	 *
+	 * @return void
+	 */
+	public function down()
+	{
+		Schema::drop('project_files');
+	}
+}

+ 34 - 0
database/migrations/2020_06_05_165357_create_pre_project_label_table.php

@@ -0,0 +1,34 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+class CreatePreProjectLabelTable extends Migration
+{
+	/**
+	 * Run the migrations.
+	 *
+	 * @return void
+	 */
+	public function up()
+	{
+		Schema::create('project_label', function(Blueprint $table)
+		{
+			$table->increments('id');
+			$table->integer('projectid')->nullable()->default(0)->comment('项目ID');
+			$table->string('title', 100)->nullable()->default('')->comment('分类名称');
+			$table->integer('inorder')->nullable()->comment('排序(ASC)');
+		});
+	}
+
+	/**
+	 * Reverse the migrations.
+	 *
+	 * @return void
+	 */
+	public function down()
+	{
+		Schema::drop('project_label');
+	}
+}

+ 39 - 0
database/migrations/2020_06_05_165357_create_pre_project_lists_table.php

@@ -0,0 +1,39 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+class CreatePreProjectListsTable extends Migration
+{
+	/**
+	 * Run the migrations.
+	 *
+	 * @return void
+	 */
+	public function up()
+	{
+		Schema::create('project_lists', function(Blueprint $table)
+		{
+			$table->increments('id');
+			$table->string('title')->nullable()->default('')->comment('项目名称');
+			$table->string('createuser', 100)->nullable()->default('')->comment('项目创建者用户名');
+			$table->string('username', 100)->nullable()->default('')->comment('项目所有者用户名');
+			$table->integer('complete')->nullable()->default(0)->comment('已完成数量');
+			$table->integer('unfinished')->nullable()->default(0)->comment('未完成数量');
+			$table->bigInteger('indate')->nullable()->default(0)->comment('添加时间');
+			$table->boolean('delete')->nullable()->default(0)->index('IDEX_delete')->comment('是否删除');
+			$table->bigInteger('deletedate')->nullable()->default(0)->comment('删除时间');
+		});
+	}
+
+	/**
+	 * Reverse the migrations.
+	 *
+	 * @return void
+	 */
+	public function down()
+	{
+		Schema::drop('project_lists');
+	}
+}

+ 38 - 0
database/migrations/2020_06_05_165357_create_pre_project_log_table.php

@@ -0,0 +1,38 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+class CreatePreProjectLogTable extends Migration
+{
+	/**
+	 * Run the migrations.
+	 *
+	 * @return void
+	 */
+	public function up()
+	{
+		Schema::create('project_log', function(Blueprint $table)
+		{
+			$table->increments('id');
+			$table->string('type', 50)->nullable()->default('')->comment('类型:评论、日志');
+			$table->integer('projectid')->nullable()->default(0)->comment('项目ID');
+			$table->integer('taskid')->nullable()->default(0)->comment('相关数据ID');
+			$table->string('username', 100)->nullable()->default('')->comment('关系用户名');
+			$table->string('detail', 500)->nullable()->default('')->comment('详细信息');
+			$table->bigInteger('indate')->nullable()->default(0)->comment('添加时间');
+			$table->text('other')->nullable()->comment('其他参数');
+		});
+	}
+
+	/**
+	 * Reverse the migrations.
+	 *
+	 * @return void
+	 */
+	public function down()
+	{
+		Schema::drop('project_log');
+	}
+}

+ 53 - 0
database/migrations/2020_06_05_165357_create_pre_project_task_table.php

@@ -0,0 +1,53 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+class CreatePreProjectTaskTable extends Migration
+{
+	/**
+	 * Run the migrations.
+	 *
+	 * @return void
+	 */
+	public function up()
+	{
+		Schema::create('project_task', function(Blueprint $table)
+		{
+			$table->increments('id');
+			$table->integer('projectid')->nullable()->default(0)->index('IDEX_projectid')->comment('项目ID');
+			$table->integer('labelid')->nullable()->default(0)->comment('项目子分类ID');
+			$table->string('createuser', 100)->nullable()->default('')->comment('创建者用户名');
+			$table->string('username', 100)->nullable()->default('')->comment('负责人用户名');
+			$table->string('title')->nullable()->default('')->comment('标题');
+			$table->string('desc', 500)->nullable()->default('')->comment('描述');
+			$table->boolean('level')->nullable()->default(1)->comment('优先级别:1~4');
+			$table->boolean('complete')->nullable()->default(0)->index('IDEX_status')->comment('是否完成:0|1');
+			$table->bigInteger('completedate')->nullable()->default(0)->comment('完成时间');
+			$table->text('subtask')->nullable()->comment('子任务列表');
+			$table->text('follower')->nullable()->comment('关注人列表');
+			$table->integer('pushlid')->nullable()->default(0)->comment('已发送的最后动态ID');
+			$table->integer('filenum')->nullable()->default(0)->comment('附件数量');
+			$table->bigInteger('startdate')->nullable()->default(0)->comment('计划开始时间');
+			$table->bigInteger('enddate')->nullable()->default(0)->comment('计划结束时间');
+			$table->tinyInteger('archived')->nullable()->default(0)->comment('是否归档');
+			$table->bigInteger('archiveddate')->nullable()->default(0)->comment('归档时间');
+			$table->boolean('delete')->nullable()->default(0)->index('IDEX_delete')->comment('是否删除');
+			$table->bigInteger('deletedate')->nullable()->default(0)->comment('删除时间');
+			$table->integer('inorder')->nullable()->default(0)->comment('排序(DESC)');
+			$table->integer('userorder')->nullable()->default(0)->comment('会员自己的排序(DESC)');
+			$table->bigInteger('indate')->nullable()->default(0)->comment('添加时间');
+		});
+	}
+
+	/**
+	 * Reverse the migrations.
+	 *
+	 * @return void
+	 */
+	public function down()
+	{
+		Schema::drop('project_task');
+	}
+}

+ 37 - 0
database/migrations/2020_06_05_165357_create_pre_project_users_table.php

@@ -0,0 +1,37 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+class CreatePreProjectUsersTable extends Migration
+{
+	/**
+	 * Run the migrations.
+	 *
+	 * @return void
+	 */
+	public function up()
+	{
+		Schema::create('project_users', function(Blueprint $table)
+		{
+			$table->increments('id');
+			$table->string('type', 50)->nullable()->default('')->index('IDEX_type')->comment('类型:成员、收藏、关注');
+			$table->integer('projectid')->nullable()->default(0)->index('IDEX_projectid')->comment('项目ID');
+			$table->integer('taskid')->nullable()->default(0)->index('IDEX_taskid')->comment('任务ID');
+			$table->boolean('isowner')->nullable()->default(0)->comment('是否项目所有者');
+			$table->string('username', 100)->nullable()->default('')->index('IDEX_username')->comment('关系用户名');
+			$table->bigInteger('indate')->nullable()->default(0)->comment('添加时间');
+		});
+	}
+
+	/**
+	 * Reverse the migrations.
+	 *
+	 * @return void
+	 */
+	public function down()
+	{
+		Schema::drop('project_users');
+	}
+}

+ 35 - 0
database/migrations/2020_06_05_165357_create_pre_report_ccuser_table.php

@@ -0,0 +1,35 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+class CreatePreReportCcuserTable extends Migration
+{
+	/**
+	 * Run the migrations.
+	 *
+	 * @return void
+	 */
+	public function up()
+	{
+		Schema::create('report_ccuser', function(Blueprint $table)
+		{
+			$table->increments('id');
+			$table->integer('rid')->nullable()->default(0)->comment('汇报ID');
+			$table->string('username', 100)->nullable()->default('');
+			$table->boolean('cc')->nullable()->default(0)->comment('是否生效');
+			$table->unique(['rid','username'], 'IDEX_rid_username');
+		});
+	}
+
+	/**
+	 * Reverse the migrations.
+	 *
+	 * @return void
+	 */
+	public function down()
+	{
+		Schema::drop('report_ccuser');
+	}
+}

+ 33 - 0
database/migrations/2020_06_05_165357_create_pre_report_content_table.php

@@ -0,0 +1,33 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+class CreatePreReportContentTable extends Migration
+{
+	/**
+	 * Run the migrations.
+	 *
+	 * @return void
+	 */
+	public function up()
+	{
+		Schema::create('report_content', function(Blueprint $table)
+		{
+			$table->increments('id');
+			$table->integer('rid')->nullable()->default(0)->unique('IDEX_rid')->comment('汇报ID');
+			$table->text('content')->nullable()->comment('内容');
+		});
+	}
+
+	/**
+	 * Reverse the migrations.
+	 *
+	 * @return void
+	 */
+	public function down()
+	{
+		Schema::drop('report_content');
+	}
+}

+ 39 - 0
database/migrations/2020_06_05_165357_create_pre_report_lists_table.php

@@ -0,0 +1,39 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+class CreatePreReportListsTable extends Migration
+{
+	/**
+	 * Run the migrations.
+	 *
+	 * @return void
+	 */
+	public function up()
+	{
+		Schema::create('report_lists', function(Blueprint $table)
+		{
+			$table->increments('id');
+			$table->string('username', 100)->nullable()->default('');
+			$table->string('title', 100)->nullable()->default('');
+			$table->string('type', 100)->nullable()->default('');
+			$table->string('status', 100)->nullable()->default('');
+			$table->text('ccuser')->nullable()->comment('抄送人');
+			$table->string('date', 20)->nullable()->default('')->comment('日期');
+			$table->bigInteger('indate')->nullable()->default(0);
+			$table->unique(['username','type','date'], 'IDEX_username_type_date');
+		});
+	}
+
+	/**
+	 * Reverse the migrations.
+	 *
+	 * @return void
+	 */
+	public function down()
+	{
+		Schema::drop('report_lists');
+	}
+}

+ 34 - 0
database/migrations/2020_06_05_165357_create_pre_setting_table.php

@@ -0,0 +1,34 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+class CreatePreSettingTable extends Migration
+{
+	/**
+	 * Run the migrations.
+	 *
+	 * @return void
+	 */
+	public function up()
+	{
+		Schema::create('setting', function(Blueprint $table)
+		{
+			$table->increments('id');
+			$table->string('title', 100)->nullable()->default('')->index('IDEX_TITLE');
+			$table->string('desc')->nullable()->default('')->comment('参数描述、备注');
+			$table->longText('setting')->nullable();
+		});
+	}
+
+	/**
+	 * Reverse the migrations.
+	 *
+	 * @return void
+	 */
+	public function down()
+	{
+		Schema::drop('setting');
+	}
+}

+ 0 - 0
database/migrations/2020_06_05_165357_create_pre_users_table.php


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