wanghongkai 6 лет назад
Сommit
081e0e3b51
100 измененных файлов с 6581 добавлено и 0 удалено
  1. 4 0
      .gitignore
  2. 674 0
      LICENSE
  3. 139 0
      README.md
  4. 130 0
      app.sh
  5. 37 0
      config.yml
  6. 211 0
      pom.xml
  7. 26 0
      src/main/java/com/moekr/moocoder/Application.java
  8. 49 0
      src/main/java/com/moekr/moocoder/data/converter/FailureSetConverter.java
  9. 29 0
      src/main/java/com/moekr/moocoder/data/converter/StringListConverter.java
  10. 29 0
      src/main/java/com/moekr/moocoder/data/converter/StringSetConverter.java
  11. 17 0
      src/main/java/com/moekr/moocoder/data/dao/CommitDAO.java
  12. 20 0
      src/main/java/com/moekr/moocoder/data/dao/CourseDAO.java
  13. 189 0
      src/main/java/com/moekr/moocoder/data/dao/ExamDAO.java
  14. 22 0
      src/main/java/com/moekr/moocoder/data/dao/ProblemDAO.java
  15. 59 0
      src/main/java/com/moekr/moocoder/data/dao/RecordDAO.java
  16. 26 0
      src/main/java/com/moekr/moocoder/data/dao/ResultDAO.java
  17. 22 0
      src/main/java/com/moekr/moocoder/data/dao/UserDAO.java
  18. 52 0
      src/main/java/com/moekr/moocoder/data/entity/Commit.java
  19. 47 0
      src/main/java/com/moekr/moocoder/data/entity/Course.java
  20. 82 0
      src/main/java/com/moekr/moocoder/data/entity/Exam.java
  21. 96 0
      src/main/java/com/moekr/moocoder/data/entity/Problem.java
  22. 60 0
      src/main/java/com/moekr/moocoder/data/entity/Record.java
  23. 48 0
      src/main/java/com/moekr/moocoder/data/entity/Result.java
  24. 78 0
      src/main/java/com/moekr/moocoder/data/entity/User.java
  25. 30 0
      src/main/java/com/moekr/moocoder/logic/AsyncWrapper.java
  26. 35 0
      src/main/java/com/moekr/moocoder/logic/ExecutorConfiguration.java
  27. 38 0
      src/main/java/com/moekr/moocoder/logic/TaskScheduler.java
  28. 5 0
      src/main/java/com/moekr/moocoder/logic/api/DockerApi.java
  29. 67 0
      src/main/java/com/moekr/moocoder/logic/api/GitlabApi.java
  30. 18 0
      src/main/java/com/moekr/moocoder/logic/api/JenkinsApi.java
  31. 45 0
      src/main/java/com/moekr/moocoder/logic/api/impl/DockerApiImpl.java
  32. 171 0
      src/main/java/com/moekr/moocoder/logic/api/impl/GitlabApiImpl.java
  33. 160 0
      src/main/java/com/moekr/moocoder/logic/api/impl/JenkinsApiImpl.java
  34. 38 0
      src/main/java/com/moekr/moocoder/logic/api/impl/PoolingJenkinsHttpClient.java
  35. 18 0
      src/main/java/com/moekr/moocoder/logic/api/vo/BuildDetails.java
  36. 27 0
      src/main/java/com/moekr/moocoder/logic/api/vo/CoverageResult.java
  37. 11 0
      src/main/java/com/moekr/moocoder/logic/api/vo/CoverageResultElement.java
  38. 10 0
      src/main/java/com/moekr/moocoder/logic/api/vo/GitlabUser.java
  39. 9 0
      src/main/java/com/moekr/moocoder/logic/api/vo/MutationResult.java
  40. 10 0
      src/main/java/com/moekr/moocoder/logic/service/CommitService.java
  41. 46 0
      src/main/java/com/moekr/moocoder/logic/service/CourseService.java
  42. 27 0
      src/main/java/com/moekr/moocoder/logic/service/ExamService.java
  43. 7 0
      src/main/java/com/moekr/moocoder/logic/service/MailService.java
  44. 7 0
      src/main/java/com/moekr/moocoder/logic/service/NotifyService.java
  45. 17 0
      src/main/java/com/moekr/moocoder/logic/service/OnlineCodeService.java
  46. 21 0
      src/main/java/com/moekr/moocoder/logic/service/ProblemService.java
  47. 10 0
      src/main/java/com/moekr/moocoder/logic/service/RecordService.java
  48. 46 0
      src/main/java/com/moekr/moocoder/logic/service/ResultService.java
  49. 7 0
      src/main/java/com/moekr/moocoder/logic/service/StatisticService.java
  50. 26 0
      src/main/java/com/moekr/moocoder/logic/service/UserService.java
  51. 123 0
      src/main/java/com/moekr/moocoder/logic/service/impl/BuildManager.java
  52. 61 0
      src/main/java/com/moekr/moocoder/logic/service/impl/ClosedBuildChecker.java
  53. 57 0
      src/main/java/com/moekr/moocoder/logic/service/impl/ClosedExamChecker.java
  54. 121 0
      src/main/java/com/moekr/moocoder/logic/service/impl/CommitManager.java
  55. 40 0
      src/main/java/com/moekr/moocoder/logic/service/impl/CommitServiceImpl.java
  56. 109 0
      src/main/java/com/moekr/moocoder/logic/service/impl/CourseServiceImpl.java
  57. 62 0
      src/main/java/com/moekr/moocoder/logic/service/impl/DockerImageBuilder.java
  58. 77 0
      src/main/java/com/moekr/moocoder/logic/service/impl/ExamPaperBuilder.java
  59. 238 0
      src/main/java/com/moekr/moocoder/logic/service/impl/ExamServiceImpl.java
  60. 50 0
      src/main/java/com/moekr/moocoder/logic/service/impl/GitHelper.java
  61. 72 0
      src/main/java/com/moekr/moocoder/logic/service/impl/MailServiceImpl.java
  62. 58 0
      src/main/java/com/moekr/moocoder/logic/service/impl/NotifyServiceImpl.java
  63. 262 0
      src/main/java/com/moekr/moocoder/logic/service/impl/OnlineCodeServiceImpl.java
  64. 151 0
      src/main/java/com/moekr/moocoder/logic/service/impl/ProblemFormatter.java
  65. 154 0
      src/main/java/com/moekr/moocoder/logic/service/impl/ProblemServiceImpl.java
  66. 46 0
      src/main/java/com/moekr/moocoder/logic/service/impl/ProblemUpdater.java
  67. 40 0
      src/main/java/com/moekr/moocoder/logic/service/impl/RecordServiceImpl.java
  68. 146 0
      src/main/java/com/moekr/moocoder/logic/service/impl/ResultServiceImpl.java
  69. 38 0
      src/main/java/com/moekr/moocoder/logic/service/impl/StatisticServiceImpl.java
  70. 179 0
      src/main/java/com/moekr/moocoder/logic/service/impl/UserServiceImpl.java
  71. 12 0
      src/main/java/com/moekr/moocoder/logic/storage/StorageProvider.java
  72. 45 0
      src/main/java/com/moekr/moocoder/logic/storage/StorageProviderConfiguration.java
  73. 78 0
      src/main/java/com/moekr/moocoder/logic/storage/impl/CosStorageProvider.java
  74. 43 0
      src/main/java/com/moekr/moocoder/logic/storage/impl/LocalStorageProvider.java
  75. 78 0
      src/main/java/com/moekr/moocoder/logic/storage/impl/OssStorageProvider.java
  76. 112 0
      src/main/java/com/moekr/moocoder/logic/storage/impl/QiniuStorageProvider.java
  77. 50 0
      src/main/java/com/moekr/moocoder/logic/vo/CodeVO.java
  78. 89 0
      src/main/java/com/moekr/moocoder/logic/vo/CommitVO.java
  79. 28 0
      src/main/java/com/moekr/moocoder/logic/vo/CourseVO.java
  80. 62 0
      src/main/java/com/moekr/moocoder/logic/vo/ExamVO.java
  81. 70 0
      src/main/java/com/moekr/moocoder/logic/vo/JoinedExamVO.java
  82. 56 0
      src/main/java/com/moekr/moocoder/logic/vo/ProblemVO.java
  83. 77 0
      src/main/java/com/moekr/moocoder/logic/vo/RecordVO.java
  84. 90 0
      src/main/java/com/moekr/moocoder/logic/vo/ResultVO.java
  85. 14 0
      src/main/java/com/moekr/moocoder/logic/vo/StatisticVO.java
  86. 34 0
      src/main/java/com/moekr/moocoder/logic/vo/UserVO.java
  87. 22 0
      src/main/java/com/moekr/moocoder/logic/vo/internal/ExamTestCaseVO.java
  88. 116 0
      src/main/java/com/moekr/moocoder/logic/vo/internal/ResultDetailVO.java
  89. 88 0
      src/main/java/com/moekr/moocoder/util/ApplicationProperties.java
  90. 6 0
      src/main/java/com/moekr/moocoder/util/Method.java
  91. 35 0
      src/main/java/com/moekr/moocoder/util/ToolKit.java
  92. 26 0
      src/main/java/com/moekr/moocoder/util/editors/DefaultNumberEditor.java
  93. 18 0
      src/main/java/com/moekr/moocoder/util/editors/PageNumberEditor.java
  94. 22 0
      src/main/java/com/moekr/moocoder/util/editors/RangeNumberEditor.java
  95. 28 0
      src/main/java/com/moekr/moocoder/util/enums/BuildStatus.java
  96. 34 0
      src/main/java/com/moekr/moocoder/util/enums/ExamStatus.java
  97. 16 0
      src/main/java/com/moekr/moocoder/util/enums/FileType.java
  98. 58 0
      src/main/java/com/moekr/moocoder/util/enums/ProblemType.java
  99. 23 0
      src/main/java/com/moekr/moocoder/util/enums/UserRole.java
  100. 15 0
      src/main/java/com/moekr/moocoder/util/exceptions/AccessDeniedException.java

+ 4 - 0
.gitignore

@@ -0,0 +1,4 @@
+.idea
+target
+*.iml
+config-dev.yml

+ 674 - 0
LICENSE

@@ -0,0 +1,674 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+the GNU General Public License is 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.  We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors.  You can apply it to
+your programs, too.
+
+  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.
+
+  To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights.  Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received.  You must make sure that they, too, receive
+or can get the source code.  And you must show them these terms so they
+know their rights.
+
+  Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+  For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software.  For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+  Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so.  This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software.  The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable.  Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products.  If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+  Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary.  To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+  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 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. Use with the GNU Affero General Public License.
+
+  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 Affero 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 special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU 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 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 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 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 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 General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+    <program>  Copyright (C) <year>  <name of author>
+    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+  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 GPL, see
+<http://www.gnu.org/licenses/>.
+
+  The GNU General Public License does not permit incorporating your program
+into proprietary programs.  If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.  But first, please read
+<http://www.gnu.org/philosophy/why-not-lgpl.html>.

+ 139 - 0
README.md

@@ -0,0 +1,139 @@
+MOOCODER Backend
+===========================
+基于GitLab、Jenkins与Docker的自动化编程考试系统后端部分
+
+### 主要架构
+
+![主要架构](https://pic.moekr.com/2018/05/997939A6D068F036A0E8850D2BF25D1E.png)
+
+### 特性
+
+- [x] 使用通用的Git协议
+- [x] 自动化测试,实时展示成绩
+- [x] 完全隐藏GitLab和Jenkins
+- [x] Docker中运行测试,隔离网络
+- [x] CPU、内存资源限制
+- [ ] LDAP统一认证
+- [ ] 分布式与GitLab集群
+- [ ] WebIDE支持
+
+### 支持题型
+
+- [x] Python
+- [x] Python覆盖率测试
+- [x] Java
+- [x] Java覆盖率测试
+- [x] Java变异覆盖测试
+
+### 编译
+
+1. 编译[前端项目](https://github.com/Moekr/moocoder-frontend)
+2. 对前端项目进行[WebJars封装](https://github.com/Moekr/moocoder-frontend-webjars)
+3. 编译本项目
+
+### 依赖
+
+> 推荐使用Linux操作系统并在宿主机安装docker,所有依赖实例除NGINX外都使用docker运行,其中Jenkins实例推荐将宿主机的docker二进制文件和docker unix socket映射至容器中
+
+* Docker支持
+* 邮件发送支持
+* 一个GitLab实例
+    ```
+    docker run -d --name moocoder-gitlab -p 8081:80 --restart always -v /srv/moocoder/gitlab/config:/etc/gitlab:Z -v /srv/moocoder/gitlab/log:/var/log/gitlab:Z -v /srv/moocoder/gitlab/data:/var/opt/gitlab:Z gitlab/gitlab-ce:10.8.0-ce.0
+    ```
+* 一个Jenkins实例
+    ```
+    docker run -d --name moocoder-jenkins -p 8082:8080 --restart always -v /srv/moocoder/jenkins:/var/jenkins_home:Z -v /usr/bin/docker:/usr/bin/docker:Z -v /var/run/docker.sock:/var/run/docker.sock:Z -u root jenkins/jenkins:2.124
+    ```
+    
+    注意:最新版本的Jenkins官方Docker镜像缺少docker二进制文件所依赖的运行库libltdl,需要使用`apt install libltdl-dev`进行安装
+    
+    其他:由于Jenkins Workspace中的文件均为临时性文件,当磁盘负担较重时可以使用`-v /dev/shm/jenkins:/var/jenkins_home/workspace:Z`来使用内存进行加速
+    
+* 一个Docker Registry实例
+    ```
+    docker run -d --name moocoder-registry -p 5000:5000 --restart always -v /srv/moocoder/registry:/var/lib/registry:Z library/registry:2.6.2
+    ```
+* 一个NGINX实例
+
+### 配置
+
+1. 选择一个随机字符串作为内部的Secret
+2. 配置GitLab
+    * 为管理员用户生成一个至少包含api与sudo权限的Token
+    * 为GitLab自带的NGINX增加一个反向代理服务,用于为WebHook请求增加`X-Moocoder-Secret`头
+        * 修改GitLab的配置文件,示例:
+            ```diff
+              # nginx['custom_gitlab_server_config'] = "location ^~ /foo-namespace/bar-project/raw/ {\n deny all;\n}\n"
+            - # nginx['custom_nginx_config'] = "include /etc/nginx/conf.d/example.conf;"
+            + nginx['custom_nginx_config'] = "include /etc/gitlab/webhook-proxy.conf;"
+              # nginx['proxy_read_timeout'] = 3600
+            ```
+        * 增加新的NGINX配置文件,示例:
+            ```
+            server {
+            	listen 3000 default_server;
+            	listen [::]:3000 default_server;
+            	server_name _;
+            	
+            	location / {
+            		proxy_pass http://backend_host:8090;
+            		
+            		proxy_set_header X-Moocoder-Secret backend_secret;
+            	}
+            }
+            ```
+        * 勾选系统设置中的`Allow requests to the local network from hooks and services`
+3. 配置Jenkins
+    * 增加全局变量`MOOCODER_HOST`和`MOOCODER_SECRET`
+    * 增加一个ID为`MOOCODER`的Credential,内容为GitLab管理员用户的账号密码
+    * 设置合适的Executor数量
+    * 安装Cobertura插件,用于测试覆盖率报告的获取
+4. 配置Docker
+    * 设置Docker Daemon信任启动的Registry实例为insecure的
+    * 如果Docker Daemon与后端主程序不在同一台设备上,需要设置Docker暴露TCP通信端口
+5. 配置NGINX,主要用于拦截部分外部请求和分流Git请求,示例:
+    ```
+    server {
+        listen 80 default_server;
+        listen [::]:80 default_server;
+        server_name _;
+    
+        location ~ .*\.(js|css|ico|woff2)$ {
+            include /etc/nginx/proxy_params;
+            
+            proxy_pass http://localhost:8090;
+            
+            proxy_cache moocoder;
+            expires 7d;
+    
+            add_header Cache "$upstream_cache_status";
+            proxy_ignore_headers X-Accel-Expires Expires Cache-Control Set-Cookie;
+            proxy_hide_header Pragma;
+    
+            proxy_cache_valid 200 304 301 302 8h;
+            proxy_cache_valid 404 1m;
+            proxy_cache_valid any 2d;
+        }
+    
+        location ~ /internal/ {
+            return 403;
+        }
+    
+        location ~ / {
+            include /etc/nginx/proxy_params;
+    
+            if ($request_uri ~* (info/refs|git-upload-pack|git-receive-pack)) {
+                proxy_pass http://localhost:8081;
+            }
+    
+            proxy_pass http://localhost:8090;
+        }
+    }
+    ```
+6. 配置好后端项目config.yml中的各项参数
+7. (可选)配置防火墙,拦截对部分端口的访问
+
+### 协议
+
+GPLv3

+ 130 - 0
app.sh

@@ -0,0 +1,130 @@
+#!/usr/bin/env bash
+
+# Application root directory.
+path=/srv/moocoder
+# Application username.
+name=moocoder-backend
+# Application version.
+version=0.4.5
+
+# Application config file. Default config.yml
+config_file=${path}/config.yml
+# File which store application process PID. Default proc.pid
+pid_file=${path}/proc.pid
+# Directory which store application log. Default ${path}/log
+log_dir=${path}/log
+# Name template of log files (Parsed with shell command "date"). Default %Y-%m-%d_%H-%M-%S.log
+log_name=%Y-%m-%d_%H-%M-%S.log
+# Time in second how long "stop" and "force-stop" wait for application totally stop. Default 60
+wait_count=60
+
+# JVM default time zone. Default GMT+8
+time_zone="GMT+8"
+
+function start {
+    if [ -f ${pid_file} ]
+    then
+        local pid=$(cat ${pid_file})
+        if ps -p ${pid} > /dev/null
+        then
+            echo "Application ${name} is already running! (Version:${version} PID:${pid})"
+        else
+            rm -rf ${pid_file}
+        fi
+    fi
+    if [ ! -f ${pid_file} ]
+    then
+        if [ ! -d ${log_dir} ]
+        then
+            mkdir ${log_dir}
+        fi
+        java -server -jar ${path}/${name}-${version}.jar --spring.config.additional-location=${config_file} -Duser.timezone=${time_zone} &>${log_dir}/$(date +${log_name}) &
+        local pid=$(echo -e "$!\c")
+        echo -e "${pid}\c" > ${pid_file}
+        echo "Start application ${name} successfully! (Version:${version} PID:${pid})"
+    fi
+}
+
+function stop {
+    if [ -f ${pid_file} ]
+    then
+        if ! ps -p $(cat ${pid_file}) > /dev/null
+        then
+            rm -rf ${pid_file}
+        fi
+    fi
+    if [ -f ${pid_file} ]
+    then
+        local pid=$(cat ${pid_file})
+        kill ${pid}
+        local count=${wait_count}
+        while((${count} > 0))
+        do
+            sleep 1
+            if ! ps -p ${pid} > /dev/null
+            then
+                rm -rf ${pid_file}
+                echo "Stop application ${name} successfully! (Version:${version})"
+                break
+            fi
+            count=`expr ${count} - 1`
+        done
+        if [ ${count} -eq 0 ]
+        then
+            echo "Failed to stop application ${name}! (Version:${version} PID:${pid})"
+            exit 1
+        fi
+    else
+        echo "Application ${name} is not running! (Version:${version})"
+    fi
+}
+
+function restart {
+    stop
+    start
+}
+
+function status {
+    if [ -f ${pid_file} ]
+    then
+        if ! ps -p $(cat ${pid_file}) > /dev/null
+        then
+            rm -rf ${pid_file}
+        fi
+    fi
+    if [ -f ${pid_file} ]
+    then
+        local pid=$(cat ${pid_file})
+        echo "Application ${name} is running! (Version:${version} PID:${pid})"
+    else
+        echo "Application ${name} is not running! (Version:${version})"
+    fi
+}
+
+function usage {
+    echo "Usage: $0 {start|stop|restart|status}"
+}
+
+pushd ${path} > /dev/null
+
+case $1 in
+start)
+    start
+;;
+stop)
+    stop
+;;
+restart)
+    restart
+;;
+status)
+    status
+;;
+*)
+    usage
+;;
+esac
+
+popd > /dev/null
+
+exit 0

+ 37 - 0
config.yml

@@ -0,0 +1,37 @@
+server:
+  port: 8080
+spring:
+  datasource:
+    url: jdbc:mysql://10.1.1.173:3306/moocoder?useUnicode=true&characterEncoding=UTF-8&useSSL=false
+    username: root
+    password: NJU67mysql
+  jpa:
+    database-platform: org.hibernate.dialect.MySQL57InnoDBDialect
+  security:
+    user:
+      name: root
+      password: NJU67software
+  mail:
+    host: smtp.yandex.com
+    username: noreply@moocoder.cn
+    password: 1143757638qaz
+
+moocoder:
+  secret: 1qaz2wsx
+  gitlab:
+    host: http://10.1.1.173:8081
+    git-proxy: http://218.94.159.108:81
+    username: root
+    token: Mv1qa3kX1YerHzP7yuCR
+  jenkins:
+    host: http://10.1.1.173:8082
+    username: root
+    token: 1146673971cf0d60a15f2def55acdc64a7
+  docker:
+    registry: 10.1.1.173:5000
+  mail:
+    from: moocoder@example.com
+  storage:
+    type: local
+    properties:
+      location: ./problem

+ 211 - 0
pom.xml

@@ -0,0 +1,211 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <groupId>com.moekr</groupId>
+    <artifactId>moocoder-backend</artifactId>
+    <version>0.4.5</version>
+
+    <packaging>jar</packaging>
+
+    <parent>
+        <groupId>org.springframework.boot</groupId>
+        <artifactId>spring-boot-starter-parent</artifactId>
+        <version>2.0.0.RELEASE</version>
+    </parent>
+
+    <properties>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <java.version>1.8</java.version>
+        <skipTests>true</skipTests>
+    </properties>
+
+    <dependencies>
+        <!-- Spring Boot Auto-Configurable Dependencies -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-jpa</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-jdbc</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-validation</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-thymeleaf</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-security</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-cache</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-actuator</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-mail</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-configuration-processor</artifactId>
+        </dependency>
+
+        <!-- Hibernate Dependencies -->
+        <dependency>
+            <groupId>org.hibernate</groupId>
+            <artifactId>hibernate-java8</artifactId>
+        </dependency>
+
+        <!-- Apache Commons Dependencies -->
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-lang3</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>commons-codec</groupId>
+            <artifactId>commons-codec</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-compress</artifactId>
+            <version>1.16.1</version>
+        </dependency>
+
+        <!-- Api Dependencies -->
+        <dependency>
+            <groupId>org.gitlab4j</groupId>
+            <artifactId>gitlab4j-api</artifactId>
+            <version>4.8.7</version>
+        </dependency>
+        <dependency>
+            <groupId>com.offbytwo.jenkins</groupId>
+            <artifactId>jenkins-client</artifactId>
+            <version>0.3.7</version>
+        </dependency>
+        <dependency>
+            <groupId>com.spotify</groupId>
+            <artifactId>docker-client</artifactId>
+            <version>8.11.2</version>
+        </dependency>
+
+        <!-- Utility Dependencies -->
+        <dependency>
+            <groupId>org.json</groupId>
+            <artifactId>json</artifactId>
+            <version>20180130</version>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.jgit</groupId>
+            <artifactId>org.eclipse.jgit</artifactId>
+            <version>4.11.0.201803080745-r</version>
+        </dependency>
+        <dependency>
+            <groupId>org.zeroturnaround</groupId>
+            <artifactId>zt-zip</artifactId>
+            <version>1.12</version>
+        </dependency>
+
+        <!-- Storage Provider Dependencies -->
+        <dependency>
+            <groupId>com.aliyun.oss</groupId>
+            <artifactId>aliyun-sdk-oss</artifactId>
+            <version>3.1.0</version>
+        </dependency>
+        <dependency>
+            <groupId>com.qiniu</groupId>
+            <artifactId>qiniu-java-sdk</artifactId>
+            <version>7.2.11</version>
+        </dependency>
+        <dependency>
+            <groupId>com.qcloud</groupId>
+            <artifactId>cos_api</artifactId>
+            <version>5.4.1</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.slf4j</groupId>
+                    <artifactId>slf4j-log4j12</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+
+        <!-- WebJars Dependencies -->
+        <dependency>
+            <groupId>org.webjars</groupId>
+            <artifactId>webjars-locator</artifactId>
+            <version>0.34</version>
+        </dependency>
+        <dependency>
+            <groupId>org.webjars</groupId>
+            <artifactId>jquery</artifactId>
+            <version>3.2.1</version>
+        </dependency>
+        <dependency>
+            <groupId>org.webjars</groupId>
+            <artifactId>bootstrap</artifactId>
+            <version>3.3.7</version>
+        </dependency>
+        <dependency>
+            <groupId>org.webjars</groupId>
+            <artifactId>font-awesome</artifactId>
+            <version>4.7.0</version>
+        </dependency>
+
+        <dependency>
+            <groupId>cn.seecoder</groupId>
+            <artifactId>seecoder-frontend-webjars</artifactId>
+            <version>0.1.6</version>
+        </dependency>
+
+        <!-- Database Driver Dependencies -->
+        <dependency>
+            <groupId>mysql</groupId>
+            <artifactId>mysql-connector-java</artifactId>
+            <scope>runtime</scope>
+        </dependency>
+
+        <!-- Lombok Dependencies -->
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+            <scope>provided</scope>
+        </dependency>
+
+        <!-- Test Dependencies -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+            </plugin>
+        </plugins>
+    </build>
+</project>

+ 26 - 0
src/main/java/com/moekr/moocoder/Application.java

@@ -0,0 +1,26 @@
+package com.moekr.moocoder;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
+import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
+
+import java.time.ZoneId;
+import java.util.TimeZone;
+
+@SpringBootApplication
+@EnableJpaRepositories(basePackages = "com.moekr.moocoder.data.dao")
+@EnableJpaAuditing
+public class Application extends SpringApplication {
+	public static ConfigurableApplicationContext context;
+	public static void main(String[] args) {
+		// 强制写入默认时区
+		TimeZone.setDefault(TimeZone.getTimeZone(ZoneId.of("GMT+8")));
+		context = SpringApplication.run(Application.class, args);
+	}
+
+	public static <T> T getBean(Class<T> clazz) {
+		return context.getBean(clazz);
+	}
+}

+ 49 - 0
src/main/java/com/moekr/moocoder/data/converter/FailureSetConverter.java

@@ -0,0 +1,49 @@
+package com.moekr.moocoder.data.converter;
+
+import com.moekr.moocoder.data.entity.Record.Failure;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import javax.persistence.AttributeConverter;
+import java.util.HashSet;
+import java.util.Set;
+
+public class FailureSetConverter implements AttributeConverter<Set<Failure>, String> {
+	@Override
+	public String convertToDatabaseColumn(Set<Failure> attribute) {
+		JSONArray array = new JSONArray();
+		for (Failure failure : attribute) {
+			JSONObject object = new JSONObject();
+			object.put("name", failure.getName());
+			object.put("status", failure.getStatus());
+			object.put("details", failure.getDetails());
+			object.put("trace", failure.getTrace());
+			array.put(object);
+		}
+		return array.toString();
+	}
+
+	@Override
+	public Set<Failure> convertToEntityAttribute(String dbData) {
+		Set<Failure> failures = new HashSet<>();
+		JSONArray array = new JSONArray(dbData);
+		for (Object object : array) {
+			if (object instanceof JSONObject) {
+				try {
+					Failure failure = new Failure();
+					if (((JSONObject) object).has("name"))
+						failure.setName(((JSONObject) object).getString("name"));
+					if (((JSONObject) object).has("status"))
+						failure.setStatus(((JSONObject) object).getString("status"));
+					if (((JSONObject) object).has("details"))
+						failure.setDetails(((JSONObject) object).getString("details"));
+					if (((JSONObject) object).has("trace"))
+						failure.setTrace(((JSONObject) object).getString("trace"));
+					failures.add(failure);
+				} catch (JSONException ignore) { }
+			}
+		}
+		return failures;
+	}
+}

+ 29 - 0
src/main/java/com/moekr/moocoder/data/converter/StringListConverter.java

@@ -0,0 +1,29 @@
+package com.moekr.moocoder.data.converter;
+
+import org.json.JSONArray;
+
+import javax.persistence.AttributeConverter;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+public class StringListConverter implements AttributeConverter<List<String>, String> {
+	@Override
+	public String convertToDatabaseColumn(List<String> attribute) {
+		attribute = attribute.stream().filter(Objects::nonNull).collect(Collectors.toList());
+		return new JSONArray(attribute).toString();
+	}
+
+	@Override
+	public List<String> convertToEntityAttribute(String dbData) {
+		JSONArray array = new JSONArray(dbData);
+		List<String> list = new ArrayList<>();
+		for (Object object : array) {
+			if (object instanceof String) {
+				list.add((String) object);
+			}
+		}
+		return list;
+	}
+}

+ 29 - 0
src/main/java/com/moekr/moocoder/data/converter/StringSetConverter.java

@@ -0,0 +1,29 @@
+package com.moekr.moocoder.data.converter;
+
+import org.json.JSONArray;
+
+import javax.persistence.AttributeConverter;
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+public class StringSetConverter implements AttributeConverter<Set<String>, String> {
+	@Override
+	public String convertToDatabaseColumn(Set<String> attribute) {
+		attribute = attribute.stream().filter(Objects::nonNull).collect(Collectors.toSet());
+		return new JSONArray(attribute).toString();
+	}
+
+	@Override
+	public Set<String> convertToEntityAttribute(String dbData) {
+		JSONArray array = new JSONArray(dbData);
+		Set<String> set = new HashSet<>();
+		for (Object object : array) {
+			if (object instanceof String) {
+				set.add((String) object);
+			}
+		}
+		return set;
+	}
+}

+ 17 - 0
src/main/java/com/moekr/moocoder/data/dao/CommitDAO.java

@@ -0,0 +1,17 @@
+package com.moekr.moocoder.data.dao;
+
+import com.moekr.moocoder.data.entity.Commit;
+import com.moekr.moocoder.data.entity.Result;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface CommitDAO extends JpaRepository<Commit, String> {
+	Commit findById(int commitId);
+
+	Commit findFirstByResultOrderByIdDesc(Result result);
+
+	Commit findFirstByResultAndFinishedOrderByIdAsc(Result result, boolean finished);
+
+	default Commit findFirstUnfinishedByResult(Result result) {
+		return findFirstByResultAndFinishedOrderByIdAsc(result, false);
+	}
+}

+ 20 - 0
src/main/java/com/moekr/moocoder/data/dao/CourseDAO.java

@@ -0,0 +1,20 @@
+package com.moekr.moocoder.data.dao;
+
+import com.moekr.moocoder.data.entity.Course;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+/**
+ * @author XiangzheXu
+ * create-time: 2018/12/9
+ */
+public interface CourseDAO extends JpaRepository<Course, Integer> {
+    Course getCourseById(Integer id);
+
+    Course getCourseByCode(String code);
+
+
+    Page<Course> getAllByTeacherID(Integer teacherID, Pageable pageable);
+
+}

+ 189 - 0
src/main/java/com/moekr/moocoder/data/dao/ExamDAO.java

@@ -0,0 +1,189 @@
+package com.moekr.moocoder.data.dao;
+
+import com.moekr.moocoder.data.entity.Exam;
+import com.moekr.moocoder.data.entity.User;
+import com.moekr.moocoder.util.enums.ExamStatus;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageImpl;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+
+import java.time.LocalDateTime;
+import java.util.Collection;
+import java.util.List;
+
+public interface ExamDAO extends JpaRepository<Exam, Integer> {
+
+	Exam findById(int id);
+
+	List<Exam> findAllByStatus(ExamStatus status);
+
+	Page<Exam> findAllByStatus(ExamStatus status,  Pageable pageable);
+
+
+	Page<Exam> findAllByStatusAndCourseIdIn(ExamStatus status, Collection<Integer> courseId, Pageable pageable);
+
+	default Page<Exam> findAllByActualStatus(ExamStatus status, Pageable pageable) {
+		switch (status) {
+			case READY:
+				return findAllReady(pageable);
+			case AVAILABLE:
+				return findAllAvailable(pageable);
+			case FINISHED:
+				return findAllFinished(pageable);
+		}
+		return findAllByStatus(status, pageable);
+	}
+
+	Page<Exam> findAllByCreator(User creator, Pageable pageable);
+
+	Page<Exam> findAllByCreatorAndStatus(User creator, ExamStatus status, Pageable pageable);
+
+	default Page<Exam> findAllByCreatorAndActualStatus(User creator, ExamStatus status, Pageable pageable) {
+		switch (status) {
+			case READY:
+				return findAllReadyByCreator(creator, pageable);
+			case AVAILABLE:
+				return findAllAvailableByCreator(creator, pageable);
+			case FINISHED:
+				return findAllFinishedByCreator(creator, pageable);
+		}
+		return findAllByCreatorAndStatus(creator, status, pageable);
+	}
+
+	default Page<Exam> findAllJoined(int userId, Pageable pageable) {
+		List<Exam> examList = findAllJoined(userId, pageable.getOffset(), pageable.getPageSize());
+		return new PageImpl<>(examList, pageable, countAllJoined(userId));
+	}
+
+	/**
+	 * 不能查询到关闭的或结束的并且未参加的考试
+	 * @param userId 只能查询自己已经加入的课程
+	 */
+	default Page<Exam> findAllByActualStatus(int userId, ExamStatus status, Pageable pageable){
+
+		switch (status) {
+			case READY:
+				return findAllReady(userId,pageable);
+			case AVAILABLE:
+				return findAllAvailable(userId, pageable);
+			case FINISHED:
+				return findAllJoinedAndFinished(userId, pageable);
+			case CLOSED:
+				return findAllJoinedAndStatusIs(userId, status, pageable);
+		}
+		List<Integer> courseId = getCourseIdByUserID(userId);
+
+		return findAllByStatusAndCourseIdIn(status,courseId, pageable);
+	}
+
+	default Page<Exam> findAllJoinedAndFinished(int userId, Pageable pageable) {
+		LocalDateTime now = LocalDateTime.now();
+		List<Exam> examList = findAllJoinedByStatusAndEndAtIsLessThan(userId, ExamStatus.AVAILABLE.toString(), now, pageable.getOffset(), pageable.getPageSize());
+		return new PageImpl<>(examList, pageable, countAllJoinedByStatusAndEndAtIsLessThan(userId, ExamStatus.AVAILABLE.toString(), now));
+	}
+
+	default Page<Exam> findAllJoinedAndStatusIs(int userId, ExamStatus status, Pageable pageable) {
+		List<Exam> examList = findAllJoinedAndStatusIs(userId, status.toString(), pageable.getOffset(), pageable.getPageSize());
+		return new PageImpl<>(examList, pageable, countAllJoinedAndStatusIs(userId, status.toString()));
+	}
+
+
+
+    /**
+     * 查询所有考试,不包括已关闭的
+     * @param userId
+     * @param pageable
+     * @return
+     */
+    default Page<Exam> findAllExcludeClosed(int userId, Pageable pageable) {
+        LocalDateTime now = LocalDateTime.now();
+        List<Exam> examList = findAllJoinedOrStatusAvailable(userId, pageable.getOffset(), pageable.getPageSize());
+        return new PageImpl<>(examList, pageable, countAllJoinedOrStatusAvailable(userId));
+    }
+
+    @Query(value = "SELECT sc.course FROM STUDENT_COURSE sc WHERE sc.student = ?1", nativeQuery = true)
+	List<Integer> getCourseIdByUserID(int userId);
+
+	@Query(value = "SELECT * FROM ENTITY_EXAM AS e WHERE e.id IN (SELECT r.exam FROM ENTITY_RESULT AS r WHERE r.owner = ?1) ORDER BY e.id DESC LIMIT ?2,?3", nativeQuery = true)
+	List<Exam> findAllJoined(int userId, long offset, int limit);
+
+	@Query(value = "SELECT COUNT(*) FROM ENTITY_EXAM AS e WHERE e.id IN (SELECT r.exam FROM ENTITY_RESULT AS r WHERE r.owner = ?1)", nativeQuery = true)
+	long countAllJoined(int userId);
+
+	@Query(value = "SELECT * FROM ENTITY_EXAM AS e WHERE e.id IN (SELECT r.exam FROM ENTITY_RESULT AS r WHERE r.owner = ?1) and e.status = ?2 and e.end_at < ?3 ORDER BY e.id DESC LIMIT ?4,?5", nativeQuery = true)
+	List<Exam> findAllJoinedByStatusAndEndAtIsLessThan(int userId, String status, LocalDateTime now, long offset, int limit);
+
+	@Query(value = "SELECT COUNT(*) FROM ENTITY_EXAM AS e WHERE e.id IN (SELECT r.exam FROM ENTITY_RESULT AS r WHERE r.owner = ?1) and e.status = ?2 and e.end_at < ?3", nativeQuery = true)
+	long countAllJoinedByStatusAndEndAtIsLessThan(int userId, String status, LocalDateTime now);
+
+	@Query(value = "SELECT * FROM ENTITY_EXAM AS e WHERE e.id IN (SELECT r.exam FROM ENTITY_RESULT AS r WHERE r.owner = ?1) and e.status = ?2 ORDER BY e.id DESC LIMIT ?3,?4", nativeQuery = true)
+	List<Exam> findAllJoinedAndStatusIs(int userId, String status, long offset, int limit);
+
+	@Query(value = "SELECT COUNT(*) FROM ENTITY_EXAM AS e WHERE e.id IN (SELECT r.exam FROM ENTITY_RESULT AS r WHERE r.owner = ?1) and e.status = ?2", nativeQuery = true)
+	long countAllJoinedAndStatusIs(int userId, String status);
+
+    @Query(value = "SELECT * FROM ENTITY_EXAM AS e WHERE " +
+			 "?1 IN (SELECT sc.student FROM STUDENT_COURSE sc WHERE sc.course = e.courseId ) AND " +
+			"(e.id IN (SELECT r.exam FROM ENTITY_RESULT AS r WHERE r.owner = ?1) or e.status = 'AVAILABLE') ORDER BY e.id DESC LIMIT ?2,?3", nativeQuery = true)
+    List<Exam> findAllJoinedOrStatusAvailable(int userId, long offset, int limit);
+
+    @Query(value = "SELECT COUNT(*) FROM ENTITY_EXAM AS e WHERE e.id IN (SELECT r.exam FROM ENTITY_RESULT AS r WHERE r.owner = ?1) or e.status = 'AVAILABLE'", nativeQuery = true)
+    long countAllJoinedOrStatusAvailable(int userId);
+
+	default Page<Exam> findAllReady(Pageable pageable) {
+		return findAllByStatusAndStartAtIsGreaterThan(ExamStatus.AVAILABLE, LocalDateTime.now(), pageable);
+	}
+	default Page<Exam> findAllReady(int userId,Pageable pageable) {
+
+		return findAllByStatusAndStartAtIsGreaterThanAndCourseIdIn(ExamStatus.AVAILABLE, LocalDateTime.now(),
+			getCourseIdByUserID(userId),pageable);
+	}
+
+	default Page<Exam> findAllAvailable(Pageable pageable) {
+		LocalDateTime now = LocalDateTime.now();
+		return findAllByStatusAndStartAtIsLessThanAndEndAtIsGreaterThan(ExamStatus.AVAILABLE, now, now, pageable);
+	}
+
+	default Page<Exam> findAllAvailable(int userID, Pageable pageable) {
+		LocalDateTime now = LocalDateTime.now();
+		return findAllByStatusAndStartAtIsLessThanAndEndAtIsGreaterThanAndCourseIdIn(ExamStatus.AVAILABLE, now, now,
+				getCourseIdByUserID(userID), pageable);
+	}
+
+	default Page<Exam> findAllFinished(Pageable pageable) {
+		return findAllByStatusAndEndAtIsLessThan(ExamStatus.AVAILABLE, LocalDateTime.now(), pageable);
+	}
+
+	default Page<Exam> findAllReadyByCreator(User creator, Pageable pageable) {
+		return findAllByCreatorAndStatusAndStartAtIsGreaterThan(creator, ExamStatus.AVAILABLE, LocalDateTime.now(), pageable);
+	}
+
+	default Page<Exam> findAllAvailableByCreator(User creator, Pageable pageable) {
+		LocalDateTime now = LocalDateTime.now();
+		return findAllByCreatorAndStatusAndStartAtIsLessThanAndEndAtIsGreaterThan(creator, ExamStatus.AVAILABLE, now, now, pageable);
+	}
+
+	default Page<Exam> findAllFinishedByCreator(User creator, Pageable pageable) {
+		return findAllByCreatorAndStatusAndEndAtIsLessThan(creator, ExamStatus.AVAILABLE, LocalDateTime.now(), pageable);
+	}
+
+	Page<Exam> findAllByStatusAndStartAtIsGreaterThanAndCourseIdIn(ExamStatus status, LocalDateTime startAt, Collection<Integer> courseId, Pageable pageable);
+
+	// 不要直接调用下方的方法
+
+	Page<Exam> findAllByStatusAndStartAtIsGreaterThan(ExamStatus status, LocalDateTime startAtGreaterThan, Pageable pageable);
+
+	Page<Exam> findAllByStatusAndStartAtIsLessThanAndEndAtIsGreaterThan(ExamStatus status, LocalDateTime startAtLessThan, LocalDateTime endAtGreaterThan, Pageable pageable);
+
+	Page<Exam> findAllByStatusAndStartAtIsLessThanAndEndAtIsGreaterThanAndCourseIdIn(ExamStatus status, LocalDateTime startAt, LocalDateTime endAt, Collection<Integer> courseId, Pageable pageable);
+
+	Page<Exam> findAllByStatusAndEndAtIsLessThan(ExamStatus status, LocalDateTime endAtLessThan, Pageable pageable);
+
+	Page<Exam> findAllByCreatorAndStatusAndStartAtIsGreaterThan(User creator, ExamStatus status, LocalDateTime startAtGreaterThan, Pageable pageable);
+
+	Page<Exam> findAllByCreatorAndStatusAndStartAtIsLessThanAndEndAtIsGreaterThan(User creator, ExamStatus status, LocalDateTime startAtLessThan, LocalDateTime endAtGreaterThan, Pageable pageable);
+
+	Page<Exam> findAllByCreatorAndStatusAndEndAtIsLessThan(User creator, ExamStatus status, LocalDateTime endAtLessThan, Pageable pageable);
+}

+ 22 - 0
src/main/java/com/moekr/moocoder/data/dao/ProblemDAO.java

@@ -0,0 +1,22 @@
+package com.moekr.moocoder.data.dao;
+
+import com.moekr.moocoder.data.entity.Problem;
+import com.moekr.moocoder.data.entity.User;
+import com.moekr.moocoder.util.enums.ProblemType;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+
+public interface ProblemDAO extends JpaRepository<Problem, Integer> {
+	Page<Problem> findAllByType(ProblemType type, Pageable pageable);
+
+	Page<Problem> findAllByCreatorIsNullOrCreator(User creator, Pageable pageable);
+
+	@Query("SELECT p FROM Problem p WHERE (p.creator IS NULL OR p.creator = ?1) AND p.type = ?2")
+	Page<Problem> findAllByCreatorIsNullOrCreatorAndType(User creator, ProblemType type, Pageable pageable);
+
+	Problem findById(int id);
+
+	Integer countByType(ProblemType type);
+}

+ 59 - 0
src/main/java/com/moekr/moocoder/data/dao/RecordDAO.java

@@ -0,0 +1,59 @@
+package com.moekr.moocoder.data.dao;
+
+import com.moekr.moocoder.data.entity.*;
+import com.moekr.moocoder.util.enums.BuildStatus;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+
+import java.util.List;
+import java.util.Set;
+
+public interface RecordDAO extends JpaRepository<Record, Integer> {
+	Record findById(int id);
+
+	Set<Record> findAllByStatus(BuildStatus status);
+
+	Record findByCommit_Result_IdAndNumber(int resultId, int number);
+
+	default Record findByResultIdAndBuildNumber(int resultId, int buildNumber) {
+		return findByCommit_Result_IdAndNumber(resultId, buildNumber);
+	}
+
+	Record findFirstByCommit_ResultAndCommit_FinishedAndProblemOrderByIdDesc(Result result, boolean finished, Problem problem);
+
+	default Record findLastBuiltByResultAndProblem(Result result, Problem problem) {
+		return findFirstByCommit_ResultAndCommit_FinishedAndProblemOrderByIdDesc(result, true, problem);
+	}
+
+	@Query(value = "SELECT r.problem.id AS problem, r.problem.name AS problemName, MAX(r.failures) AS testCase FROM Record r WHERE r.commit.result.exam = ?1 AND r.status In ?2 GROUP BY r.problem")
+	List<TestCaseInterface> findExamTestCase(Exam exam, List<BuildStatus> statuses);
+
+    interface TestCaseInterface {
+        int getProblem();
+        String getProblemName();
+        Set<Record.Failure> getTestCase();
+    }
+
+    @Query(value = "SELECT res.owner AS userId, u.username, res.problem, r2.status, CAST(r2.failures AS char) AS testCaseResult," +
+            " r2.console_output AS consoleOutput, r2.score " +
+            "FROM " +
+                "(SELECT er.owner, r.problem, max(r.id) AS rid " +
+                    "FROM (SELECT result.* FROM ENTITY_RESULT result where result.exam = ?1 AND result.owner <> ?2) er " +
+                    "JOIN ENTITY_COMMIT ec ON er.id=ec.result " +
+                    "JOIN ENTITY_RECORD r ON ec.id=r.commit " +
+                    "WHERE ec.finished=b'1' " +
+                    "GROUP BY er.owner,r.problem" +
+                ") res, ENTITY_RECORD r2, ENTITY_USER u " +
+            "WHERE res.rid=r2.id and res.owner=u.id", nativeQuery = true)
+    List<TestCaseResultInterface> findExamResultRecordsExcludeCreator(int examId, int creatorId);
+
+    interface TestCaseResultInterface {
+        int getUserId();
+        String getUsername();
+        int getProblem();
+        String getStatus();
+        String getTestCaseResult(); // 原生sql语句,获取的是String类型的failures
+        String getConsoleOutput();
+        int getScore();
+    }
+}

+ 26 - 0
src/main/java/com/moekr/moocoder/data/dao/ResultDAO.java

@@ -0,0 +1,26 @@
+package com.moekr.moocoder.data.dao;
+
+import com.moekr.moocoder.data.entity.Exam;
+import com.moekr.moocoder.data.entity.Record;
+import com.moekr.moocoder.data.entity.Result;
+import com.moekr.moocoder.data.entity.User;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+
+import java.util.List;
+import java.util.Map;
+
+public interface ResultDAO extends JpaRepository<Result, Integer> {
+	Result findById(int id);
+
+	Result findByOwnerAndExam(User owner, Exam exam);
+
+	List<Result> findAllByOwner(User owner);
+
+	List<Result> findAllByExam(Exam exam);
+
+	List<Result> findAllByExamAndOwnerIsNot(Exam exam, User owner);
+
+	@Query(value = "SELECT res FROM Result res WHERE res.exam = ?1 AND res.owner <> ?2 AND res.lastCommitAt is null")
+    List<Result> findAllWithNoCommitByExam(Exam exam, User owner);
+}

+ 22 - 0
src/main/java/com/moekr/moocoder/data/dao/UserDAO.java

@@ -0,0 +1,22 @@
+package com.moekr.moocoder.data.dao;
+
+import com.moekr.moocoder.data.entity.User;
+import com.moekr.moocoder.util.enums.UserRole;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface UserDAO extends JpaRepository<User, Integer> {
+	User findById(int id);
+
+	User findByUsername(String username);
+
+	User findByEmail(String email);
+
+	User findByPhone(String phone);
+
+	Page<User> findAllByUsernameLike(String search, Pageable pageable);
+
+	Integer countByRole(UserRole role);
+
+}

+ 52 - 0
src/main/java/com/moekr/moocoder/data/entity/Commit.java

@@ -0,0 +1,52 @@
+package com.moekr.moocoder.data.entity;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+import org.hibernate.annotations.LazyCollection;
+import org.hibernate.annotations.LazyCollectionOption;
+import org.springframework.data.annotation.CreatedDate;
+import org.springframework.data.jpa.domain.support.AuditingEntityListener;
+
+import javax.persistence.*;
+import java.time.LocalDateTime;
+import java.util.HashSet;
+import java.util.Set;
+
+@Data
+@EqualsAndHashCode(exclude = {"result", "records"})
+@ToString(exclude = {"result", "records"})
+@Entity
+@Table(name = "ENTITY_COMMIT")
+@EntityListeners(AuditingEntityListener.class)
+public class Commit {
+	@Id
+	@GeneratedValue(strategy = GenerationType.IDENTITY)
+	@Column(name = "id")
+	private Integer id;
+
+	@Basic
+	@Column(name = "hash", nullable = false)
+	private String hash;
+
+	@Basic
+	@Column(name = "finished", columnDefinition = "BIT(1) NOT NULL DEFAULT 0")
+	private boolean finished;
+
+	@Basic
+	@Column(name = "score", nullable = false)
+	private Integer score = 0;
+
+	@Basic
+	@Column(name = "created_at", nullable = false)
+	@CreatedDate
+	private LocalDateTime createdAt;
+
+	@ManyToOne(targetEntity = Result.class, fetch = FetchType.LAZY)
+	@JoinColumn(name = "result", referencedColumnName = "id")
+	private Result result;
+
+	@OneToMany(targetEntity = Record.class, mappedBy = "commit", cascade = CascadeType.REMOVE)
+	@LazyCollection(LazyCollectionOption.EXTRA)
+	private Set<Record> records = new HashSet<>();
+}

+ 47 - 0
src/main/java/com/moekr/moocoder/data/entity/Course.java

@@ -0,0 +1,47 @@
+package com.moekr.moocoder.data.entity;
+
+import lombok.Data;
+import org.springframework.data.jpa.domain.support.AuditingEntityListener;
+
+import javax.persistence.*;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * @author XiangzheXu
+ * create-time: 2018/12/9
+ */
+
+@Data
+@Entity
+@Table(name = "ENTITY_COURSE")
+@EntityListeners(AuditingEntityListener.class)
+public class Course {
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    @Column(name = "id")
+    private Integer id;
+
+    /**
+     * 课程名
+     */
+    private String name;
+
+    /**
+     * 选课码
+     */
+    private String code;
+
+    /**
+     * 课程对应的老师
+     */
+    private Integer teacherID;
+
+    /**
+     * 选课学生
+     */
+    @ManyToMany
+    @JoinTable(name="STUDENT_COURSE", joinColumns = @JoinColumn(name = "course", referencedColumnName = "id"),
+    inverseJoinColumns = @JoinColumn(name = "student", referencedColumnName = "id"))
+    private List<User> students=new LinkedList<>();
+}

+ 82 - 0
src/main/java/com/moekr/moocoder/data/entity/Exam.java

@@ -0,0 +1,82 @@
+package com.moekr.moocoder.data.entity;
+
+import com.moekr.moocoder.util.enums.ExamStatus;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+import org.hibernate.annotations.LazyCollection;
+import org.hibernate.annotations.LazyCollectionOption;
+import org.springframework.data.annotation.CreatedDate;
+import org.springframework.data.jpa.domain.support.AuditingEntityListener;
+
+import javax.persistence.*;
+import java.time.LocalDateTime;
+import java.util.HashSet;
+import java.util.Set;
+
+@Data
+@EqualsAndHashCode(exclude = {"creator", "problems", "results"})
+@ToString(exclude = {"creator", "problems", "results"})
+@Entity
+@Table(name = "ENTITY_EXAM")
+@EntityListeners(AuditingEntityListener.class)
+public class Exam {
+	@Id
+	@Column(name = "id")
+	private Integer id;
+
+	@Basic
+	@Column(name = "uuid", nullable = false)
+	private String uuid;
+
+	@Basic
+	@Column(name = "name", nullable = false)
+	private String name;
+
+	@Basic
+	@Column(name = "created_at", nullable = false)
+	@CreatedDate
+	private LocalDateTime createdAt;
+
+	@Basic
+	@Column(name = "start_at", nullable = false)
+	private LocalDateTime startAt;
+
+	@Basic
+	@Column(name = "end_at", nullable = false)
+	private LocalDateTime endAt;
+
+	@Enumerated(value = EnumType.STRING)
+	@Column(name = "status", columnDefinition = "VARCHAR(255) NOT NULL DEFAULT 'PREPARING'")
+	private ExamStatus status = ExamStatus.PREPARING;
+
+	@Basic
+	@Column(name = "initial_hash")
+	private String initialHash;
+
+	@ManyToOne(targetEntity = User.class, fetch = FetchType.LAZY)
+	@JoinColumn(name = "creator", referencedColumnName = "id")
+	private User creator;
+
+	@ManyToMany(targetEntity = Problem.class, cascade = CascadeType.DETACH)
+	@JoinTable(name = "LINK_PROBLEM_EXAM",
+			joinColumns = @JoinColumn(name = "exam", referencedColumnName = "id"),
+			inverseJoinColumns = @JoinColumn(name = "problem", referencedColumnName = "id")
+	)
+	@LazyCollection(LazyCollectionOption.EXTRA)
+	private Set<Problem> problems = new HashSet<>();
+
+	@OneToMany(targetEntity = Result.class, mappedBy = "exam", cascade = CascadeType.REMOVE)
+	@LazyCollection(LazyCollectionOption.EXTRA)
+	private Set<Result> results = new HashSet<>();
+
+
+	private Integer courseId;
+
+	public void setStatus(ExamStatus status) {
+		if (status == ExamStatus.READY || status == ExamStatus.FINISHED) {
+			throw new IllegalArgumentException("READY 与 FINISHED 状态不应持久化!");
+		}
+		this.status = status;
+	}
+}

+ 96 - 0
src/main/java/com/moekr/moocoder/data/entity/Problem.java

@@ -0,0 +1,96 @@
+package com.moekr.moocoder.data.entity;
+
+import com.moekr.moocoder.data.converter.StringSetConverter;
+import com.moekr.moocoder.util.enums.ProblemType;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+import org.hibernate.annotations.LazyCollection;
+import org.hibernate.annotations.LazyCollectionOption;
+import org.hibernate.annotations.Where;
+import org.springframework.data.annotation.CreatedDate;
+import org.springframework.data.jpa.domain.support.AuditingEntityListener;
+
+import javax.persistence.*;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.util.HashSet;
+import java.util.Set;
+
+@Data
+@EqualsAndHashCode(exclude = {"creator", "examSet"})
+@ToString(exclude = {"creator", "examSet"})
+@Entity
+@Table(name = "ENTITY_PROBLEM")
+@Where(clause = "deprecated = 0")
+@EntityListeners(AuditingEntityListener.class)
+public class Problem {
+	@Id
+	@GeneratedValue(strategy = GenerationType.IDENTITY)
+	@Column(name = "id")
+	private Integer id;
+
+	@Basic
+	@Column(name = "name", nullable = false)
+	private String name;
+
+	@Enumerated(EnumType.STRING)
+	@Column(name = "type", nullable = false)
+	private ProblemType type;
+
+	@Basic
+	@Column(name = "description", columnDefinition = "TEXT NOT NULL")
+	private String description;
+
+	@Basic
+	@Column(name = "public_files", columnDefinition = "JSON NOT NULL")
+	@Convert(converter = StringSetConverter.class)
+	private Set<String> publicFiles = new HashSet<>();
+
+	@Basic
+	@Column(name = "protected_files", columnDefinition = "JSON NOT NULL")
+	@Convert(converter = StringSetConverter.class)
+	private Set<String> protectedFiles = new HashSet<>();
+
+	@Basic
+	@Column(name = "private_files", columnDefinition = "JSON NOT NULL")
+	@Convert(converter = StringSetConverter.class)
+	private Set<String> privateFiles = new HashSet<>();
+
+	@Basic
+	@Column(name = "created_at", nullable = false)
+	@CreatedDate
+	private LocalDateTime createdAt;
+
+	@Basic
+	@Column(name = "modified_at")
+	private LocalDateTime modifiedAt;
+
+	@Basic
+	@Column(name = "deprecated", columnDefinition = "BIT(1) NOT NULL DEFAULT 0")
+	private boolean deprecated;
+
+	@ManyToOne(targetEntity = User.class, fetch = FetchType.LAZY)
+	@JoinColumn(name = "creator", referencedColumnName = "id")
+	private User creator;
+
+	@ManyToMany(targetEntity = Exam.class, cascade = CascadeType.DETACH)
+	@JoinTable(name = "LINK_PROBLEM_EXAM",
+			joinColumns = @JoinColumn(name = "problem", referencedColumnName = "id"),
+			inverseJoinColumns = @JoinColumn(name = "exam", referencedColumnName = "id")
+	)
+	@LazyCollection(LazyCollectionOption.EXTRA)
+	private Set<Exam> examSet = new HashSet<>();
+
+	public String getUniqueName() {
+		return (id + "-" + name).toLowerCase();
+	}
+
+	public String getImageName() {
+		return getUniqueName();
+	}
+
+	public String getImageTag() {
+		return modifiedAt == null ? null : String.valueOf(modifiedAt.atZone(ZoneId.systemDefault()).toEpochSecond());
+	}
+}

+ 60 - 0
src/main/java/com/moekr/moocoder/data/entity/Record.java

@@ -0,0 +1,60 @@
+package com.moekr.moocoder.data.entity;
+
+import com.moekr.moocoder.data.converter.FailureSetConverter;
+import com.moekr.moocoder.util.enums.BuildStatus;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+import javax.persistence.*;
+import java.util.HashSet;
+import java.util.Set;
+
+@Data
+@EqualsAndHashCode(exclude = {"failures", "commit", "problem"})
+@ToString(exclude = {"consoleOutput", "failures", "commit", "problem"})
+@Entity
+@Table(name = "ENTITY_RECORD")
+public class Record {
+	@Id
+	@GeneratedValue(strategy = GenerationType.IDENTITY)
+	@Column(name = "id")
+	private Integer id;
+
+	@Basic
+	@Column(name = "number", nullable = false)
+	private Integer number;
+
+	@Enumerated(EnumType.STRING)
+	@Column(name = "status", columnDefinition = "VARCHAR(255) NOT NULL DEFAULT 'WAITING'")
+	private BuildStatus status = BuildStatus.WAITING;
+
+	@Basic(fetch = FetchType.LAZY)
+	@Column(name = "console_output", columnDefinition = "TEXT NOT NULL")
+	private String consoleOutput = "";
+
+	@Basic
+	@Column(name = "score", columnDefinition = "INT(11) NOT NULL DEFAULT 0")
+	private Integer score = 0;
+
+	@Basic
+	@Column(name = "failures", columnDefinition = "JSON NOT NULL")
+	@Convert(converter = FailureSetConverter.class)
+	private Set<Failure> failures = new HashSet<>();
+
+	@ManyToOne(targetEntity = Commit.class, fetch = FetchType.LAZY)
+	@JoinColumn(name = "commit", referencedColumnName = "id")
+	private Commit commit;
+
+	@ManyToOne(targetEntity = Problem.class, fetch = FetchType.LAZY)
+	@JoinColumn(name = "problem", referencedColumnName = "id")
+	private Problem problem;
+
+	@Data
+	public static class Failure {
+		private String name;
+        private String status;
+		private String details;
+		private String trace;
+	}
+}

+ 48 - 0
src/main/java/com/moekr/moocoder/data/entity/Result.java

@@ -0,0 +1,48 @@
+package com.moekr.moocoder.data.entity;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+import org.hibernate.annotations.LazyCollection;
+import org.hibernate.annotations.LazyCollectionOption;
+
+import javax.persistence.*;
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+
+@Data
+@EqualsAndHashCode(exclude = {"exam", "owner", "commitList"})
+@ToString(exclude = {"exam", "owner", "commitList"})
+@Entity
+@Table(name = "ENTITY_RESULT")
+public class Result {
+	@Id
+	@Column(name = "id")
+	private Integer id;
+
+	@Basic
+	@Column(name = "score", columnDefinition = "INT(11) NOT NULL DEFAULT 0")
+	private Integer score = 0;
+
+	@Basic
+	@Column(name = "last_commit_at")
+	private LocalDateTime lastCommitAt;
+
+	@Basic
+	@Column(name = "deleted", columnDefinition = "BIT(1) NOT NULL DEFAULT 0")
+	private boolean deleted;
+
+	@ManyToOne(targetEntity = Exam.class, fetch = FetchType.LAZY)
+	@JoinColumn(name = "exam", referencedColumnName = "id")
+	private Exam exam;
+
+	@ManyToOne(targetEntity = User.class, fetch = FetchType.LAZY)
+	@JoinColumn(name = "owner", referencedColumnName = "id")
+	private User owner;
+
+	@OneToMany(targetEntity = Commit.class, mappedBy = "result", cascade = CascadeType.REMOVE)
+	@LazyCollection(LazyCollectionOption.EXTRA)
+	@OrderBy("created_at DESC")
+	private List<Commit> commitList = new ArrayList<>();
+}

+ 78 - 0
src/main/java/com/moekr/moocoder/data/entity/User.java

@@ -0,0 +1,78 @@
+package com.moekr.moocoder.data.entity;
+
+import com.moekr.moocoder.util.enums.UserRole;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+import org.hibernate.annotations.LazyCollection;
+import org.hibernate.annotations.LazyCollectionOption;
+import org.springframework.data.annotation.CreatedDate;
+import org.springframework.data.jpa.domain.support.AuditingEntityListener;
+
+import javax.persistence.*;
+import java.time.LocalDateTime;
+import java.util.HashSet;
+import java.util.Set;
+
+@Data
+@EqualsAndHashCode(exclude = {"problemSet", "examSet", "resultSet", "courses"})
+@ToString(exclude = {"problemSet", "examSet", "resultSet","courses"})
+@Entity
+@Table(name = "ENTITY_USER", indexes = {@Index(columnList = "username"), @Index(columnList = "email")})
+@EntityListeners(AuditingEntityListener.class)
+public class User {
+	@Id
+	@Column(name = "id")
+	private Integer id;
+
+	@Basic
+	@Column(name = "username", nullable = false)
+	private String username;
+
+	@Basic
+	@Column(name = "password", nullable = false)
+	private String password;
+
+	@Basic
+	@Column(name = "email", nullable = false)
+	private String email;
+
+	@Basic
+	@Column(name = "namespace", nullable = false)
+	private Integer namespace;
+
+	@Basic
+	@Column(name = "token", nullable = false)
+	private String token;
+
+	@Enumerated(EnumType.STRING)
+	@Column(name = "role", nullable = false)
+	private UserRole role;
+
+	@Enumerated(EnumType.STRING)
+	@Column(name = "phone")
+	private String phone;
+
+	@Basic
+	@Column(name = "created_at", nullable = false)
+	@CreatedDate
+	private LocalDateTime createdAt;
+
+	@OneToMany(targetEntity = Problem.class, mappedBy = "creator", cascade = CascadeType.REMOVE)
+	@LazyCollection(LazyCollectionOption.EXTRA)
+	private Set<Problem> problemSet = new HashSet<>();
+
+	@OneToMany(targetEntity = Exam.class, mappedBy = "creator", cascade = CascadeType.REMOVE)
+	@LazyCollection(LazyCollectionOption.EXTRA)
+	private Set<Exam> examSet = new HashSet<>();
+
+	@OneToMany(targetEntity = Result.class, mappedBy = "owner", cascade = CascadeType.REMOVE)
+	@LazyCollection(LazyCollectionOption.EXTRA)
+	private Set<Result> resultSet = new HashSet<>();
+
+	@ManyToMany
+	@JoinTable(name="STUDENT_COURSE", joinColumns = @JoinColumn(name = "student", referencedColumnName = "id"),
+			inverseJoinColumns = @JoinColumn(name = "course", referencedColumnName = "id"))
+	private Set<Course> courses = new HashSet<>();
+
+}

+ 30 - 0
src/main/java/com/moekr/moocoder/logic/AsyncWrapper.java

@@ -0,0 +1,30 @@
+package com.moekr.moocoder.logic;
+
+import com.moekr.moocoder.util.Method;
+import lombok.extern.apachecommons.CommonsLog;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Component;
+
+@Component
+@CommonsLog
+public class AsyncWrapper {
+
+	@Async
+	public void asyncInvoke(Method method) {
+		try {
+			method.invoke();
+		} catch (Exception e) {
+			log.error("进行异步调用时发生异常[" + e.getClass() + "]:" + e.getMessage());
+		}
+	}
+
+	@Async
+	public void asyncInvoke(Method method, long delay) {
+		try {
+			Thread.sleep(delay);
+		} catch (InterruptedException e) {
+			Thread.currentThread().interrupt();
+		}
+		asyncInvoke(method);
+	}
+}

+ 35 - 0
src/main/java/com/moekr/moocoder/logic/ExecutorConfiguration.java

@@ -0,0 +1,35 @@
+package com.moekr.moocoder.logic;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.task.TaskExecutor;
+import org.springframework.scheduling.annotation.EnableAsync;
+import org.springframework.scheduling.annotation.EnableScheduling;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ThreadPoolExecutor;
+
+@EnableScheduling
+@EnableAsync
+@Configuration
+public class ExecutorConfiguration {
+	private static final int PROCESSOR_THREAD = Runtime.getRuntime().availableProcessors();
+	private static final int MAX_THREAD = 16;
+
+	@Bean
+	public ScheduledExecutorService scheduledExecutor() {
+		return Executors.newScheduledThreadPool(Math.min(2 * PROCESSOR_THREAD, MAX_THREAD));
+	}
+
+	@Bean
+	public TaskExecutor taskExecutor() {
+		ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
+		taskExecutor.setCorePoolSize(Math.min(2 * PROCESSOR_THREAD, MAX_THREAD));
+		taskExecutor.setMaxPoolSize(Math.min(4 * PROCESSOR_THREAD, MAX_THREAD));
+		taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
+		taskExecutor.initialize();
+		return taskExecutor;
+	}
+}

+ 38 - 0
src/main/java/com/moekr/moocoder/logic/TaskScheduler.java

@@ -0,0 +1,38 @@
+package com.moekr.moocoder.logic;
+
+import com.moekr.moocoder.logic.service.impl.ClosedBuildChecker;
+import com.moekr.moocoder.logic.service.impl.ClosedExamChecker;
+import com.moekr.moocoder.util.Method;
+import lombok.extern.apachecommons.CommonsLog;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+@Component
+@CommonsLog
+public class TaskScheduler {
+	private final ClosedBuildChecker closedBuildChecker;
+	private final ClosedExamChecker closedExamChecker;
+
+	public TaskScheduler(ClosedBuildChecker closedBuildChecker, ClosedExamChecker closedExamChecker) {
+		this.closedBuildChecker = closedBuildChecker;
+		this.closedExamChecker = closedExamChecker;
+	}
+
+	@Scheduled(cron = "5 * * * * *")
+	protected void scheduledCheckClosedExam() {
+		scheduledInvoke(closedExamChecker::check);
+	}
+
+	@Scheduled(cron = "15 * * * * *")
+	protected void scheduledCheckClosedBuild() {
+		scheduledInvoke(closedBuildChecker::check);
+	}
+
+	private void scheduledInvoke(Method method) {
+		try {
+			method.invoke();
+		} catch (Exception e) {
+			log.error("进行定时调用时发生异常[" + e.getClass() + "]:" + e.getMessage());
+		}
+	}
+}

+ 5 - 0
src/main/java/com/moekr/moocoder/logic/api/DockerApi.java

@@ -0,0 +1,5 @@
+package com.moekr.moocoder.logic.api;
+
+public interface DockerApi {
+	void build(String directory, String imageName, String imageTag) throws Exception;
+}

+ 67 - 0
src/main/java/com/moekr/moocoder/logic/api/GitlabApi.java

@@ -0,0 +1,67 @@
+package com.moekr.moocoder.logic.api;
+
+import com.moekr.moocoder.logic.api.vo.GitlabUser;
+import org.gitlab4j.api.GitLabApiException;
+import org.gitlab4j.api.models.CommitAction;
+
+import java.io.UnsupportedEncodingException;
+import java.util.List;
+import java.util.Set;
+
+public interface GitlabApi {
+	GitlabUser createUser(String username, String email, String password) throws GitLabApiException;
+
+	Integer createProject(String name) throws GitLabApiException;
+
+	Integer forkProject(int userId, int projectId, int namespaceId) throws GitLabApiException;
+
+	Set<String> compare(int projectId, String from, String to) throws GitLabApiException;
+
+	void archiveProject(int projectId) throws GitLabApiException;
+
+	void changePassword(int userId, String password) throws GitLabApiException;
+
+	void deleteUser(int userId) throws GitLabApiException;
+
+	void deleteProject(int projectId) throws GitLabApiException;
+
+	/**
+	 * get all files' path of a base path in a project
+	 * @param projectId
+	 * @param basePath
+	 * @param branch
+	 * @return
+	 * @throws GitLabApiException
+	 */
+	List<String> getFilePaths(int projectId, String basePath, String branch) throws GitLabApiException;
+
+	/**
+	 * get file content
+	 * @param projectId
+	 * @param branch
+	 * @param path
+	 * @return
+	 */
+	String getRepositoryFile(int projectId, String branch, String path) throws GitLabApiException, UnsupportedEncodingException;
+
+	/**
+	 * get file content encode with base64
+	 * @param projectId
+	 * @param branch
+	 * @param path
+	 * @return
+	 */
+	String getEncodedRepositoryFile(int projectId, String branch, String path) throws GitLabApiException, UnsupportedEncodingException;
+
+	/**
+	 * create a commit
+	 * @param projectId
+	 * @param branch
+	 * @param commitMessage
+	 * @param email
+	 * @param username
+	 * @param actions
+	 */
+	void createCommit(int projectId, String branch, String commitMessage, String email, String username, List<CommitAction> actions) throws GitLabApiException;
+
+}

+ 18 - 0
src/main/java/com/moekr/moocoder/logic/api/JenkinsApi.java

@@ -0,0 +1,18 @@
+package com.moekr.moocoder.logic.api;
+
+import com.moekr.moocoder.logic.api.vo.BuildDetails;
+import com.offbytwo.jenkins.model.QueueItem;
+
+import java.io.IOException;
+import java.util.Map;
+
+public interface JenkinsApi {
+	void createJob(int id) throws IOException;
+
+	QueueItem invokeBuild(int id, Map<String, String> paramMap) throws IOException;
+
+	void deleteJob(int id) throws IOException;
+
+	// waitFinished: 是否等待直到构建完全完成
+	BuildDetails fetchBuildDetails(int id, int buildNumber, String target, boolean waitFinished) throws IOException;
+}

+ 45 - 0
src/main/java/com/moekr/moocoder/logic/api/impl/DockerApiImpl.java

@@ -0,0 +1,45 @@
+package com.moekr.moocoder.logic.api.impl;
+
+import com.moekr.moocoder.logic.api.DockerApi;
+import com.moekr.moocoder.util.ApplicationProperties;
+import com.moekr.moocoder.util.ApplicationProperties.Docker;
+import com.spotify.docker.client.DefaultDockerClient;
+import com.spotify.docker.client.DockerClient;
+import com.spotify.docker.client.DockerClient.BuildParam;
+import com.spotify.docker.client.ProgressHandler;
+import com.spotify.docker.client.messages.ProgressMessage;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.io.File;
+import java.io.IOException;
+
+@Component
+public class DockerApiImpl implements DockerApi {
+	private final String registry;
+	private final DockerClient client;
+	private final ProgressHandler handler;
+
+	@Autowired
+	public DockerApiImpl(ApplicationProperties properties) {
+		Docker docker = properties.getDocker();
+		registry = docker.getRegistry();
+		client = new DefaultDockerClient(docker.getHost());
+		handler = new NoActionProgressHandler();
+	}
+
+	public void build(String directory, String imageName, String imageTag) throws Exception {
+		String imageId = client.build(new File(directory).toPath(), handler, BuildParam.name(registry + "/" + imageName + ":" + imageTag), BuildParam.forceRm());
+		if (imageId == null) {
+			throw new IOException("Failed to build docker image under [" + directory + "]");
+		}
+		client.push(registry + "/" + imageName + ":" + imageTag, handler);
+	}
+
+	private class NoActionProgressHandler implements ProgressHandler {
+		@Override
+		public void progress(ProgressMessage message) {
+			//IGNORE THE MESSAGE
+		}
+	}
+}

+ 171 - 0
src/main/java/com/moekr/moocoder/logic/api/impl/GitlabApiImpl.java

@@ -0,0 +1,171 @@
+package com.moekr.moocoder.logic.api.impl;
+
+import com.moekr.moocoder.logic.api.GitlabApi;
+import com.moekr.moocoder.logic.api.vo.GitlabUser;
+import com.moekr.moocoder.util.ApplicationProperties;
+import com.moekr.moocoder.util.ApplicationProperties.Gitlab;
+import com.moekr.moocoder.util.ToolKit;
+import org.gitlab4j.api.*;
+import org.gitlab4j.api.models.*;
+import org.gitlab4j.api.models.ImpersonationToken.Scope;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+@Component
+public class GitlabApiImpl implements GitlabApi {
+	private final Gitlab gitlab;
+
+	private final GitLabApi server;
+
+	@Autowired
+	public GitlabApiImpl(ApplicationProperties properties) {
+		gitlab = properties.getGitlab();
+		this.server = new GitLabApi(gitlab.getHost(), gitlab.getToken());
+	}
+
+	@Override
+	public synchronized GitlabUser createUser(String username, String email, String password) throws GitLabApiException {
+		User user = new User();
+		user.setUsername(username);
+		user.setEmail(email);
+		user.setName(username);
+		user.setSkipConfirmation(true);
+		user = server.getUserApi().createUser(user, password, 1000);
+		GitlabUser gitlabUser = new GitlabUser();
+		gitlabUser.setId(user.getId());
+		gitlabUser.setNamespace(fetchNamespace(username));
+		gitlabUser.setToken(createToken(user.getId()));
+		return gitlabUser;
+	}
+
+	private Integer fetchNamespace(String username) throws GitLabApiException {
+		Namespace namespace;
+		namespace = server.getNamespaceApi().findNamespaces(username)
+				.stream()
+				.filter(ns -> "user".equals(ns.getKind()))
+				.reduce((ns1, ns2) -> ns1.getId() > ns2.getId() ? ns1 : ns2)
+				.orElse(null);
+		return namespace != null ? namespace.getId() : null;
+	}
+
+	private String createToken(int userId) throws GitLabApiException {
+		Scope[] scopes = new Scope[]{Scope.API};
+		ImpersonationToken token;
+		token = server.getUserApi().createImpersonationToken(userId, "AES-" + ToolKit.randomUUID(), null, scopes);
+		return token.getToken();
+	}
+
+	@Override
+	public synchronized Integer createProject(String name) throws GitLabApiException {
+		Project project = new Project();
+		project.setName(name);
+		project.setVisibility(Visibility.INTERNAL);
+		project = server.getProjectApi().createProject(project);
+		return project != null ? project.getId() : null;
+	}
+
+	@Override
+	public synchronized Integer forkProject(int userId, int projectId, int namespaceId) throws GitLabApiException {
+		Project project;
+		try {
+			server.setSudoAsId(userId);
+			project = server.getProjectApi().forkProject(projectId, namespaceId);
+			project.setVisibility(Visibility.PRIVATE);
+			project = server.getProjectApi().updateProject(project);
+		} finally {
+			server.unsudo();
+		}
+		if (project != null) {
+			createWebHook(project.getId());
+		}
+		return project != null ? project.getId() : null;
+	}
+
+	@Override
+	public synchronized Set<String> compare(int projectId, String from, String to) throws GitLabApiException {
+		CompareResults compareResults = server.getRepositoryApi().compare(projectId, from, to);
+		return compareResults.getDiffs().stream()
+				.map(Diff::getNewPath)
+				.map(p -> {
+					int end = p.indexOf('/');
+					if (end == -1) {
+						end = p.length();
+					}
+					return p.substring(0, end);
+				})
+				.collect(Collectors.toSet());
+	}
+
+	private void createWebHook(int id) throws GitLabApiException {
+		String url = gitlab.getWebHookProxy() + "/internal/notify/webhook/" + id;
+		server.getProjectApi().addHook(id, url, true, false, false);
+	}
+
+	@Override
+	public synchronized void archiveProject(int projectId) throws GitLabApiException {
+		server.getProjectApi().archiveProject(projectId);
+	}
+
+	@Override
+	public synchronized void changePassword(int userId, String password) throws GitLabApiException {
+		User user = server.getUserApi().getUser(userId);
+		server.getUserApi().modifyUser(user, password, user.getProjectsLimit());
+	}
+
+	@Override
+	public synchronized void deleteUser(int userId) throws GitLabApiException {
+		server.getUserApi().deleteUser(userId, true);
+	}
+
+	@Override
+	public void deleteProject(int projectId) throws GitLabApiException {
+		server.getProjectApi().deleteProject(projectId);
+	}
+
+	@Override
+	public List<String> getFilePaths(int projectId, String basePath, String branch) throws GitLabApiException {
+	    RepositoryApi repositoryApi = server.getRepositoryApi();
+	    List<TreeItem> treeItems = repositoryApi.getTree(projectId, basePath, branch, true);
+	    List<String> filePaths = new ArrayList<>();
+	    for (TreeItem item : treeItems) {
+	        if (TreeItem.Type.BLOB.equals(item.getType())) {
+	            filePaths.add(item.getPath());
+            }
+        }
+	    return filePaths;
+    }
+
+	@Override
+	public String getRepositoryFile(int projectId, String branch, String path) throws GitLabApiException, UnsupportedEncodingException {
+		RepositoryFileApi repositoryFileApi = server.getRepositoryFileApi();
+		RepositoryFile repositoryFile = repositoryFileApi.getFile(path, projectId, branch);
+		if(repositoryFile!=null){
+			return new String(Base64.getDecoder().decode(repositoryFile.getContent()), "UTF-8");
+		}
+		return null;
+	}
+
+    @Override
+	public String getEncodedRepositoryFile(int projectId, String branch, String path) throws GitLabApiException, UnsupportedEncodingException {
+        RepositoryFileApi repositoryFileApi = server.getRepositoryFileApi();
+        RepositoryFile repositoryFile = repositoryFileApi.getFile(path, projectId, branch);
+        if(repositoryFile!=null){
+            return repositoryFile.getContent();
+        }
+        return null;
+    }
+
+	@Override
+	public void createCommit(int projectId, String branch, String commitMessage, String email, String username, List<CommitAction> actions) throws GitLabApiException {
+		CommitsApi commitsApi = server.getCommitsApi();
+        commitsApi.createCommit(projectId, branch, commitMessage, null, email, username,actions);
+    }
+
+}

+ 160 - 0
src/main/java/com/moekr/moocoder/logic/api/impl/JenkinsApiImpl.java

@@ -0,0 +1,160 @@
+package com.moekr.moocoder.logic.api.impl;
+
+import com.moekr.moocoder.logic.api.JenkinsApi;
+import com.moekr.moocoder.logic.api.vo.BuildDetails;
+import com.moekr.moocoder.logic.api.vo.CoverageResult;
+import com.moekr.moocoder.logic.api.vo.MutationResult;
+import com.moekr.moocoder.util.ApplicationProperties;
+import com.moekr.moocoder.util.ApplicationProperties.Jenkins;
+import com.offbytwo.jenkins.JenkinsServer;
+import com.offbytwo.jenkins.client.JenkinsHttpClient;
+import com.offbytwo.jenkins.model.Artifact;
+import com.offbytwo.jenkins.model.BuildWithDetails;
+import com.offbytwo.jenkins.model.QueueItem;
+import com.offbytwo.jenkins.model.QueueReference;
+import lombok.extern.apachecommons.CommonsLog;
+import org.apache.commons.io.output.ByteArrayOutputStream;
+import org.dom4j.*;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.PostConstruct;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.List;
+import java.util.Map;
+
+@Component
+@CommonsLog
+public class JenkinsApiImpl implements JenkinsApi {
+	private final ApplicationProperties properties;
+
+	private JenkinsServer server;
+	private String configTemplate;
+
+	public JenkinsApiImpl(ApplicationProperties properties) {
+		this.properties = properties;
+	}
+
+	@PostConstruct
+	private void initialize() throws URISyntaxException, IOException {
+		Jenkins jenkins = properties.getJenkins();
+		URI uri = new URI(jenkins.getHost());
+		String username = jenkins.getUsername();
+		String token = jenkins.getToken();
+		JenkinsHttpClient client = new PoolingJenkinsHttpClient(uri, username, token);
+		server = new JenkinsServer(client);
+		configTemplate = readTemplate();
+	}
+
+	@Override
+	public void createJob(int id) throws IOException {
+		server.createJob(String.valueOf(id), configTemplate);
+	}
+
+	@Override
+	public QueueItem invokeBuild(int id, Map<String, String> paramMap) throws IOException {
+		QueueReference reference;
+		if (paramMap == null) {
+			reference = server.getJob(String.valueOf(id)).build();
+		} else {
+			// 0.3.7版本的Jenkins客户端库中build(Map<String, String> params)方法存在BUG,会触发两次构建
+			reference = server.getJob(String.valueOf(id)).build(paramMap, false);
+		}
+		QueueItem item = server.getQueueItem(reference);
+		while (item.getExecutable() == null) {
+			try {
+				Thread.sleep(500);
+			} catch (InterruptedException e) {
+				Thread.currentThread().interrupt();
+			}
+			item = server.getQueueItem(reference);
+		}
+		return item;
+	}
+
+	@Override
+	public void deleteJob(int id) throws IOException {
+		server.deleteJob(String.valueOf(id));
+	}
+
+	@Override
+	public BuildDetails fetchBuildDetails(int id, int buildNumber, String target, boolean waitFinished) throws IOException {
+		BuildDetails result = new BuildDetails();
+		BuildWithDetails build;
+		build = server.getJob(String.valueOf(id)).getBuildByNumber(buildNumber).details();
+		while (waitFinished && (build.getResult() == null || build.getDuration() == 0)) {
+			try {
+				Thread.sleep(500);
+			} catch (InterruptedException e) {
+				Thread.currentThread().interrupt();
+			}
+			build = server.getJob(String.valueOf(id)).getBuildByNumber(buildNumber).details();
+		}
+		result.setConsoleOutput(build.getConsoleOutputText());
+		result.setNumber(build.getNumber());
+		result.setDuration(build.getDuration());
+		result.setBuildResult(build.getResult());
+		fetchTargetResult(result, build, target);
+		return result;
+	}
+
+	private void fetchTargetResult(BuildDetails result, BuildWithDetails build, String target) {
+		try {
+			if ("TEST".equals(target)) {
+				fetchTestResult(result, build);
+			} else if ("COVERAGE".equals(target)) {
+				fetchCoverageResult(result, build);
+			} else if ("MUTATION".equals(target)) {
+				fetchMutationResult(result, build);
+			}
+		} catch (Exception ignore) { }
+	}
+
+	private void fetchTestResult(BuildDetails result, BuildWithDetails build) throws IOException {
+		result.setTestResult(build.getTestResult());
+	}
+
+	private void fetchCoverageResult(BuildDetails result, BuildWithDetails build) throws IOException {
+		result.setCoverageResult(build.getClient().get(build.getUrl() + "/cobertura/?depth=2", CoverageResult.class));
+	}
+
+	private void fetchMutationResult(BuildDetails result, BuildWithDetails build) throws IOException, URISyntaxException, DocumentException {
+		List<Artifact> artifactList = build.getArtifacts();
+		Artifact artifact = artifactList.stream().filter(a -> a.getFileName().equals("mutations.xml")).findFirst().orElse(null);
+		if (artifact != null) {
+			MutationResult mutationResult = new MutationResult();
+			ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+			outputStream.write(build.downloadArtifact(artifact));
+			String mutationReport = new String(outputStream.toByteArray());
+			Document document = DocumentHelper.parseText(mutationReport);
+			Element root = document.getRootElement();
+			for (Object element : root.elements("mutation")) {
+				if (element instanceof Element) {
+					Attribute attribute = ((Element) element).attribute("detected");
+					if (attribute != null) {
+						mutationResult.setMutations(mutationResult.getMutations() + 1);
+						if ("true".equals(attribute.getValue())) {
+							mutationResult.setDetectedMutations(mutationResult.getDetectedMutations() + 1);
+						}
+					}
+				}
+			}
+			result.setMutationResult(mutationResult);
+		}
+	}
+
+	private String readTemplate() throws IOException {
+		StringBuilder stringBuilder = new StringBuilder();
+		InputStream inputStream = JenkinsApiImpl.class.getClassLoader().getResourceAsStream("jenkins/config.xml");
+		BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
+		String buffer;
+		while ((buffer = bufferedReader.readLine()) != null) {
+			stringBuilder.append(buffer).append("\n");
+		}
+		return stringBuilder.toString();
+	}
+}

+ 38 - 0
src/main/java/com/moekr/moocoder/logic/api/impl/PoolingJenkinsHttpClient.java

@@ -0,0 +1,38 @@
+package com.moekr.moocoder.logic.api.impl;
+
+import com.offbytwo.jenkins.client.JenkinsHttpClient;
+import org.apache.commons.lang.StringUtils;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.impl.auth.BasicScheme;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
+import org.apache.http.protocol.BasicHttpContext;
+import org.apache.http.protocol.HttpContext;
+
+import java.net.URI;
+
+class PoolingJenkinsHttpClient extends JenkinsHttpClient {
+	PoolingJenkinsHttpClient(URI uri, String username, String password) {
+		super(uri, poolingHttpClient(uri, username, password));
+		if (StringUtils.isNotBlank(username)) {
+			HttpContext localContext = new BasicHttpContext();
+			localContext.setAttribute("preemptive-auth", new BasicScheme());
+			super.setLocalContext(localContext);
+		}
+	}
+
+	private static CloseableHttpClient poolingHttpClient(URI uri, String username, String password) {
+		HttpClientBuilder builder = addAuthentication(HttpClientBuilder.create(), uri, username, password);
+		PoolingHttpClientConnectionManager manager = new PoolingHttpClientConnectionManager();
+		manager.setMaxTotal(256);
+		manager.setDefaultMaxPerRoute(128);
+		builder.setDefaultRequestConfig(RequestConfig.custom()
+				.setConnectTimeout(3000)
+				.setConnectionRequestTimeout(5000)
+				.setSocketTimeout(10000)
+				.build())
+				.setConnectionManager(manager);
+		return builder.build();
+	}
+}

+ 18 - 0
src/main/java/com/moekr/moocoder/logic/api/vo/BuildDetails.java

@@ -0,0 +1,18 @@
+package com.moekr.moocoder.logic.api.vo;
+
+import com.offbytwo.jenkins.model.BuildResult;
+import com.offbytwo.jenkins.model.TestResult;
+import lombok.Data;
+import lombok.ToString;
+
+@Data
+@ToString(exclude = "consoleOutput")
+public class BuildDetails {
+	private int number;
+	private long duration;
+	private BuildResult buildResult;
+	private String consoleOutput;
+	private TestResult testResult;
+	private CoverageResult coverageResult;
+	private MutationResult mutationResult;
+}

+ 27 - 0
src/main/java/com/moekr/moocoder/logic/api/vo/CoverageResult.java

@@ -0,0 +1,27 @@
+package com.moekr.moocoder.logic.api.vo;
+
+import com.offbytwo.jenkins.model.BaseModel;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.Setter;
+import lombok.ToString;
+
+import java.util.List;
+
+@Setter
+@EqualsAndHashCode(callSuper = true)
+@ToString
+public class CoverageResult extends BaseModel {
+	private Results results;
+
+	public List<CoverageResultElement> getElements() {
+		return results.getElements();
+	}
+
+	@Data
+	@EqualsAndHashCode
+	@ToString
+	private static class Results {
+		private List<CoverageResultElement> elements;
+	}
+}

+ 11 - 0
src/main/java/com/moekr/moocoder/logic/api/vo/CoverageResultElement.java

@@ -0,0 +1,11 @@
+package com.moekr.moocoder.logic.api.vo;
+
+import lombok.Data;
+
+@Data
+public class CoverageResultElement {
+	private String name;
+	private int numerator;
+	private int denominator;
+	private int ratio;
+}

+ 10 - 0
src/main/java/com/moekr/moocoder/logic/api/vo/GitlabUser.java

@@ -0,0 +1,10 @@
+package com.moekr.moocoder.logic.api.vo;
+
+import lombok.Data;
+
+@Data
+public class GitlabUser {
+	private Integer id;
+	private Integer namespace;
+	private String token;
+}

+ 9 - 0
src/main/java/com/moekr/moocoder/logic/api/vo/MutationResult.java

@@ -0,0 +1,9 @@
+package com.moekr.moocoder.logic.api.vo;
+
+import lombok.Data;
+
+@Data
+public class MutationResult {
+	private int mutations;
+	private int detectedMutations;
+}

+ 10 - 0
src/main/java/com/moekr/moocoder/logic/service/CommitService.java

@@ -0,0 +1,10 @@
+package com.moekr.moocoder.logic.service;
+
+import com.moekr.moocoder.logic.vo.CommitVO;
+import com.moekr.moocoder.util.exceptions.ServiceException;
+
+public interface CommitService {
+	CommitVO retrieve(int userId, int commitId) throws ServiceException;
+
+	CommitVO retrieve(int commitId) throws ServiceException;
+}

+ 46 - 0
src/main/java/com/moekr/moocoder/logic/service/CourseService.java

@@ -0,0 +1,46 @@
+package com.moekr.moocoder.logic.service;
+
+import com.moekr.moocoder.data.entity.Course;
+import com.moekr.moocoder.util.exceptions.ServiceException;
+import org.springframework.data.domain.Page;
+
+/**
+ * @author XiangzheXu
+ * create-time: 2018/12/9
+ */
+public interface CourseService {
+    /**
+     * 创建课程
+     */
+    public Course createCourse(Course toCreate) throws ServiceException;
+
+    /**
+     * 查找某一老师新建的所有课程
+     * @param teacherID 老师ID
+     */
+    public Page<Course> findCoursesByTeacherID(Integer teacherID, int page, int limit);
+
+    /**
+     * 获取所有课程
+     */
+    public Page<Course> findCourses(int page, int limit);
+
+    /**
+     * 更新某一课程的信息
+     * @param updated 更新的课程信息,以课程ID来区分不同课程
+     */
+    public Course updateCourse(Course updated) throws ServiceException;
+
+    /**
+     * 学生选课
+     * @param code 选课码
+     * @param userID 学生ID
+     */
+    public Course addCourse(String code, Integer userID) throws ServiceException;
+
+    /**
+     * 判断选课码是否重复
+     */
+    public boolean codeDuplicate(String code);
+
+}

+ 27 - 0
src/main/java/com/moekr/moocoder/logic/service/ExamService.java

@@ -0,0 +1,27 @@
+package com.moekr.moocoder.logic.service;
+
+import com.moekr.moocoder.logic.vo.ExamVO;
+import com.moekr.moocoder.util.enums.ExamStatus;
+import com.moekr.moocoder.util.exceptions.ServiceException;
+import com.moekr.moocoder.web.dto.ExamDTO;
+import org.springframework.data.domain.Page;
+
+public interface ExamService {
+	ExamVO create(int userId, ExamDTO examDTO) throws ServiceException;
+
+	Page<ExamVO> retrievePage(int userId, int page, int limit, boolean joined, ExamStatus status) throws ServiceException;
+
+	Page<ExamVO> retrievePage(int userId, int page, int limit, ExamStatus status) throws ServiceException;
+
+	Page<ExamVO> retrievePage(int page, int limit, ExamStatus status) throws ServiceException;
+
+	ExamVO retrieve(int userId, int examId) throws ServiceException;
+
+	ExamVO retrieve(int examId) throws ServiceException;
+
+	ExamVO update(int userId, int examId, ExamDTO examDTO) throws ServiceException;
+
+	void delete(int userId, int examId) throws ServiceException;
+
+	void join(int userId, int examId) throws ServiceException;
+}

+ 7 - 0
src/main/java/com/moekr/moocoder/logic/service/MailService.java

@@ -0,0 +1,7 @@
+package com.moekr.moocoder.logic.service;
+
+import org.springframework.web.servlet.ModelAndView;
+
+public interface MailService {
+	void send(String to, String personal, String subject, ModelAndView modelAndView);
+}

+ 7 - 0
src/main/java/com/moekr/moocoder/logic/service/NotifyService.java

@@ -0,0 +1,7 @@
+package com.moekr.moocoder.logic.service;
+
+public interface NotifyService {
+	void webHook(int id, String commitHash);
+
+	void callback(int id, int buildNumber);
+}

+ 17 - 0
src/main/java/com/moekr/moocoder/logic/service/OnlineCodeService.java

@@ -0,0 +1,17 @@
+package com.moekr.moocoder.logic.service;
+
+import com.moekr.moocoder.logic.vo.CodeVO;
+import com.moekr.moocoder.util.exceptions.ServiceException;
+import com.moekr.moocoder.web.dto.CodeDTO;
+
+import java.util.Map;
+
+public interface OnlineCodeService {
+    CodeVO getCode(int userId, int examId) throws ServiceException;
+
+    CodeVO getCode(int userId, int examId, int questionId) throws ServiceException;
+
+    void submitCode(int userId, int examId, Map<Integer, CodeDTO> codeDTOs) throws ServiceException;
+
+    void submitCode(int userId, int examId, int questionId, CodeDTO codeDTO) throws ServiceException;
+}

+ 21 - 0
src/main/java/com/moekr/moocoder/logic/service/ProblemService.java

@@ -0,0 +1,21 @@
+package com.moekr.moocoder.logic.service;
+
+import com.moekr.moocoder.logic.vo.ProblemVO;
+import com.moekr.moocoder.util.enums.ProblemType;
+import com.moekr.moocoder.util.exceptions.ServiceException;
+import com.moekr.moocoder.web.dto.ProblemDTO;
+import org.springframework.data.domain.Page;
+
+public interface ProblemService {
+	ProblemVO create(int userId, ProblemDTO problemDTO, byte[] content) throws ServiceException;
+
+	Page<ProblemVO> retrievePage(int userId, int page, int limit, ProblemType type) throws ServiceException;
+
+	Page<ProblemVO> retrievePage(int page, int limit, ProblemType type) throws ServiceException;
+
+	ProblemVO retrieve(int userId, int problemId) throws ServiceException;
+
+	void update(int userId, int problemId, String path, byte[] content) throws ServiceException;
+
+	void delete(int userId, int problemId) throws ServiceException;
+}

+ 10 - 0
src/main/java/com/moekr/moocoder/logic/service/RecordService.java

@@ -0,0 +1,10 @@
+package com.moekr.moocoder.logic.service;
+
+import com.moekr.moocoder.logic.vo.RecordVO;
+import com.moekr.moocoder.util.exceptions.ServiceException;
+
+public interface RecordService {
+	RecordVO retrieve(int userId, int recordId) throws ServiceException;
+
+	RecordVO retrieve(int recordId) throws ServiceException;
+}

+ 46 - 0
src/main/java/com/moekr/moocoder/logic/service/ResultService.java

@@ -0,0 +1,46 @@
+package com.moekr.moocoder.logic.service;
+
+import com.moekr.moocoder.logic.vo.ResultVO;
+import com.moekr.moocoder.logic.vo.internal.ExamTestCaseVO;
+import com.moekr.moocoder.logic.vo.internal.ResultDetailVO;
+import com.moekr.moocoder.util.exceptions.ServiceException;
+
+import java.util.List;
+import java.util.Map;
+
+public interface ResultService {
+	ResultVO retrieve(int userId, int resultId) throws ServiceException;
+
+	List<ResultVO> retrieveByOwner(int userId) throws ServiceException;
+
+	List<ResultVO> retrieveByExam(int userId, int examId) throws ServiceException;
+
+	ResultVO retrieve(int resultId) throws ServiceException;
+
+	List<ResultVO> retrieveByExam(int examId) throws ServiceException;
+
+	/**
+	 * 获取考试每道题目的测试用例名称
+	 * 考试中没有运行过的题目的测试用例留空
+	 * @param examId
+	 * @return
+	 * @throws ServiceException
+	 */
+	ExamTestCaseVO retrieveDetailedTestCaseByExam(int examId) throws ServiceException;
+
+	/**
+	 * 获取考试详细成绩
+	 * @param examId
+	 * @return
+	 * @throws ServiceException
+	 */
+	List<ResultDetailVO> retrieveDetailedTestCaseResultByExam(int examId) throws ServiceException;
+
+	/**
+	 * 获取没有提交过的学生的记录
+	 * @param examId
+	 * @return
+	 * @throws ServiceException
+	 */
+	List<ResultVO> retrieveWithNoCommitByExam(int examId) throws ServiceException;
+}

+ 7 - 0
src/main/java/com/moekr/moocoder/logic/service/StatisticService.java

@@ -0,0 +1,7 @@
+package com.moekr.moocoder.logic.service;
+
+import com.moekr.moocoder.logic.vo.StatisticVO;
+
+public interface StatisticService {
+	StatisticVO statistic();
+}

+ 26 - 0
src/main/java/com/moekr/moocoder/logic/service/UserService.java

@@ -0,0 +1,26 @@
+package com.moekr.moocoder.logic.service;
+
+import com.moekr.moocoder.logic.vo.UserVO;
+import com.moekr.moocoder.util.exceptions.ServiceException;
+import com.moekr.moocoder.web.dto.UserDTO;
+import com.moekr.moocoder.web.dto.form.ChangePasswordForm;
+import com.moekr.moocoder.web.dto.form.RegisterForm;
+import org.springframework.data.domain.Page;
+
+public interface UserService {
+	UserVO create(UserDTO userDTO) throws ServiceException;
+
+	Page<UserVO> retrievePage(int page, int limit, String search) throws ServiceException;
+
+	UserVO retrieve(int userId) throws ServiceException;
+
+	void delete(int userId) throws ServiceException;
+
+	UserVO register(RegisterForm form) throws ServiceException;
+
+	void changePassword(int userId, ChangePasswordForm form) throws ServiceException;
+
+	void resetPassword(int userId) throws ServiceException;
+
+	UserVO login(String username, String password) throws ServiceException;
+}

+ 123 - 0
src/main/java/com/moekr/moocoder/logic/service/impl/BuildManager.java

@@ -0,0 +1,123 @@
+package com.moekr.moocoder.logic.service.impl;
+
+import com.moekr.moocoder.data.dao.CommitDAO;
+import com.moekr.moocoder.data.dao.RecordDAO;
+import com.moekr.moocoder.data.dao.ResultDAO;
+import com.moekr.moocoder.data.entity.*;
+import com.moekr.moocoder.logic.api.JenkinsApi;
+import com.moekr.moocoder.logic.api.vo.BuildDetails;
+import com.moekr.moocoder.util.ApplicationProperties;
+import com.moekr.moocoder.util.ToolKit;
+import com.moekr.moocoder.util.enums.BuildStatus;
+import com.moekr.moocoder.util.enums.ProblemType;
+import com.moekr.moocoder.util.problem.evaluator.Evaluator;
+import com.moekr.moocoder.util.problem.helper.ProblemHelper;
+import com.offbytwo.jenkins.model.QueueItem;
+import lombok.extern.apachecommons.CommonsLog;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@Component
+@CommonsLog
+public class BuildManager {
+	private final CommitDAO commitDAO;
+	private final RecordDAO recordDAO;
+	private final ResultDAO resultDAO;
+	private final JenkinsApi jenkinsApi;
+	private final ApplicationProperties properties;
+
+	@Autowired
+	public BuildManager(CommitDAO commitDAO, RecordDAO recordDAO, ResultDAO resultDAO, JenkinsApi jenkinsApi, ApplicationProperties properties) {
+		this.commitDAO = commitDAO;
+		this.recordDAO = recordDAO;
+		this.resultDAO = resultDAO;
+		this.jenkinsApi = jenkinsApi;
+		this.properties = properties;
+	}
+
+	@SuppressWarnings("BooleanMethodIsAlwaysInverted")
+	@Transactional
+	public boolean invokeNextBuild(int resultId) {
+		Result result = resultDAO.findById(resultId);
+		if (result == null) return false;
+		Record record;
+		while ((record = nextUnbuiltRecord(result)) != null) {
+			try {
+				QueueItem item = jenkinsApi.invokeBuild(resultId, buildParam(record));
+				record.setNumber(item.getExecutable().getNumber().intValue());
+				record.setStatus(BuildStatus.RUNNING);
+				return true;
+			} catch (Exception e) {
+				log.error("触发构建" + record.getId() + "时发生异常" + ToolKit.format(e));
+				record.setConsoleOutput("系统内部发生未知异常,请尝试重新提交!");
+				record.setStatus(BuildStatus.FAILURE);
+			} finally {
+				recordDAO.save(record);
+			}
+		}
+		return false;
+	}
+
+	@Transactional
+	public boolean recordBuildResult(int id, int buildNumber) {
+		Record record = recordDAO.findByResultIdAndBuildNumber(id, buildNumber);
+		if (record == null || record.getStatus() != BuildStatus.RUNNING) {
+			return false;
+		}
+		Problem problem = record.getProblem();
+		ProblemType type = problem.getType();
+		try {
+			BuildDetails buildDetails = jenkinsApi.fetchBuildDetails(id, buildNumber, type.getTarget(), true);
+			Evaluator evaluator = type.getEvaluator();
+			evaluator.evaluate(record, buildDetails);
+		} catch (Exception e) {
+			log.error("获取构建记录" + buildNumber + "时发生异常" + ToolKit.format(e));
+			record.setConsoleOutput("系统内部发生未知异常,请尝试重新提交!");
+			record.setStatus(BuildStatus.FAILURE);
+		} finally {
+			recordDAO.save(record);
+		}
+		return true;
+	}
+
+	private Record nextUnbuiltRecord(Result result) {
+		Commit commit = commitDAO.findFirstUnfinishedByResult(result);
+		if (commit == null) return null;
+		Record unbuiltRecord = null;
+		for (Record record : commit.getRecords()) {
+			switch (record.getStatus()) {
+				case RUNNING:
+					return null;
+				case WAITING:
+					if (unbuiltRecord == null) {
+						unbuiltRecord = record;
+					}
+			}
+		}
+		return unbuiltRecord;
+	}
+
+	private Map<String, String> buildParam(Record record) {
+		Exam exam = record.getCommit().getResult().getExam();
+		User user = record.getCommit().getResult().getOwner();
+		Problem problem = record.getProblem();
+		String uniqueName = problem.getUniqueName();
+		ProblemHelper helper = problem.getType().getHelper();
+		Map<String, String> param = new HashMap<>();
+		param.put("PROB_TARGET", problem.getType().getTarget());
+		param.put("GIT_URL", properties.getGitlab().getHost() + "/" + user.getUsername() + "/" + exam.getUuid());
+		param.put("COMMIT_HASH", record.getCommit().getHash());
+		param.put("DOCKER_IMAGE", properties.getDocker().getRegistry() + "/" + problem.getImageName() + ":" + problem.getImageTag());
+		param.put("EXECUTE_SHELL", "#!/bin/bash\n"
+				+ "cp -R /var/ws/code/" + uniqueName + "/* /var/ws/tmp/ &>/dev/null || :\n"
+				+ "pushd " + uniqueName + " &>/dev/null\n"
+				+ "cp --parents -R ." + helper.editableDirectory() + "/*" + " /var/ws/tmp/ &>/dev/null || :\n"
+				+ "popd &>/dev/null\n"
+				+ helper.runScript(uniqueName));
+		return param;
+	}
+}

+ 61 - 0
src/main/java/com/moekr/moocoder/logic/service/impl/ClosedBuildChecker.java

@@ -0,0 +1,61 @@
+package com.moekr.moocoder.logic.service.impl;
+
+import com.moekr.moocoder.data.dao.RecordDAO;
+import com.moekr.moocoder.data.entity.Record;
+import com.moekr.moocoder.data.entity.Result;
+import com.moekr.moocoder.logic.api.JenkinsApi;
+import com.moekr.moocoder.logic.api.vo.BuildDetails;
+import com.moekr.moocoder.logic.service.NotifyService;
+import com.moekr.moocoder.util.enums.BuildStatus;
+import com.moekr.moocoder.util.enums.ProblemType;
+import lombok.extern.apachecommons.CommonsLog;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+@Component
+@CommonsLog
+public class ClosedBuildChecker {
+	private static final int THRESHOLD = 3;
+
+	private final RecordDAO recordDAO;
+	private final JenkinsApi jenkinsApi;
+	private final NotifyService notifyService;
+
+	private Map<Integer, Integer> history;
+
+	public ClosedBuildChecker(RecordDAO recordDAO, JenkinsApi jenkinsApi, NotifyService notifyService) {
+		this.recordDAO = recordDAO;
+		this.jenkinsApi = jenkinsApi;
+		this.notifyService = notifyService;
+		this.history = new HashMap<>();
+	}
+
+	@Transactional
+	public void check() {
+		Set<Record> records = recordDAO.findAllByStatus(BuildStatus.RUNNING);
+		Map<Integer, Integer> newHistory = records.stream().collect(Collectors.toMap(Record::getId, r -> 1));
+		history.forEach((key, value) -> newHistory.computeIfPresent(key, (k, v) -> value + v));
+		history = newHistory;
+		history.entrySet().stream().filter(e -> e.getValue() > THRESHOLD).forEach(e -> invokeCallback(e.getKey()));
+	}
+
+	private void invokeCallback(int recordId) {
+		Record record = recordDAO.findById(recordId);
+		if (record.getStatus() != BuildStatus.RUNNING) return;
+		Result result = record.getCommit().getResult();
+		ProblemType type = record.getProblem().getType();
+		try {
+			BuildDetails details = jenkinsApi.fetchBuildDetails(result.getId(), record.getNumber(), type.getTarget(), false);
+			// 表明构建仍在进行,此时不应该触发回调
+			if (details.getBuildResult() == null) return;
+		} catch (Exception e) {
+			return;
+		}
+		notifyService.callback(result.getId(), record.getNumber());
+	}
+}

+ 57 - 0
src/main/java/com/moekr/moocoder/logic/service/impl/ClosedExamChecker.java

@@ -0,0 +1,57 @@
+package com.moekr.moocoder.logic.service.impl;
+
+import com.moekr.moocoder.data.dao.ExamDAO;
+import com.moekr.moocoder.data.dao.ResultDAO;
+import com.moekr.moocoder.data.entity.Exam;
+import com.moekr.moocoder.data.entity.Result;
+import com.moekr.moocoder.logic.api.GitlabApi;
+import com.moekr.moocoder.logic.api.JenkinsApi;
+import com.moekr.moocoder.util.ToolKit;
+import com.moekr.moocoder.util.enums.ExamStatus;
+import lombok.extern.apachecommons.CommonsLog;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Component
+@CommonsLog
+public class ClosedExamChecker {
+	private final ExamDAO examDAO;
+	private final ResultDAO resultDAO;
+	private final GitlabApi gitlabApi;
+	private final JenkinsApi jenkinsApi;
+
+	public ClosedExamChecker(ExamDAO examDAO, ResultDAO resultDAO, GitlabApi gitlabApi, JenkinsApi jenkinsApi) {
+		this.examDAO = examDAO;
+		this.resultDAO = resultDAO;
+		this.gitlabApi = gitlabApi;
+		this.jenkinsApi = jenkinsApi;
+	}
+
+	@Transactional
+	public void check() {
+		List<Exam> examList = examDAO.findAllByStatus(ExamStatus.AVAILABLE);
+		// XXX 等待30分钟所有提交均完成测试
+		LocalDateTime now = LocalDateTime.now().minusMinutes(30);
+		examList = examList.stream()
+				.filter(e -> e.getEndAt().isBefore(now))
+				.peek(e -> e.setStatus(ExamStatus.CLOSED))
+				.collect(Collectors.toList());
+		examList = examDAO.saveAll(examList);
+		for (Exam exam : examList) {
+			for (Result result : exam.getResults()) {
+				try {
+					gitlabApi.archiveProject(result.getId());
+					jenkinsApi.deleteJob(result.getId());
+					result.setDeleted(true);
+				} catch (Exception e) {
+					log.error("归档试卷" + result.getId() + "时发生异常" + ToolKit.format(e));
+				}
+			}
+			resultDAO.saveAll(exam.getResults());
+		}
+	}
+}

+ 121 - 0
src/main/java/com/moekr/moocoder/logic/service/impl/CommitManager.java

@@ -0,0 +1,121 @@
+package com.moekr.moocoder.logic.service.impl;
+
+import com.moekr.moocoder.data.dao.CommitDAO;
+import com.moekr.moocoder.data.dao.RecordDAO;
+import com.moekr.moocoder.data.dao.ResultDAO;
+import com.moekr.moocoder.data.entity.*;
+import com.moekr.moocoder.logic.api.GitlabApi;
+import com.moekr.moocoder.util.enums.BuildStatus;
+import lombok.extern.apachecommons.CommonsLog;
+import org.gitlab4j.api.GitLabApiException;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.LocalDateTime;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+@Component
+@CommonsLog
+public class CommitManager {
+	private final CommitDAO commitDAO;
+	private final RecordDAO recordDAO;
+	private final ResultDAO resultDAO;
+	private final GitlabApi gitlabApi;
+
+	@Autowired
+	public CommitManager(CommitDAO commitDAO, RecordDAO recordDAO, ResultDAO resultDAO, GitlabApi gitlabApi) {
+		this.commitDAO = commitDAO;
+		this.recordDAO = recordDAO;
+		this.resultDAO = resultDAO;
+		this.gitlabApi = gitlabApi;
+	}
+
+	@Transactional
+	public boolean initializeCommit(int resultId, String commitHash) {
+		Result result = resultDAO.findById(resultId);
+		Exam exam = result == null ? null : result.getExam();
+		LocalDateTime now = LocalDateTime.now();
+		if (exam != null && exam.getEndAt().isAfter(now)) {
+			Set<Problem> problems = calculateProblemsToBuild(result, commitHash);
+			Commit newCommit = new Commit();
+			newCommit.setHash(commitHash);
+			newCommit.setResult(result);
+			newCommit = commitDAO.save(newCommit);
+			for (Problem problem : problems) {
+				Record record = new Record();
+				record.setNumber(-1);
+				record.setCommit(newCommit);
+				record.setProblem(problem);
+				recordDAO.save(record);
+			}
+			result.setLastCommitAt(now);
+			resultDAO.save(result);
+			return true;
+		}
+		return false;
+	}
+
+	@Transactional
+	public boolean finalizeCommit(int resultId) {
+		Result result = resultDAO.findById(resultId);
+		if (result == null) {
+			return false;
+		}
+		Commit commit = commitDAO.findFirstUnfinishedByResult(result);
+		if (commit == null) {
+			return false;
+		}
+		if (commit.getRecords().stream().anyMatch(r -> r.getStatus() == BuildStatus.WAITING || r.getStatus() == BuildStatus.RUNNING)) {
+			return false;
+		}
+		Map<Problem, Record> recordMap = commit.getRecords().stream()
+				.collect(Collectors.toMap(Record::getProblem, r -> r));
+		Exam exam = result.getExam();
+		Set<Problem> problems = exam.getProblems();
+		int totalScore = 0;
+		for (Problem problem : problems) {
+			Record record = recordMap.get(problem);
+			if (record == null) {
+				record = recordDAO.findLastBuiltByResultAndProblem(result, problem);
+			}
+			if (record != null) {
+				totalScore = totalScore + record.getScore();
+			}
+		}
+		commit.setScore(totalScore / problems.size());
+		commit.setFinished(true);
+		commit = commitDAO.save(commit);
+		if (commit.getScore() > result.getScore()) {
+			result.setScore(commit.getScore());
+			resultDAO.save(result);
+		}
+		return true;
+	}
+
+	private Set<Problem> calculateProblemsToBuild(Result result, String commitHash) {
+		Exam exam = result.getExam();
+		Commit lastCommit = commitDAO.findFirstByResultOrderByIdDesc(result);
+		String lastHash = lastCommit == null ? exam.getInitialHash() : lastCommit.getHash();
+		Map<String, Problem> problemMap = exam.getProblems().stream()
+				.collect(Collectors.toMap(Problem::getUniqueName, p -> p));
+		Set<Problem> problems;
+		try {
+			Set<String> difference = gitlabApi.compare(result.getId(), lastHash, commitHash);
+			problems = difference.stream()
+					.map(problemMap::get)
+					.filter(Objects::nonNull)
+					.collect(Collectors.toSet());
+		} catch (GitLabApiException e) {
+			problems = new HashSet<>();
+		}
+		if (problems.isEmpty()) {
+			problems = exam.getProblems();
+		}
+		return problems;
+	}
+}

+ 40 - 0
src/main/java/com/moekr/moocoder/logic/service/impl/CommitServiceImpl.java

@@ -0,0 +1,40 @@
+package com.moekr.moocoder.logic.service.impl;
+
+import com.moekr.moocoder.data.dao.CommitDAO;
+import com.moekr.moocoder.data.entity.Commit;
+import com.moekr.moocoder.logic.service.CommitService;
+import com.moekr.moocoder.logic.vo.CommitVO;
+import com.moekr.moocoder.util.exceptions.AccessDeniedException;
+import com.moekr.moocoder.util.exceptions.Asserts;
+import com.moekr.moocoder.util.exceptions.ServiceException;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+@Service
+public class CommitServiceImpl implements CommitService {
+	private final CommitDAO commitDAO;
+
+	@Autowired
+	public CommitServiceImpl(CommitDAO commitDAO) {
+		this.commitDAO = commitDAO;
+	}
+
+	@Override
+	public CommitVO retrieve(int userId, int commitId) throws ServiceException {
+		Commit commit = commitDAO.findById(commitId);
+		Asserts.notNull(commit, "所选的提交记录不存在");
+		if (commit.getResult().getOwner().getId() == userId) {
+			return new CommitVO(commit);
+		} else if (commit.getResult().getExam().getCreator().getId() == userId) {
+			return new CommitVO(commit);
+		}
+		throw new AccessDeniedException();
+	}
+
+	@Override
+	public CommitVO retrieve(int commitId) throws ServiceException {
+		Commit commit = commitDAO.findById(commitId);
+		Asserts.notNull(commit, "所选的提交记录不存在");
+		return new CommitVO(commit);
+	}
+}

+ 109 - 0
src/main/java/com/moekr/moocoder/logic/service/impl/CourseServiceImpl.java

@@ -0,0 +1,109 @@
+package com.moekr.moocoder.logic.service.impl;
+
+import com.moekr.moocoder.data.dao.CourseDAO;
+import com.moekr.moocoder.data.dao.UserDAO;
+import com.moekr.moocoder.data.entity.Course;
+import com.moekr.moocoder.data.entity.User;
+import com.moekr.moocoder.logic.service.CourseService;
+import com.moekr.moocoder.util.enums.UserRole;
+import com.moekr.moocoder.util.exceptions.ServiceException;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+
+/**
+ * @author XiangzheXu
+ * create-time: 2018/12/9
+ */
+@Component
+public class CourseServiceImpl implements CourseService {
+    private static final Sort PAGE_SORT = Sort.by(Sort.Direction.DESC, "id");
+    private CourseDAO courseDAO;
+    private UserDAO userDAO;
+
+    @Autowired
+    public CourseServiceImpl(CourseDAO courseDAO, UserDAO userDAO) {
+        this.courseDAO = courseDAO;
+        this.userDAO = userDAO;
+    }
+
+
+
+    @Override
+    public Course createCourse(Course toCreate) throws ServiceException {
+        //新建课程,应该由数据库自动生成ID
+        toCreate.setId(null);
+        Course courseByCode = courseDAO.getCourseByCode(toCreate.getCode());
+        if (courseByCode != null) {
+            throw new ServiceException("选课码重复");
+        }
+        return courseDAO.save(toCreate);
+    }
+
+    @Override
+    public Page<Course> findCoursesByTeacherID(Integer teacherID, int page, int limit) {
+        Pageable pageable = PageRequest.of(page, limit, PAGE_SORT);
+        return courseDAO.getAllByTeacherID(teacherID,pageable);
+    }
+
+    @Override
+    public Page<Course> findCourses(int page, int limit){
+        Pageable pageable = PageRequest.of(page, limit, PAGE_SORT);
+        return courseDAO.findAll(pageable);
+    }
+
+    @Override
+    public Course updateCourse(Course updated) throws ServiceException {
+        Course courseByCode = courseDAO.getCourseByCode(updated.getCode());
+        if (courseByCode != null) {
+            throw new ServiceException("选课码重复");
+        }
+        Course courseByID = courseDAO.getCourseById(updated.getId());
+        if (courseByID == null) {
+            throw new ServiceException("此课程ID不存在");
+        }
+        courseByID.setCode(updated.getCode());
+        courseByID.setName(updated.getName());
+        return courseDAO.save(courseByID);
+    }
+
+    /**
+     * 学生选课
+     *
+     * @param code   选课码
+     * @param userID 学生ID
+     */
+    @Override
+    public Course addCourse(String code, Integer userID) throws ServiceException {
+        User student = userDAO.findById(userID).orElse(null);
+        if (student == null || !student.getRole().equals(UserRole.STUDENT)) {
+            throw new ServiceException("只有学生才可以选课");
+        }
+        Course courseToAdd = courseDAO.getCourseByCode(code);
+        if (courseToAdd == null) {
+            throw new ServiceException("此课程不存在");
+        }
+        List<User> students = courseToAdd.getStudents();
+        if (students.contains(student)) {
+            throw new ServiceException("已经加入此课程");
+        }
+        students.add(student);
+//        Set<Course> courses = student.getCourses();
+//        courses.add(courseToAdd);
+
+//        userDAO.save(student);
+
+        return courseDAO.save(courseToAdd);
+    }
+
+    @Override
+    public boolean codeDuplicate(String code) {
+        Course courseByCode = courseDAO.getCourseByCode(code);
+        return courseByCode != null;
+    }
+}

+ 62 - 0
src/main/java/com/moekr/moocoder/logic/service/impl/DockerImageBuilder.java

@@ -0,0 +1,62 @@
+package com.moekr.moocoder.logic.service.impl;
+
+import com.moekr.moocoder.data.dao.ProblemDAO;
+import com.moekr.moocoder.data.entity.Problem;
+import com.moekr.moocoder.logic.api.DockerApi;
+import com.moekr.moocoder.logic.storage.StorageProvider;
+import lombok.extern.apachecommons.CommonsLog;
+import org.apache.commons.io.FileUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
+import org.zeroturnaround.zip.ZipUtil;
+
+import java.io.*;
+import java.nio.file.Files;
+import java.time.LocalDateTime;
+
+@Component
+@CommonsLog
+public class DockerImageBuilder {
+	private static final String TEMP_PREFIX = "MOOCODER-DOCKER-";
+
+	private final ProblemDAO problemDAO;
+	private final DockerApi dockerApi;
+	private final StorageProvider storageProvider;
+
+
+	@Autowired
+	public DockerImageBuilder(ProblemDAO problemDAO, DockerApi dockerApi, StorageProvider storageProvider) {
+		this.problemDAO = problemDAO;
+		this.dockerApi = dockerApi;
+		this.storageProvider = storageProvider;
+	}
+
+	@Transactional(rollbackFor = Exception.class)
+	public void buildDockerImage(int problemId) throws Exception {
+		Problem problem = problemDAO.findById(problemId);
+		problem.setModifiedAt(LocalDateTime.now());
+		File tempDir = Files.createTempDirectory(TEMP_PREFIX).toFile();
+		try {
+			buildDockerImage(tempDir, problem);
+			problemDAO.save(problem);
+		} finally {
+			FileUtils.deleteDirectory(tempDir);
+		}
+	}
+
+	private void buildDockerImage(File tempDir, Problem problem) throws Exception {
+		File problemDir = new File(new File(tempDir, "code"), problem.getUniqueName());
+		if (!problemDir.mkdirs()) {
+			throw new IOException("创建临时文件夹失败!");
+		}
+		byte[] content = storageProvider.fetch(problem.getUniqueName() + ".zip");
+		ByteArrayInputStream inputStream = new ByteArrayInputStream(content);
+		ZipUtil.unpack(inputStream, problemDir);
+		File dockerFile = new File(tempDir, "Dockerfile");
+		try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(dockerFile)))) {
+			writer.write(problem.getType().getHelper().dockerFile(problem.getUniqueName()));
+		}
+		dockerApi.build(tempDir.getAbsolutePath(), problem.getImageName(), problem.getImageTag());
+	}
+}

+ 77 - 0
src/main/java/com/moekr/moocoder/logic/service/impl/ExamPaperBuilder.java

@@ -0,0 +1,77 @@
+package com.moekr.moocoder.logic.service.impl;
+
+import com.moekr.moocoder.data.dao.ExamDAO;
+import com.moekr.moocoder.data.entity.Exam;
+import com.moekr.moocoder.data.entity.Problem;
+import com.moekr.moocoder.logic.storage.StorageProvider;
+import com.moekr.moocoder.util.enums.ExamStatus;
+import lombok.extern.apachecommons.CommonsLog;
+import org.apache.commons.io.FileUtils;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
+import org.zeroturnaround.zip.ZipUtil;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.Set;
+
+@Component
+@CommonsLog
+public class ExamPaperBuilder {
+	private static final String TEMP_PREFIX = "MOOCODER-PAPER-";
+
+	private final ExamDAO examDAO;
+	private final StorageProvider provider;
+	private final GitHelper helper;
+
+	@Autowired
+	public ExamPaperBuilder(ExamDAO examDAO, GitHelper helper, StorageProvider provider) {
+		this.examDAO = examDAO;
+		this.helper = helper;
+		this.provider = provider;
+	}
+
+	@Transactional
+	public void buildPaper(int examId) throws IOException, GitAPIException {
+		Exam exam = examDAO.findById(examId);
+		if (exam.getStatus() != ExamStatus.PREPARING) return;
+		File tempDir = Files.createTempDirectory(TEMP_PREFIX).toFile();
+		try {
+			releaseCode(exam.getProblems(), tempDir);
+			exam.setInitialHash(helper.push(tempDir, exam.getUuid()));
+			exam.setStatus(ExamStatus.AVAILABLE);
+		} catch (Exception e) {
+			exam.setStatus(ExamStatus.UNAVAILABLE);
+			throw e;
+		} finally {
+			examDAO.save(exam);
+			FileUtils.deleteDirectory(tempDir);
+		}
+	}
+
+	private void releaseCode(Set<Problem> problems, File codeDir) throws IOException {
+		FileUtils.cleanDirectory(codeDir);
+		for (Problem problem : problems) {
+			File problemDir = new File(codeDir, problem.getUniqueName());
+			if (!problemDir.mkdir()) {
+				throw new IOException("创建临时文件夹失败!");
+			}
+			byte[] content = provider.fetch(problem.getUniqueName() + ".zip");
+			ByteArrayInputStream inputStream = new ByteArrayInputStream(content);
+			ZipUtil.unpack(inputStream, problemDir);
+			for (String path : problem.getPrivateFiles()) {
+				if (File.separatorChar != '/') {
+					path = path.replace('/', File.separatorChar);
+				}
+				File privateFile = new File(problemDir, path);
+				if (privateFile.exists() && !privateFile.delete()) {
+					log.error("删除私有文件[" + privateFile.getAbsolutePath() + "]失败!");
+				}
+			}
+		}
+	}
+}

+ 238 - 0
src/main/java/com/moekr/moocoder/logic/service/impl/ExamServiceImpl.java

@@ -0,0 +1,238 @@
+package com.moekr.moocoder.logic.service.impl;
+
+import com.moekr.moocoder.data.dao.*;
+import com.moekr.moocoder.data.entity.*;
+import com.moekr.moocoder.logic.AsyncWrapper;
+import com.moekr.moocoder.logic.api.GitlabApi;
+import com.moekr.moocoder.logic.api.JenkinsApi;
+import com.moekr.moocoder.logic.service.ExamService;
+import com.moekr.moocoder.logic.vo.ExamVO;
+import com.moekr.moocoder.logic.vo.JoinedExamVO;
+import com.moekr.moocoder.util.ApplicationProperties;
+import com.moekr.moocoder.util.ToolKit;
+import com.moekr.moocoder.util.enums.ExamStatus;
+import com.moekr.moocoder.util.enums.UserRole;
+import com.moekr.moocoder.util.exceptions.*;
+import com.moekr.moocoder.web.dto.ExamDTO;
+import lombok.extern.apachecommons.CommonsLog;
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+@Service
+@CommonsLog
+public class ExamServiceImpl implements ExamService {
+	private static final Sort PAGE_SORT = Sort.by(Sort.Direction.DESC, "id");
+
+	private final UserDAO userDAO;
+	private final CourseDAO courseDAO;
+	private final ProblemDAO problemDAO;
+	private final ExamDAO examDAO;
+	private final ResultDAO resultDAO;
+	private final GitlabApi gitlabApi;
+	private final JenkinsApi jenkinsApi;
+	private final ExamPaperBuilder paperBuilder;
+	private final AsyncWrapper asyncWrapper;
+	private final ApplicationProperties properties;
+
+	@Autowired
+	public ExamServiceImpl(UserDAO userDAO, CourseDAO courseDAO, ProblemDAO problemDAO, ExamDAO examDAO, ResultDAO resultDAO,
+						   GitlabApi gitlabApi, JenkinsApi jenkinsApi, ExamPaperBuilder paperBuilder, AsyncWrapper asyncWrapper, ApplicationProperties properties) {
+		this.userDAO = userDAO;
+		this.courseDAO = courseDAO;
+		this.problemDAO = problemDAO;
+		this.examDAO = examDAO;
+		this.resultDAO = resultDAO;
+		this.gitlabApi = gitlabApi;
+		this.jenkinsApi = jenkinsApi;
+		this.paperBuilder = paperBuilder;
+		this.asyncWrapper = asyncWrapper;
+		this.properties = properties;
+	}
+
+	@Override
+	@Transactional
+	public ExamVO create(int userId, ExamDTO examDTO) throws ServiceException {
+		User user = userDAO.findById(userId);
+		List<Problem> problemList = problemDAO.findAllById(examDTO.getProblems());
+		Set<Problem> problemSet = problemList.stream()
+				.filter(p -> p.getCreator() == null || p.getCreator().getId() == userId)
+				.collect(Collectors.toSet());
+		if (examDTO.getProblems().size() != problemSet.size()) {
+			throw new InvalidRequestException("存在无效的题目!");
+		}
+		Exam exam = new Exam();
+		BeanUtils.copyProperties(examDTO, exam);
+		exam.setUuid(ToolKit.randomUUID());
+		try {
+			exam.setId(gitlabApi.createProject(exam.getUuid()));
+		} catch (Exception e) {
+			throw new ServiceException("创建试题时发生异常" + ToolKit.format(e));
+		}
+		exam.setCreator(user);
+		exam.setProblems(problemSet);
+		asyncWrapper.asyncInvoke(() -> paperBuilder.buildPaper(exam.getId()), 1000);
+
+		return new ExamVO(examDAO.save(exam),  getCourse(exam));
+	}
+
+	private Course getCourse(Exam exam) {
+		Course courseById = courseDAO.getCourseById(exam.getCourseId());
+
+		return courseById;
+	}
+
+	@Override
+	public Page<ExamVO> retrievePage(int userId, int page, int limit, boolean joined, ExamStatus status) {
+		Pageable pageable = PageRequest.of(page, limit, PAGE_SORT);
+		Page<Exam> pageResult;
+		if (joined) {
+			pageResult = examDAO.findAllJoined(userId, pageable);
+		} else if (status == null) {
+//			pageResult = examDAO.findAll(pageable);
+			pageResult = examDAO.findAllExcludeClosed(userId, pageable);
+		} else {
+//			pageResult = examDAO.findAllByActualStatus(status, pageable);
+			pageResult = examDAO.findAllByActualStatus(userId, status, pageable);
+		}
+		return pageResult.map(e -> convert(userId, e));
+	}
+
+	@Override
+	public Page<ExamVO> retrievePage(int userId, int page, int limit, ExamStatus status) {
+		User user = userDAO.findById(userId);
+		Pageable pageable = PageRequest.of(page, limit, PAGE_SORT);
+		Page<Exam> pageResult;
+		if (status == null) {
+			pageResult = examDAO.findAllByCreator(user, pageable);
+		} else {
+			pageResult = examDAO.findAllByCreatorAndActualStatus(user, status, pageable);
+		}
+		return pageResult.map(e -> convert(user, e));
+	}
+
+	@Override
+	public Page<ExamVO> retrievePage(int page, int limit, ExamStatus status) {
+		Pageable pageable = PageRequest.of(page, limit, PAGE_SORT);
+		Page<Exam> pageResult;
+		if (status == null) {
+			pageResult = examDAO.findAll(pageable);
+		} else {
+			pageResult = examDAO.findAllByActualStatus(status, pageable);
+		}
+		return pageResult.map(exam -> new JoinedExamVO(exam, getCourse(exam)));
+	}
+
+	@Override
+	public ExamVO retrieve(int userId, int examId) throws ServiceException {
+		Exam exam = examDAO.findById(examId);
+		Asserts.notNull(exam, "所选考试不存在");
+		return convert(userId, exam);
+	}
+
+	@Override
+	public ExamVO retrieve(int examId) throws ServiceException {
+		Exam exam = examDAO.findById(examId);
+		Asserts.notNull(exam, "所选考试不存在");
+		return new JoinedExamVO(exam, getCourse(exam));
+	}
+
+	private ExamVO convert(int userId, Exam exam) {
+		return convert(userDAO.findById(userId), exam);
+	}
+
+	private ExamVO convert(User user, Exam exam) {
+		Result result = resultDAO.findByOwnerAndExam(user, exam);
+		if (result != null) {
+			String url = properties.getGitlab().getGitProxy() + "/" + result.getOwner().getUsername() + "/" + exam.getUuid();
+			return new JoinedExamVO(exam, url, result, getCourse(exam));
+		} else if (exam.getCreator().getId().equals(user.getId())) {
+			return new JoinedExamVO(exam, getCourse(exam));
+		} else {
+			return new ExamVO(exam, getCourse(exam));
+		}
+	}
+
+	@Override
+	@Transactional
+	public ExamVO update(int userId, int examId, ExamDTO examDTO) throws ServiceException {
+		Exam exam = examDAO.findById(examId);
+		Asserts.notNull(exam, "所选考试不存在");
+		checkAccess(userId, exam);
+		exam.setStartAt(examDTO.getStartAt());
+		exam.setEndAt(examDTO.getEndAt());
+		return new ExamVO(examDAO.save(exam), getCourse(exam));
+	}
+
+	@Override
+	@Transactional
+	public void delete(int userId, int examId) throws ServiceException {
+		Exam exam = examDAO.findById(examId);
+		Asserts.notNull(exam, "所选考试不存在");
+		checkAccess(userId, exam);
+		for (Result result : exam.getResults()) {
+			try {
+				gitlabApi.deleteProject(result.getId());
+				if (!result.isDeleted()) {
+					jenkinsApi.deleteJob(result.getId());
+				}
+			} catch (Exception e) {
+				log.error("删除试卷" + result.getId() + "时发生异常" + ToolKit.format(e));
+			}
+		}
+		try {
+			gitlabApi.deleteProject(exam.getId());
+		} catch (Exception e) {
+			log.error("删除试题" + exam.getId() + "时发生异常" + ToolKit.format(e));
+		}
+		examDAO.delete(exam);
+	}
+
+	@Override
+	@Transactional
+	public void join(int userId, int examId) throws ServiceException {
+		User user = userDAO.findById(userId);
+		Exam exam = examDAO.findById(examId);
+		Asserts.notNull(exam, "所选考试不存在");
+		if (resultDAO.findByOwnerAndExam(user, exam) != null) {
+			throw new AlreadyInExaminationException();
+		} else if (user.getRole() == UserRole.TEACHER && !user.equals(exam.getCreator())) {
+			throw new AccessDeniedException();
+		} else if (exam.getStatus() == ExamStatus.PREPARING) {
+			throw new EntityNotAvailableException("考试正在准备中!");
+		} else if (exam.getStatus() == ExamStatus.CLOSED) {
+			throw new EntityNotAvailableException("考试已经结束!");
+		}
+		Result result = new Result();
+		try {
+			result.setId(gitlabApi.forkProject(userId, examId, user.getNamespace()));
+			jenkinsApi.createJob(result.getId());
+		} catch (Exception e) {
+			throw new ServiceException("复制试卷时发生异常" + ToolKit.format(e));
+		}
+		result.setOwner(user);
+		result.setExam(exam);
+		resultDAO.save(result);
+	}
+
+	private void checkAccess(int userId, Exam exam) throws AccessDeniedException {
+		if (userId != 0) {
+			checkAccess(userDAO.findById(userId), exam);
+		}
+	}
+
+	private void checkAccess(User user, Exam exam) throws AccessDeniedException {
+		if (user != null && !exam.getCreator().equals(user)) {
+			throw new AccessDeniedException();
+		}
+	}
+}

+ 50 - 0
src/main/java/com/moekr/moocoder/logic/service/impl/GitHelper.java

@@ -0,0 +1,50 @@
+package com.moekr.moocoder.logic.service.impl;
+
+import com.moekr.moocoder.util.ApplicationProperties;
+import com.moekr.moocoder.util.ApplicationProperties.Gitlab;
+import org.apache.commons.io.FileUtils;
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.transport.CredentialsProvider;
+import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+
+@Component
+public class GitHelper {
+	private static final String TEMP_PREFIX = "MOOCODER-REPOSITORY-";
+	private static final String COMMIT_MESSAGE = "初始化";
+
+	private final Gitlab gitlab;
+	private final CredentialsProvider provider;
+
+	@Autowired
+	public GitHelper(ApplicationProperties properties) {
+		this.gitlab = properties.getGitlab();
+		this.provider = new UsernamePasswordCredentialsProvider(gitlab.getUsername(), gitlab.getToken());
+	}
+
+	public String push(File sourceDir, String project) throws GitAPIException, IOException {
+		String uri = gitlab.getHost() + "/" + gitlab.getUsername() + "/" + project + ".git";
+		File tempDir = Files.createTempDirectory(TEMP_PREFIX).toFile();
+		try {
+			return push(sourceDir, tempDir, uri);
+		} finally {
+			FileUtils.deleteDirectory(tempDir);
+		}
+	}
+
+	private String push(File sourceDir, File tempDir, String uri) throws GitAPIException, IOException {
+		Git targetRepo = Git.cloneRepository().setURI(uri).setDirectory(tempDir).setCredentialsProvider(provider).call();
+		FileUtils.copyDirectory(sourceDir, tempDir);
+		targetRepo.add().addFilepattern(".").call();
+		String hash = targetRepo.commit().setMessage(COMMIT_MESSAGE).call().getName();
+		targetRepo.push().setRemote("origin").setForce(true).setPushAll().setCredentialsProvider(provider).call();
+		targetRepo.close();
+		return hash;
+	}
+}

+ 72 - 0
src/main/java/com/moekr/moocoder/logic/service/impl/MailServiceImpl.java

@@ -0,0 +1,72 @@
+package com.moekr.moocoder.logic.service.impl;
+
+import com.moekr.moocoder.logic.service.MailService;
+import com.moekr.moocoder.util.ApplicationProperties;
+import com.moekr.moocoder.util.ApplicationProperties.Mail;
+import lombok.extern.apachecommons.CommonsLog;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.mail.MailException;
+import org.springframework.mail.javamail.JavaMailSender;
+import org.springframework.mail.javamail.MimeMessageHelper;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Service;
+import org.springframework.web.servlet.ModelAndView;
+import org.thymeleaf.TemplateEngine;
+import org.thymeleaf.context.Context;
+
+import javax.mail.MessagingException;
+import javax.mail.internet.InternetAddress;
+import javax.mail.internet.MimeMessage;
+import java.io.UnsupportedEncodingException;
+import java.util.Locale;
+
+@Service
+@CommonsLog
+public class MailServiceImpl implements MailService {
+	private final TemplateEngine templateEngine;
+	private final JavaMailSender mailSender;
+
+	private final String from;
+	private final String personal;
+
+	@Autowired
+	public MailServiceImpl(TemplateEngine templateEngine, JavaMailSender mailSender, ApplicationProperties properties) {
+		this.templateEngine = templateEngine;
+		this.mailSender = mailSender;
+
+		Mail mail = properties.getMail();
+		this.from = mail.getFrom();
+		this.personal = mail.getPersonal();
+	}
+
+	@Override
+	@Async
+	public void send(String to, String personal, String subject, ModelAndView modelAndView) {
+		if (StringUtils.isBlank(from)) return;
+		MimeMessage message = mailSender.createMimeMessage();
+		MimeMessageHelper helper = new MimeMessageHelper(message);
+		try {
+			try {
+				helper.setFrom(from, this.personal);
+			} catch (UnsupportedEncodingException e) {
+				helper.setFrom(from);
+			}
+			try {
+				helper.setTo(new InternetAddress(to, personal));
+			} catch (UnsupportedEncodingException e) {
+				helper.setTo(to);
+			}
+			helper.setSubject(subject);
+			helper.setText(templateEngine.process(modelAndView.getViewName(), new Context(Locale.CHINA, modelAndView.getModel())), true);
+		} catch (MessagingException e) {
+			log.warn("生成发送给[" + to + "(" + personal + ")]的邮件[" + subject + "]时发生异常:" + e.getMessage());
+			return;
+		}
+		try {
+			mailSender.send(message);
+		} catch (MailException e) {
+			log.warn("投递发送给[" + to + "(" + personal + ")]的邮件[" + subject + "]时发生异常:" + e.getMessage());
+		}
+	}
+}

+ 58 - 0
src/main/java/com/moekr/moocoder/logic/service/impl/NotifyServiceImpl.java

@@ -0,0 +1,58 @@
+package com.moekr.moocoder.logic.service.impl;
+
+import com.moekr.moocoder.logic.service.NotifyService;
+import lombok.extern.apachecommons.CommonsLog;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Service;
+
+@Service
+@CommonsLog
+public class NotifyServiceImpl implements NotifyService {
+	private final BuildManager buildManager;
+	private final CommitManager commitManager;
+
+	@Autowired
+	public NotifyServiceImpl(BuildManager buildManager, CommitManager commitManager) {
+		this.buildManager = buildManager;
+		this.commitManager = commitManager;
+	}
+
+	@Override
+	@Async
+	public void webHook(int id, String commitHash) {
+		// 尝试创建一个新的提交记录
+		if (commitManager.initializeCommit(id, commitHash)) {
+			// 提交记录创建成功
+			// 尝试触发第一次构建
+			if (!buildManager.invokeNextBuild(id)) {
+				// 触发构建失败,可能的原因:
+				// 1、当前考生在当前考试中已经有构建正在进行
+				// 2、新创建的提交记录中所有构建均触发失败
+				// 立即关闭该提交
+				commitManager.finalizeCommit(id);
+			}
+		}
+	}
+
+	@Override
+	@Async
+	public void callback(int id, int buildNumber) {
+		// 尝试记录构建结果
+		if (buildManager.recordBuildResult(id, buildNumber)) {
+			// 记录构建结果成功
+			// 尝试触发下一次构建
+			while (!buildManager.invokeNextBuild(id)) {
+				// 触发构建失败,可能的原因:
+				// 1、当前运行的提交中没有可以运行的构建
+				// 2、当前运行的提交中所有剩余构建均触发失败
+				// 3、当前考生在当前考试中已经有构建正在进行
+				// 尝试关闭当前运行的提交
+				// 成功关闭当前运行的提交则继续尝试触发下一次构建
+				if (!commitManager.finalizeCommit(id)) {
+					break;
+				}
+			}
+		}
+	}
+}

+ 262 - 0
src/main/java/com/moekr/moocoder/logic/service/impl/OnlineCodeServiceImpl.java

@@ -0,0 +1,262 @@
+package com.moekr.moocoder.logic.service.impl;
+
+import com.moekr.moocoder.data.dao.ExamDAO;
+import com.moekr.moocoder.data.dao.ResultDAO;
+import com.moekr.moocoder.data.dao.UserDAO;
+import com.moekr.moocoder.data.entity.Exam;
+import com.moekr.moocoder.data.entity.Problem;
+import com.moekr.moocoder.data.entity.Result;
+import com.moekr.moocoder.data.entity.User;
+import com.moekr.moocoder.logic.api.GitlabApi;
+import com.moekr.moocoder.logic.service.OnlineCodeService;
+import com.moekr.moocoder.logic.vo.CodeVO;
+import com.moekr.moocoder.util.ToolKit;
+import com.moekr.moocoder.util.enums.ExamStatus;
+import com.moekr.moocoder.util.enums.FileType;
+import com.moekr.moocoder.util.exceptions.Asserts;
+import com.moekr.moocoder.util.exceptions.EntityNotAvailableException;
+import com.moekr.moocoder.util.exceptions.ServiceException;
+import com.moekr.moocoder.util.problem.helper.ProblemHelper;
+import com.moekr.moocoder.web.dto.CodeDTO;
+import lombok.extern.apachecommons.CommonsLog;
+import org.gitlab4j.api.GitLabApiException;
+import org.gitlab4j.api.models.CommitAction;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.io.UnsupportedEncodingException;
+import java.time.LocalDateTime;
+import java.util.*;
+import java.util.regex.Pattern;
+
+@Service
+@CommonsLog
+public class OnlineCodeServiceImpl implements OnlineCodeService {
+    private final UserDAO userDAO;
+    private final ExamDAO examDAO;
+    private final ResultDAO resultDAO;
+    private final GitlabApi gitlabApi;
+
+    private static final List<String> USELESS_FILE_PATH_PREFIX = Arrays.asList(
+            "/target/","/bin/","/out/","/.idea/"
+    );
+    private static final List<Pattern> USELESS_FILE_NAME_PATTERN = Arrays.asList(
+            Pattern.compile("^\\.(DS_Store|git|project|classpath|idea)$"),
+            Pattern.compile("^(Desktop\\.ini|Thumbs\\.db|__pycache__)$"),
+            Pattern.compile(".*\\.(iml|py[cod])$")
+    );
+
+    @Autowired
+    public OnlineCodeServiceImpl(UserDAO userDAO, ExamDAO examDAO, ResultDAO resultDAO, GitlabApi gitlabApi){
+        this.userDAO = userDAO;
+        this.examDAO = examDAO;
+        this.resultDAO = resultDAO;
+        this.gitlabApi = gitlabApi;
+    }
+
+    @Override
+    public CodeVO getCode(int userId, int examId) throws ServiceException {
+        Exam exam = examDAO.findById(examId);
+        Asserts.notNull(exam, "所选考试不存在");
+        User user = userDAO.findById(userId);
+        Result result = resultDAO.findByOwnerAndExam(user, exam);
+        Asserts.notNull(result, "不存在该场考试的记录");
+        if (exam.getStatus() == ExamStatus.PREPARING) {
+            throw new EntityNotAvailableException("考试正在准备中!");
+        } else if (exam.getStatus() == ExamStatus.CLOSED) {
+            throw new EntityNotAvailableException("考试已经结束!");
+        } else if(exam.getStatus() == ExamStatus.AVAILABLE){
+            LocalDateTime now = LocalDateTime.now();
+            if (now.isBefore(exam.getStartAt())) {
+                throw new EntityNotAvailableException("考试还未开始!");
+            } else if (now.isAfter(exam.getEndAt())) {
+                throw new EntityNotAvailableException("考试已经结束!");
+            }
+        }
+        List<CodeVO.NestedCodeItemVO> codeList = new ArrayList<>();
+        for(Problem problem:exam.getProblems()){
+            Map<String, String> editable = new HashMap<>();
+            Map<String, String> uneditable = new HashMap<>();
+            try {
+                String basePath = problem.getUniqueName();
+                List<String> filePaths = gitlabApi.getFilePaths(result.getId(), basePath, "master");
+                ProblemHelper helper = problem.getType().getHelper();
+                for (String filePath : filePaths) {
+                    String innerFilepath = filePath.substring(basePath.length());
+                    if (isUselessFile(innerFilepath))
+                        continue;
+                    String fileContent = gitlabApi.getEncodedRepositoryFile(result.getId(), "master", filePath);
+                    if (FileType.PUBLIC.equals(helper.fileType(innerFilepath))) {
+                        editable.put(innerFilepath, fileContent);
+                    } else {
+                        uneditable.put(innerFilepath, fileContent);
+                    }
+                }
+            } catch (GitLabApiException | UnsupportedEncodingException e) {
+                throw new ServiceException("从git获取题目时发生异常" + ToolKit.format(e));
+            }
+            CodeVO.NestedCodeItemVO itemVO = new CodeVO.NestedCodeItemVO(problem.getId(), problem.getType().getLanguage(), problem.getUniqueName(), editable, uneditable);
+            codeList.add(itemVO);
+        }
+        CodeVO codeVO = new CodeVO(examId, LocalDateTime.now(), exam.getStartAt(), exam.getEndAt(), codeList);
+        return codeVO;
+    }
+
+    private boolean isUselessFile(String filePath) {
+        for (String prefix : USELESS_FILE_PATH_PREFIX) {
+            if (filePath.startsWith(prefix)) {
+                return true;
+            }
+        }
+        int index = filePath.lastIndexOf("/")+1;
+        String fileName = filePath.substring(index);
+        for (Pattern pattern : USELESS_FILE_NAME_PATTERN) {
+            if (pattern.matcher(fileName).matches()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public CodeVO getCode(int userId, int examId, int questionId) throws ServiceException {
+        Exam exam = examDAO.findById(examId);
+        Asserts.notNull(exam, "所选考试不存在");
+        User user = userDAO.findById(userId);
+        Result result = resultDAO.findByOwnerAndExam(user, exam);
+        Asserts.notNull(result, "不存在该场考试的记录");
+        if (exam.getStatus() == ExamStatus.PREPARING) {
+            throw new EntityNotAvailableException("考试正在准备中!");
+        } else if (exam.getStatus() == ExamStatus.CLOSED) {
+            throw new EntityNotAvailableException("考试已经结束!");
+        } else if(exam.getStatus() == ExamStatus.AVAILABLE){
+            LocalDateTime now = LocalDateTime.now();
+            if (now.isBefore(exam.getStartAt())) {
+                throw new EntityNotAvailableException("考试还未开始!");
+            } else if (now.isAfter(exam.getEndAt())) {
+                throw new EntityNotAvailableException("考试已经结束!");
+            }
+        }
+        Problem problem = null;
+        for(Problem p:exam.getProblems()){
+            if(p.getId()==questionId){
+                problem = p;
+                break;
+            }
+        }
+        Asserts.notNull(problem, "所选试题不存在");
+        List<CodeVO.NestedCodeItemVO> codeList = new ArrayList<>();
+        Map<String, String> editable = new HashMap<>();
+        Map<String, String> uneditable = new HashMap<>();
+        try {
+            String basePath = problem.getUniqueName();
+            List<String> filePaths = gitlabApi.getFilePaths(result.getId(), basePath, "master");
+            ProblemHelper helper = problem.getType().getHelper();
+            for (String filePath : filePaths) {
+                String innerFilepath = filePath.substring(basePath.length());
+                if (isUselessFile(innerFilepath))
+                    continue;
+                String fileContent = gitlabApi.getEncodedRepositoryFile(result.getId(), "master", filePath);
+                if (FileType.PUBLIC.equals(helper.fileType(innerFilepath))) {
+                    editable.put(innerFilepath, fileContent);
+                } else {
+                    uneditable.put(innerFilepath, fileContent);
+                }
+            }
+        } catch (GitLabApiException | UnsupportedEncodingException e) {
+            throw new ServiceException("从git获取题目时发生异常" + ToolKit.format(e));
+        }
+        CodeVO.NestedCodeItemVO itemVO = new CodeVO.NestedCodeItemVO(questionId, problem.getType().getLanguage(), problem.getUniqueName(), editable, uneditable);
+        codeList.add(itemVO);
+        CodeVO codeVO = new CodeVO(examId, LocalDateTime.now(), exam.getStartAt(), exam.getEndAt(), codeList);
+        return codeVO;
+    }
+
+    @Override
+    public void submitCode(int userId, int examId, Map<Integer, CodeDTO> codeDTOs) throws ServiceException {
+        Exam exam = examDAO.findById(examId);
+        Asserts.notNull(exam, "所选考试不存在");
+        User user = userDAO.findById(userId);
+        Result result = resultDAO.findByOwnerAndExam(user, exam);
+        Asserts.notNull(result, "不存在该场考试的记录");
+        if (exam.getStatus() == ExamStatus.PREPARING) {
+            throw new EntityNotAvailableException("考试正在准备中!");
+        } else if (exam.getStatus() == ExamStatus.CLOSED) {
+            throw new EntityNotAvailableException("考试已经结束!");
+        } else if(exam.getStatus() == ExamStatus.AVAILABLE){
+            LocalDateTime now = LocalDateTime.now();
+            if (now.isBefore(exam.getStartAt())) {
+                throw new EntityNotAvailableException("考试还未开始!");
+            } else if (now.isAfter(exam.getEndAt())) {
+                throw new EntityNotAvailableException("考试已经结束!");
+            }
+        }
+        try {
+            List<CommitAction> actions = new ArrayList<>();
+            for(Map.Entry<Integer, CodeDTO> codeDTOEntry:codeDTOs.entrySet()) {
+                Problem problem = null;
+                for(Problem p:exam.getProblems()){
+                    if(p.getId()==codeDTOEntry.getKey()){
+                        problem = p;
+                        break;
+                    }
+                }
+                Asserts.notNull(problem, "所提交试题不存在");
+                for (Map.Entry<String, String> entry : codeDTOEntry.getValue().getCode().entrySet()) {
+                    CommitAction action = new CommitAction();
+                    action.setAction(CommitAction.Action.UPDATE);
+                    action.setEncoding(CommitAction.Encoding.TEXT);
+                    action.setFilePath("/" + problem.getUniqueName() + entry.getKey());
+                    action.setContent(entry.getValue());
+                    actions.add(action);
+                }
+            }
+            gitlabApi.createCommit(result.getId(), "master", "commit", user.getEmail(), user.getUsername(), actions);
+        } catch (GitLabApiException e) {
+            throw new ServiceException("向git提交题目时发生异常" + ToolKit.format(e));
+        }
+    }
+
+    @Override
+    public void submitCode(int userId, int examId, int questionId, CodeDTO codeDTO) throws ServiceException {
+        Exam exam = examDAO.findById(examId);
+        Asserts.notNull(exam, "所选考试不存在");
+        User user = userDAO.findById(userId);
+        Result result = resultDAO.findByOwnerAndExam(user, exam);
+        Asserts.notNull(result, "不存在该场考试的记录");
+        if (exam.getStatus() == ExamStatus.PREPARING) {
+            throw new EntityNotAvailableException("考试正在准备中!");
+        } else if (exam.getStatus() == ExamStatus.CLOSED) {
+            throw new EntityNotAvailableException("考试已经结束!");
+        } else if(exam.getStatus() == ExamStatus.AVAILABLE){
+            LocalDateTime now = LocalDateTime.now();
+            if (now.isBefore(exam.getStartAt())) {
+                throw new EntityNotAvailableException("考试还未开始!");
+            } else if (now.isAfter(exam.getEndAt())) {
+                throw new EntityNotAvailableException("考试已经结束!");
+            }
+        }
+        Problem problem = null;
+        for(Problem p:exam.getProblems()){
+            if(p.getId()==questionId){
+                problem = p;
+                break;
+            }
+        }
+        Asserts.notNull(problem, "所提交试题不存在");
+        try {
+            List<CommitAction> actions = new ArrayList<>();
+            for(Map.Entry<String, String> entry:codeDTO.getCode().entrySet()){
+                CommitAction action = new CommitAction();
+                action.setAction(CommitAction.Action.UPDATE);
+                action.setEncoding(CommitAction.Encoding.TEXT);
+                action.setFilePath("/" + problem.getUniqueName() + entry.getKey());
+                action.setContent(entry.getValue());
+                actions.add(action);
+            }
+            gitlabApi.createCommit(result.getId(), "master", "commit", user.getEmail(), user.getUsername(),actions);
+        } catch (GitLabApiException e) {
+            throw new ServiceException("向git提交题目时发生异常" + ToolKit.format(e));
+        }
+    }
+}

+ 151 - 0
src/main/java/com/moekr/moocoder/logic/service/impl/ProblemFormatter.java

@@ -0,0 +1,151 @@
+package com.moekr.moocoder.logic.service.impl;
+
+import com.moekr.moocoder.data.entity.Problem;
+import com.moekr.moocoder.util.ToolKit;
+import com.moekr.moocoder.util.enums.FileType;
+import com.moekr.moocoder.util.exceptions.MalformedProblemException;
+import com.moekr.moocoder.util.exceptions.ServiceException;
+import com.moekr.moocoder.util.problem.helper.ProblemHelper;
+import lombok.extern.apachecommons.CommonsLog;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.io.output.ByteArrayOutputStream;
+import org.zeroturnaround.zip.ZipUtil;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.regex.Pattern;
+import java.util.zip.ZipException;
+
+@CommonsLog
+public class ProblemFormatter {
+	private static final String TEMP_PREFIX = "MOOCODER-PROBLEM-";
+	private static final Charset CHARSET = Charset.forName("UTF-8");
+	private static final Charset ALT_CHARSET = Charset.forName("GBK");
+	private static final List<Pattern> USELESS_FILE_PATH_PATTERN = Arrays.asList(
+			Pattern.compile("^/target/"),
+			Pattern.compile("^/bin/"),
+			Pattern.compile("^/out/")
+	);
+	private static final List<Pattern> USELESS_FILE_NAME_PATTERN = Arrays.asList(
+			Pattern.compile("^\\.(DS_Store|git|project|classpath|idea)$"),
+			Pattern.compile("^(Desktop\\.ini|Thumbs\\.db|__pycache__)$"),
+			Pattern.compile("\\.(iml|py[cod])$")
+	);
+
+	public byte[] format(Problem problem, byte[] content) throws IOException, ServiceException {
+		if (content == null) {
+			throw new NullPointerException("待格式化的内容为Null!");
+		}
+		File tempDir = Files.createTempDirectory(TEMP_PREFIX).toFile();
+		try {
+			return format(problem, content, tempDir);
+		} finally {
+			FileUtils.deleteDirectory(tempDir);
+		}
+	}
+
+	private byte[] format(Problem problem, byte[] content, File tempDir) throws IOException, ServiceException {
+		ByteArrayInputStream inputStream = new ByteArrayInputStream(content);
+		try {
+			ZipUtil.unpack(inputStream, tempDir, CHARSET);
+		} catch (RuntimeException e1) {
+			log.debug("尝试使用UTF-8编码解压缩文件失败,切换至GBK编码" + ToolKit.format(e1));
+			FileUtils.cleanDirectory(tempDir);
+			inputStream = new ByteArrayInputStream(content);
+			try {
+				ZipUtil.unpack(inputStream, tempDir, ALT_CHARSET);
+			} catch (RuntimeException e2) {
+				log.debug("尝试使用GBK编码解压缩文件失败,文件可能损坏" + ToolKit.format(e2));
+				throw new ZipException("解压缩文件失败,文件可能损坏!");
+			}
+		}
+		deleteUselessFiles(tempDir);
+		ProblemHelper helper = problem.getType().getHelper();
+		List<String> fileList = listFiles(tempDir);
+		if (!helper.validate(fileList)) {
+			throw new MalformedProblemException("题目文件未能通过模式校验");
+		}
+		for (String file : fileList) {
+			FileType fileType = helper.fileType(file);
+			if (fileType == FileType.PUBLIC) {
+				problem.getPublicFiles().add(file);
+			} else if (fileType == FileType.PROTECTED) {
+				problem.getProtectedFiles().add(file);
+			} else if (fileType == FileType.PRIVATE) {
+				problem.getPrivateFiles().add(file);
+			}
+		}
+		ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+		ZipUtil.pack(tempDir, outputStream);
+		return outputStream.toByteArray();
+	}
+
+	private void deleteUselessFiles(File directory) throws IOException {
+		deleteUselessFiles(directory, directory);
+	}
+
+	private void deleteUselessFiles(File rootDir, File currentDir) throws IOException {
+		File[] uselessFiles = currentDir.listFiles((file) -> isUselessFile(rootDir, file));
+		if (uselessFiles == null) {
+			throw new IOException("枚举文件时发生异常!");
+		}
+		for (File uselessFile : uselessFiles) {
+			FileUtils.forceDelete(uselessFile);
+		}
+		File[] childrenDir = currentDir.listFiles(File::isDirectory);
+		if (childrenDir == null) {
+			throw new IOException("枚举文件时发生异常!");
+		}
+		for (File childDir : childrenDir) {
+			deleteUselessFiles(rootDir, childDir);
+		}
+	}
+
+	private boolean isUselessFile(File rootDir, File file) {
+		for (Pattern pattern : USELESS_FILE_NAME_PATTERN) {
+			if (pattern.matcher(file.getName()).matches()) {
+				return true;
+			}
+		}
+		String path = file.getAbsolutePath().substring(rootDir.getAbsolutePath().length());
+		if (file.isDirectory()) {
+			path = path + File.separator;
+		}
+		for (Pattern pattern : USELESS_FILE_PATH_PATTERN) {
+			if (pattern.matcher(path).matches()) {
+				return true;
+			}
+		}
+		return false;
+	}
+
+	private List<String> listFiles(File directory) throws IOException {
+		List<String> list = new ArrayList<>();
+		listFiles(directory, directory, list);
+		return list;
+	}
+
+	private void listFiles(File rootDir, File currentDir, List<String> list) throws IOException {
+		File[] files = currentDir.listFiles();
+		if (files == null) {
+			throw new IOException("枚举文件时发生异常!");
+		}
+		for (File file : files) {
+			if (file.isDirectory()) {
+				listFiles(rootDir, file, list);
+			} else {
+				String filePath = file.getAbsolutePath().substring(rootDir.getAbsolutePath().length());
+				if (File.separatorChar != '/') {
+					filePath = filePath.replace(File.separatorChar, '/');
+				}
+				list.add(filePath);
+			}
+		}
+	}
+}

+ 154 - 0
src/main/java/com/moekr/moocoder/logic/service/impl/ProblemServiceImpl.java

@@ -0,0 +1,154 @@
+package com.moekr.moocoder.logic.service.impl;
+
+import com.moekr.moocoder.data.dao.ProblemDAO;
+import com.moekr.moocoder.data.dao.UserDAO;
+import com.moekr.moocoder.data.entity.Problem;
+import com.moekr.moocoder.data.entity.User;
+import com.moekr.moocoder.logic.AsyncWrapper;
+import com.moekr.moocoder.logic.service.ProblemService;
+import com.moekr.moocoder.logic.storage.StorageProvider;
+import com.moekr.moocoder.logic.vo.ProblemVO;
+import com.moekr.moocoder.util.ToolKit;
+import com.moekr.moocoder.util.enums.ProblemType;
+import com.moekr.moocoder.util.exceptions.AccessDeniedException;
+import com.moekr.moocoder.util.exceptions.Asserts;
+import com.moekr.moocoder.util.exceptions.InvalidRequestException;
+import com.moekr.moocoder.util.exceptions.ServiceException;
+import com.moekr.moocoder.web.dto.ProblemDTO;
+import lombok.extern.apachecommons.CommonsLog;
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.Set;
+
+@Service
+@CommonsLog
+public class ProblemServiceImpl implements ProblemService {
+	private static final Sort PAGE_SORT = Sort.by(Sort.Direction.DESC, "id");
+
+	private final UserDAO userDAO;
+	private final ProblemDAO problemDAO;
+	private final DockerImageBuilder builder;
+	private final StorageProvider provider;
+	private final AsyncWrapper asyncWrapper;
+
+	private final ProblemFormatter formatter = new ProblemFormatter();
+	private final ProblemUpdater updater = new ProblemUpdater();
+
+	@Autowired
+	public ProblemServiceImpl(UserDAO userDAO, ProblemDAO problemDAO, DockerImageBuilder builder, StorageProvider provider, AsyncWrapper asyncWrapper) {
+		this.userDAO = userDAO;
+		this.problemDAO = problemDAO;
+		this.builder = builder;
+		this.provider = provider;
+		this.asyncWrapper = asyncWrapper;
+	}
+
+	@Override
+	@Transactional(rollbackFor = Exception.class)
+	public ProblemVO create(int userId, ProblemDTO problemDTO, byte[] content) throws ServiceException {
+		User user = userDAO.findById(userId);
+		Problem problem = new Problem();
+		BeanUtils.copyProperties(problemDTO, problem, "publicFiles", "protectedFiles", "privateFiles");
+		try {
+			content = formatter.format(problem, content);
+		} catch (IOException e) {
+			throw new ServiceException("格式化题目文件时发生异常" + ToolKit.format(e));
+		}
+		problem.setCreator(user);
+		problem = problemDAO.save(problem);
+		try {
+			provider.save(content, problem.getUniqueName() + ".zip");
+		} catch (IOException e) {
+			throw new ServiceException("保存题目文件时发生异常" + ToolKit.format(e));
+		}
+		int problemId = problem.getId();
+		asyncWrapper.asyncInvoke(() -> builder.buildDockerImage(problemId), 1000);
+		return new ProblemVO(problem);
+	}
+
+	@Override
+	public Page<ProblemVO> retrievePage(int userId, int page, int limit, ProblemType type) {
+		User user = userDAO.findById(userId);
+		Pageable pageable = PageRequest.of(page, limit, PAGE_SORT);
+		Page<Problem> pageResult;
+		if (type == null) {
+			pageResult = problemDAO.findAllByCreatorIsNullOrCreator(user, pageable);
+		} else {
+			pageResult = problemDAO.findAllByCreatorIsNullOrCreatorAndType(user, type, pageable);
+		}
+		return pageResult.map(ProblemVO::new);
+	}
+
+	@Override
+	public Page<ProblemVO> retrievePage(int page, int limit, ProblemType type) {
+		Pageable pageable = PageRequest.of(page, limit, PAGE_SORT);
+		Page<Problem> pageResult;
+		if (type == null) {
+			pageResult = problemDAO.findAll(pageable);
+		} else {
+			pageResult = problemDAO.findAllByType(type, pageable);
+		}
+		return pageResult.map(ProblemVO::new);
+	}
+
+	@Override
+	public ProblemVO retrieve(int userId, int problemId) throws ServiceException {
+		Problem problem = problemDAO.findById(problemId);
+		Asserts.notNull(problem, "所选题目不存在");
+		if (problem.getCreator() != null && userId != 0 && userId != problem.getCreator().getId()) {
+			throw new AccessDeniedException();
+		}
+		return new ProblemVO(problem);
+	}
+
+	@Override
+	public void update(int userId, int problemId, String path, byte[] content) throws ServiceException {
+		Problem problem = problemDAO.findById(problemId);
+		Asserts.notNull(problem, "所选题目不存在");
+		checkAccess(userId, problem);
+		Set<String> files = new HashSet<>();
+		files.addAll(problem.getPublicFiles());
+		files.addAll(problem.getProtectedFiles());
+		files.addAll(problem.getPrivateFiles());
+		if (files.contains(path)) {
+			try {
+				byte[] origin = provider.fetch(problem.getUniqueName() + ".zip");
+				provider.save(updater.update(origin, path, content), problem.getUniqueName() + ".zip");
+				asyncWrapper.asyncInvoke(() -> builder.buildDockerImage(problem.getId()));
+			} catch (IOException e) {
+				throw new ServiceException("更新题目文件时发生异常" + ToolKit.format(e));
+			}
+		} else {
+			throw new InvalidRequestException("所选题目中没有该文件");
+		}
+	}
+
+	@Override
+	@Transactional
+	public void delete(int userId, int problemId) throws ServiceException {
+		Problem problem = problemDAO.findById(problemId);
+		Asserts.notNull(problem, "所选题目不存在");
+		checkAccess(userId, problem);
+		if (!problem.getExamSet().isEmpty()) {
+			problem.setDeprecated(true);
+			problemDAO.save(problem);
+		} else {
+			problemDAO.delete(problem);
+		}
+	}
+
+	private void checkAccess(int userId, Problem problem) throws AccessDeniedException {
+		if (userId != 0 && (problem.getCreator() == null || problem.getCreator().getId() != userId)) {
+			throw new AccessDeniedException();
+		}
+	}
+}

+ 46 - 0
src/main/java/com/moekr/moocoder/logic/service/impl/ProblemUpdater.java

@@ -0,0 +1,46 @@
+package com.moekr.moocoder.logic.service.impl;
+
+import lombok.extern.apachecommons.CommonsLog;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.io.output.ByteArrayOutputStream;
+import org.zeroturnaround.zip.ZipUtil;
+
+import java.io.*;
+import java.nio.charset.Charset;
+import java.nio.file.Files;
+
+@CommonsLog
+public class ProblemUpdater {
+	private static final String TEMP_PREFIX = "MOOCODER-PROBLEM-";
+	private static final Charset CHARSET = Charset.forName("UTF-8");
+
+	public byte[] update(byte[] origin, String path, byte[] content) throws IOException {
+		if (origin == null) {
+			throw new NullPointerException("原始数据为Null!");
+		}
+		if (content == null) {
+			throw new NullPointerException("文件内容为Null!");
+		}
+		File tempDir = Files.createTempDirectory(TEMP_PREFIX).toFile();
+		try {
+			return update(tempDir, origin, path, content);
+		} finally {
+			FileUtils.deleteDirectory(tempDir);
+		}
+	}
+
+	private byte[] update(File tempDir, byte[] origin, String path, byte[] content) throws IOException {
+		ByteArrayInputStream inputStream = new ByteArrayInputStream(origin);
+		ZipUtil.unpack(inputStream, tempDir, CHARSET);
+		if (File.separatorChar != '/') {
+			path = path.replace('/', File.separatorChar);
+		}
+		File file = new File(tempDir, path);
+		try (OutputStream outputStream = new FileOutputStream(file)) {
+			outputStream.write(content);
+		}
+		ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+		ZipUtil.pack(tempDir, outputStream);
+		return outputStream.toByteArray();
+	}
+}

+ 40 - 0
src/main/java/com/moekr/moocoder/logic/service/impl/RecordServiceImpl.java

@@ -0,0 +1,40 @@
+package com.moekr.moocoder.logic.service.impl;
+
+import com.moekr.moocoder.data.dao.RecordDAO;
+import com.moekr.moocoder.data.entity.Record;
+import com.moekr.moocoder.logic.service.RecordService;
+import com.moekr.moocoder.logic.vo.RecordVO;
+import com.moekr.moocoder.util.exceptions.AccessDeniedException;
+import com.moekr.moocoder.util.exceptions.Asserts;
+import com.moekr.moocoder.util.exceptions.ServiceException;
+import lombok.extern.apachecommons.CommonsLog;
+import org.springframework.stereotype.Service;
+
+@Service
+@CommonsLog
+public class RecordServiceImpl implements RecordService {
+	private final RecordDAO recordDAO;
+
+	public RecordServiceImpl(RecordDAO recordDAO) {
+		this.recordDAO = recordDAO;
+	}
+
+	@Override
+	public RecordVO retrieve(int userId, int recordId) throws ServiceException {
+		Record record = recordDAO.findById(recordId);
+		Asserts.notNull(record, "选择的提交记录不存在");
+		if (record.getCommit().getResult().getOwner().getId() == userId) {
+			return new RecordVO(record);
+		} else if (record.getCommit().getResult().getExam().getCreator().getId() == userId) {
+			return new RecordVO(record);
+		}
+		throw new AccessDeniedException();
+	}
+
+	@Override
+	public RecordVO retrieve(int recordId) throws ServiceException {
+		Record record = recordDAO.findById(recordId);
+		Asserts.notNull(record, "选择的提交记录不存在");
+		return new RecordVO(record);
+	}
+}

+ 146 - 0
src/main/java/com/moekr/moocoder/logic/service/impl/ResultServiceImpl.java

@@ -0,0 +1,146 @@
+package com.moekr.moocoder.logic.service.impl;
+
+import com.moekr.moocoder.data.dao.*;
+import com.moekr.moocoder.data.entity.*;
+import com.moekr.moocoder.logic.service.ResultService;
+import com.moekr.moocoder.logic.vo.ResultVO;
+import com.moekr.moocoder.logic.vo.internal.ExamTestCaseVO;
+import com.moekr.moocoder.logic.vo.internal.ResultDetailVO;
+import com.moekr.moocoder.util.enums.BuildStatus;
+import com.moekr.moocoder.util.exceptions.AccessDeniedException;
+import com.moekr.moocoder.util.exceptions.Asserts;
+import com.moekr.moocoder.util.exceptions.ServiceException;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+@Service
+public class ResultServiceImpl implements ResultService {
+	private final ExamDAO examDAO;
+    private final CourseDAO courseDAO;
+	private final ResultDAO resultDAO;
+	private final RecordDAO recordDAO;
+	private final UserDAO userDAO;
+
+	@Autowired
+	public ResultServiceImpl(ExamDAO examDAO, CourseDAO courseDAO, ResultDAO resultDAO, RecordDAO recordDAO, UserDAO userDAO) {
+		this.examDAO = examDAO;
+		this.courseDAO = courseDAO;
+		this.resultDAO = resultDAO;
+		this.recordDAO = recordDAO;
+		this.userDAO = userDAO;
+	}
+
+	@Override
+	public ResultVO retrieve(int userId, int resultId) throws ServiceException {
+		Result result = resultDAO.findById(resultId);
+		Asserts.notNull(result, "所选的成绩不存在");
+		if (result.getOwner().getId() == userId) {
+			return new ResultVO(result, true);
+		} else if (result.getExam().getCreator().getId() == userId) {
+			return new ResultVO(result, true);
+		}
+		throw new AccessDeniedException();
+	}
+
+	@Override
+	public List<ResultVO> retrieveByOwner(int userId) throws ServiceException {
+		User user = userDAO.findById(userId);
+		Asserts.notNull(user, "所选用户不存在");
+		return resultDAO.findAllByOwner(user).stream().map(r -> new ResultVO(r, getCourse(r.getExam()), false)).collect(Collectors.toList());
+	}
+
+    private Course getCourse(Exam exam) {
+        Course course = courseDAO.getCourseById(exam.getCourseId());
+        return course;
+    }
+
+	@Override
+	public List<ResultVO> retrieveByExam(int userId, int examId) throws ServiceException {
+		Exam exam = examDAO.findById(examId);
+		Asserts.notNull(exam, "所选的考试不存在");
+		User user = userDAO.findById(userId);
+		return resultDAO.findAllByExamAndOwnerIsNot(exam, user).stream().map(r -> new ResultVO(r, false)).collect(Collectors.toList());
+	}
+
+	@Override
+	public ResultVO retrieve(int resultId) throws ServiceException {
+		Result result = resultDAO.findById(resultId);
+		Asserts.notNull(result, "所选的成绩不存在");
+		return new ResultVO(result, true);
+	}
+
+	@Override
+	public List<ResultVO> retrieveByExam(int examId) throws ServiceException {
+		Exam exam = examDAO.findById(examId);
+		Asserts.notNull(exam, "所选的考试不存在");
+		return resultDAO.findAllByExamAndOwnerIsNot(exam, exam.getCreator()).stream().map(r -> new ResultVO(r, false)).collect(Collectors.toList());
+	}
+
+    /**
+     * 考试中没有运行过的题目的测试用例留空
+     * @param examId
+     * @throws ServiceException
+     */
+	public ExamTestCaseVO retrieveDetailedTestCaseByExam(int examId) throws ServiceException {
+		Exam exam = examDAO.findById(examId);
+		Asserts.notNull(exam, "所选的考试不存在");
+        List<BuildStatus> buildStatuses = new ArrayList(){{
+            add(BuildStatus.SUCCESS);
+            add(BuildStatus.UNSTABLE);
+        }};
+        List<RecordDAO.TestCaseInterface> testCaseInterfaceList = recordDAO.findExamTestCase(exam, buildStatuses);
+        Map<Integer, String> problemNames = new HashMap<>();
+        Map<Integer, List<String>> examTestCases = new HashMap<>();
+        for (RecordDAO.TestCaseInterface testCaseInterface:testCaseInterfaceList) {
+            List<String> testCaseList = new ArrayList<>();
+            for (Record.Failure item:testCaseInterface.getTestCase()) {
+                testCaseList.add(item.getName());
+            }
+            testCaseList.sort(new Comparator<String>() {
+                @Override
+                public int compare(String o1, String o2) {
+                    return o1.compareTo(o2);
+                }
+            });
+            examTestCases.put(testCaseInterface.getProblem(), testCaseList);
+            problemNames.put(testCaseInterface.getProblem(), testCaseInterface.getProblemName());
+        }
+        // 添加没有运行过的题目
+        if (examTestCases.size()<exam.getProblems().size()) {
+            for (Problem problem:exam.getProblems()) {
+                if (!examTestCases.containsKey(problem.getId())) {
+                    examTestCases.put(problem.getId(), null);
+                }
+                if (!problemNames.containsKey(problem.getId())) {
+                    problemNames.put(problem.getId(), problem.getName());
+                }
+            }
+        }
+        return new ExamTestCaseVO(examId, problemNames, examTestCases);
+	}
+
+    /**
+     * 查询考试详细成绩,不包括未提交的人,和没有提交记录的题目的成绩
+     * @param examId
+     * @return
+     * @throws ServiceException
+     */
+	public List<ResultDetailVO> retrieveDetailedTestCaseResultByExam(int examId) throws ServiceException {
+        Exam exam = examDAO.findById(examId);
+        Asserts.notNull(exam, "所选的考试不存在");
+        List<RecordDAO.TestCaseResultInterface> testCaseResultInterfaces = recordDAO.findExamResultRecordsExcludeCreator(exam.getId(), exam.getCreator().getId());
+        int problemCount = exam.getProblems().size();
+        List<ResultDetailVO> resultDetailVOList = testCaseResultInterfaces.stream().collect(Collectors.groupingBy(RecordDAO.TestCaseResultInterface::getUserId))
+                .values().stream().map(t -> new ResultDetailVO(t, problemCount)).collect(Collectors.toList());
+        return resultDetailVOList;
+    }
+
+    public List<ResultVO> retrieveWithNoCommitByExam(int examId) throws ServiceException {
+        Exam exam = examDAO.findById(examId);
+        Asserts.notNull(exam, "所选的考试不存在");
+        return resultDAO.findAllWithNoCommitByExam(exam, exam.getCreator()).stream().map(r -> new ResultVO(r, false)).collect(Collectors.toList());
+    }
+}

+ 38 - 0
src/main/java/com/moekr/moocoder/logic/service/impl/StatisticServiceImpl.java

@@ -0,0 +1,38 @@
+package com.moekr.moocoder.logic.service.impl;
+
+import com.moekr.moocoder.data.dao.*;
+import com.moekr.moocoder.logic.service.StatisticService;
+import com.moekr.moocoder.logic.vo.StatisticVO;
+import com.moekr.moocoder.util.enums.ProblemType;
+import com.moekr.moocoder.util.enums.UserRole;
+import org.springframework.stereotype.Service;
+
+@Service
+public class StatisticServiceImpl implements StatisticService {
+	private final UserDAO userDAO;
+	private final ExamDAO examDAO;
+	private final ResultDAO resultDAO;
+	private final RecordDAO recordDAO;
+	private final ProblemDAO problemDAO;
+
+	public StatisticServiceImpl(UserDAO userDAO, ExamDAO examDAO, ResultDAO resultDAO, RecordDAO recordDAO, ProblemDAO problemDAO) {
+		this.userDAO = userDAO;
+		this.examDAO = examDAO;
+		this.resultDAO = resultDAO;
+		this.recordDAO = recordDAO;
+		this.problemDAO = problemDAO;
+	}
+
+	@Override
+	public StatisticVO statistic() {
+		StatisticVO statistic = new StatisticVO();
+		statistic.setStudentCount(userDAO.countByRole(UserRole.STUDENT));
+		statistic.setTeacherCount(userDAO.countByRole(UserRole.TEACHER));
+		statistic.setExaminationCount((int) examDAO.count());
+		statistic.setResultCount((int) resultDAO.count());
+		statistic.setRecordCount((int) recordDAO.count());
+		statistic.setJavaProblemCount(problemDAO.countByType(ProblemType.JAVA));
+		statistic.setPythonProblemCount(problemDAO.countByType(ProblemType.PYTHON));
+		return statistic;
+	}
+}

+ 179 - 0
src/main/java/com/moekr/moocoder/logic/service/impl/UserServiceImpl.java

@@ -0,0 +1,179 @@
+package com.moekr.moocoder.logic.service.impl;
+
+import com.moekr.moocoder.data.dao.UserDAO;
+import com.moekr.moocoder.data.entity.Result;
+import com.moekr.moocoder.data.entity.User;
+import com.moekr.moocoder.logic.api.GitlabApi;
+import com.moekr.moocoder.logic.api.JenkinsApi;
+import com.moekr.moocoder.logic.api.vo.GitlabUser;
+import com.moekr.moocoder.logic.service.MailService;
+import com.moekr.moocoder.logic.service.UserService;
+import com.moekr.moocoder.logic.vo.UserVO;
+import com.moekr.moocoder.util.ToolKit;
+import com.moekr.moocoder.util.enums.UserRole;
+import com.moekr.moocoder.util.exceptions.Asserts;
+import com.moekr.moocoder.util.exceptions.InvalidRequestException;
+import com.moekr.moocoder.util.exceptions.ServiceException;
+import com.moekr.moocoder.web.dto.UserDTO;
+import com.moekr.moocoder.web.dto.form.ChangePasswordForm;
+import com.moekr.moocoder.web.dto.form.RegisterForm;
+import org.apache.commons.codec.digest.DigestUtils;
+import org.gitlab4j.api.GitLabApiException;
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.ui.ModelMap;
+import org.springframework.util.Assert;
+import org.springframework.web.servlet.ModelAndView;
+
+import java.time.LocalDateTime;
+
+@Service
+public class UserServiceImpl implements UserService {
+	private static final Sort PAGE_SORT = Sort.by(Sort.Direction.ASC, "id");
+
+	private final UserDAO userDAO;
+	private final MailService mailService;
+	private final GitlabApi gitlabApi;
+	private final JenkinsApi jenkinsApi;
+
+	@Autowired
+	public UserServiceImpl(UserDAO userDAO, MailService mailService, GitlabApi gitlabApi, JenkinsApi jenkinsApi) {
+		this.userDAO = userDAO;
+		this.mailService = mailService;
+		this.gitlabApi = gitlabApi;
+		this.jenkinsApi = jenkinsApi;
+	}
+
+	@Override
+	@Transactional
+	public UserVO create(UserDTO userDTO) throws ServiceException {
+		return register(userDTO.getUsername(), userDTO.getEmail(), null,userDTO.getPhone(), userDTO.getRole());
+	}
+
+	@Override
+	public Page<UserVO> retrievePage(int page, int limit, String search) {
+		Pageable pageable = PageRequest.of(page, limit, PAGE_SORT);
+		Page<User> pageResult;
+		if (search == null || search.isEmpty()) {
+			pageResult = userDAO.findAll(pageable);
+		} else {
+			pageResult = userDAO.findAllByUsernameLike("%" + search + "%", pageable);
+		}
+		return pageResult.map(UserVO::new);
+	}
+
+	@Override
+	public UserVO retrieve(int userId) throws ServiceException {
+		User user = userDAO.findById(userId);
+		Asserts.notNull(user, "所选用户不存在");
+		return new UserVO(user);
+	}
+
+	@Override
+	@Transactional
+	public void delete(int userId) throws ServiceException {
+		User user = userDAO.findById(userId);
+		Assert.notNull(user, "找不到要删除的用户");
+		Assert.isTrue(user.getRole() == UserRole.STUDENT, "目标用户只能是学生");
+		try {
+			gitlabApi.deleteUser(userId);
+		} catch (Exception e) {
+			throw new ServiceException("删除GitLab用户时发生异常[" + e.getMessage() + "]");
+		}
+		for (Result result : user.getResultSet()) {
+			if (!result.isDeleted()) {
+				try {
+					jenkinsApi.deleteJob(result.getId());
+				} catch (Exception e) {
+					throw new ServiceException("删除Jenkins项目时发生异常[" + e.getMessage() + "]");
+				}
+			}
+		}
+		userDAO.delete(user);
+	}
+
+	@Override
+	@Transactional
+	public UserVO register(RegisterForm form) throws ServiceException {
+		return register(form.getUsername(), form.getEmail(), form.getPassword(),form.getPhone(), UserRole.STUDENT);
+	}
+
+	private UserVO register(String username, String email, String password, String phone,UserRole role) throws ServiceException {
+		User user = userDAO.findByUsername(username);
+		Asserts.isNull(user, "用户名已被使用!");
+		user = userDAO.findByEmail(email);
+		Asserts.isNull(user, "邮箱已被使用!");
+		String realPassword = password == null ? ToolKit.randomPassword() : password;
+		GitlabUser gitlabUser;
+		try {
+			gitlabUser = gitlabApi.createUser(username, email, realPassword);
+		} catch (GitLabApiException e) {
+			throw new ServiceException("创建GitLab用户时发生异常[" + e.getMessage() + "]");
+		}
+		user = new User();
+		BeanUtils.copyProperties(gitlabUser, user);
+		user.setUsername(username);
+		user.setEmail(email);
+		user.setPassword(DigestUtils.sha256Hex(realPassword));
+		user.setPhone(phone);
+		user.setRole(role);
+		user.setCreatedAt(LocalDateTime.now());
+		ModelMap model = new ModelMap();
+		model.addAttribute("username", username);
+		if (password == null) {
+			model.addAttribute("password", realPassword);
+		}
+		model.addAttribute("role", role);
+//		mailService.send(email, username, "注册成功 | MOOCODER", new ModelAndView("mail/register", model));
+		return new UserVO(userDAO.save(user));
+	}
+
+	@Override
+	public void changePassword(int userId, ChangePasswordForm form) throws ServiceException {
+		User user = userDAO.findById(userId);
+		if (!user.getPassword().equals(DigestUtils.sha256Hex(form.getOrigin()))) {
+			throw new InvalidRequestException("原密码验证失败,如忘记密码请联系管理员重置");
+		}
+		changePassword(user, form.getPassword(), false);
+	}
+
+	@Override
+	public void resetPassword(int userId) throws ServiceException {
+		User user = userDAO.findById(userId);
+		Asserts.notNull(user, "所选用户不存在");
+		String password = ToolKit.randomPassword();
+		changePassword(user, password, true);
+	}
+
+	@Override
+	public UserVO login(String username, String password) throws ServiceException{
+		User user=userDAO.findByUsername(username);
+		if (user.getPassword().equals(DigestUtils.sha256Hex(password))){
+			return new UserVO(user);
+		}
+		else {
+			throw new ServiceException();
+		}
+	}
+
+	private void changePassword(User user, String password, boolean reset) throws ServiceException {
+		try {
+			gitlabApi.changePassword(user.getId(), password);
+		} catch (GitLabApiException e) {
+			throw new ServiceException("修改GitLab用户密码时发生异常[" + e.getMessage() + "]");
+		}
+		user.setPassword(DigestUtils.sha256Hex(password));
+		user = userDAO.save(user);
+		ModelMap model = new ModelMap();
+		if (reset) {
+			model.addAttribute("password", password);
+		}
+		mailService.send(user.getEmail(), user.getUsername(), "密码变更提醒 | MOOCODER", new ModelAndView("mail/password", model));
+	}
+}

+ 12 - 0
src/main/java/com/moekr/moocoder/logic/storage/StorageProvider.java

@@ -0,0 +1,12 @@
+package com.moekr.moocoder.logic.storage;
+
+import java.io.IOException;
+import java.util.Map;
+
+public interface StorageProvider {
+	void initialize(Map<String, String> properties) throws IOException;
+
+	void save(byte[] content, String name) throws IOException;
+
+	byte[] fetch(String name) throws IOException;
+}

+ 45 - 0
src/main/java/com/moekr/moocoder/logic/storage/StorageProviderConfiguration.java

@@ -0,0 +1,45 @@
+package com.moekr.moocoder.logic.storage;
+
+import com.moekr.moocoder.logic.storage.impl.CosStorageProvider;
+import com.moekr.moocoder.logic.storage.impl.LocalStorageProvider;
+import com.moekr.moocoder.logic.storage.impl.OssStorageProvider;
+import com.moekr.moocoder.logic.storage.impl.QiniuStorageProvider;
+import com.moekr.moocoder.util.ApplicationProperties;
+import com.moekr.moocoder.util.ApplicationProperties.Storage;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.io.IOException;
+
+@Configuration
+public class StorageProviderConfiguration {
+	private final ApplicationProperties properties;
+
+	public StorageProviderConfiguration(ApplicationProperties properties) {
+		this.properties = properties;
+	}
+
+	@Bean
+	public StorageProvider storageProvider() throws IOException {
+		Storage storage = properties.getStorage();
+		StorageProvider storageProvider;
+		switch (storage.getType()) {
+			case "local":
+				storageProvider = new LocalStorageProvider();
+				break;
+			case "oss":
+				storageProvider = new OssStorageProvider();
+				break;
+			case "cos":
+				storageProvider = new CosStorageProvider();
+				break;
+			case "qiniu":
+				storageProvider = new QiniuStorageProvider();
+				break;
+			default:
+				throw new IllegalArgumentException("没有类型为[" + storage.getType() + "]的文件存储实现!");
+		}
+		storageProvider.initialize(storage.getProperties());
+		return storageProvider;
+	}
+}

+ 78 - 0
src/main/java/com/moekr/moocoder/logic/storage/impl/CosStorageProvider.java

@@ -0,0 +1,78 @@
+package com.moekr.moocoder.logic.storage.impl;
+
+import com.moekr.moocoder.logic.storage.StorageProvider;
+import com.qcloud.cos.COSClient;
+import com.qcloud.cos.ClientConfig;
+import com.qcloud.cos.auth.BasicCOSCredentials;
+import com.qcloud.cos.auth.COSCredentials;
+import com.qcloud.cos.exception.CosClientException;
+import com.qcloud.cos.exception.CosServiceException;
+import com.qcloud.cos.model.COSObject;
+import com.qcloud.cos.model.GetObjectRequest;
+import com.qcloud.cos.model.ObjectMetadata;
+import com.qcloud.cos.model.PutObjectRequest;
+import com.qcloud.cos.region.Region;
+import org.apache.commons.io.output.ByteArrayOutputStream;
+import org.apache.http.HttpStatus;
+import org.springframework.util.Assert;
+
+import java.io.ByteArrayInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.Map;
+
+public class CosStorageProvider implements StorageProvider {
+	private COSClient client;
+	private String bucket;
+
+	@Override
+	public void initialize(Map<String, String> properties) throws IOException {
+		String accessKey = properties.get("access-key");
+		String secretKey = properties.get("secret-key");
+		String region = properties.get("region");
+		bucket = properties.get("bucket");
+		Assert.notNull(accessKey, "未提供腾讯云COS的AccessKey!");
+		Assert.notNull(secretKey, "未提供腾讯云COS的SecretKey!");
+		Assert.notNull(region, "未提供腾讯云COS的Region!");
+		Assert.notNull(bucket, "未提供腾讯云COS的Bucket!");
+		COSCredentials credentials = new BasicCOSCredentials(accessKey, secretKey);
+		ClientConfig config = new ClientConfig(new Region(region));
+		client = new COSClient(credentials, config);
+		try {
+			Assert.isTrue(client.doesBucketExist(bucket), "Bucket [" + bucket + "] 不存在!");
+		} catch (CosClientException e) {
+			throw new IOException(e);
+		}
+	}
+
+	@Override
+	public void save(byte[] content, String name) throws IOException {
+		ObjectMetadata metadata = new ObjectMetadata();
+		metadata.setContentLength(content.length);
+		PutObjectRequest request = new PutObjectRequest(bucket, name, new ByteArrayInputStream(content), metadata);
+		try {
+			client.putObject(request);
+		} catch (CosClientException e) {
+			throw new IOException(e);
+		}
+	}
+
+	@Override
+	public byte[] fetch(String name) throws IOException {
+		GetObjectRequest request = new GetObjectRequest(bucket, name);
+		COSObject object;
+		try {
+			object = client.getObject(request);
+		} catch (CosServiceException e) {
+			if (e.getStatusCode() == HttpStatus.SC_NOT_FOUND) {
+				throw new FileNotFoundException(name);
+			}
+			throw new IOException(e);
+		} catch (CosClientException e) {
+			throw new IOException(e);
+		}
+		ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+		outputStream.write(object.getObjectContent());
+		return outputStream.toByteArray();
+	}
+}

+ 43 - 0
src/main/java/com/moekr/moocoder/logic/storage/impl/LocalStorageProvider.java

@@ -0,0 +1,43 @@
+package com.moekr.moocoder.logic.storage.impl;
+
+import com.moekr.moocoder.logic.storage.StorageProvider;
+import org.apache.commons.io.FileUtils;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Map;
+
+public class LocalStorageProvider implements StorageProvider {
+	private String location;
+
+	@Override
+	public void initialize(Map<String, String> properties) throws IOException {
+		location = properties.get("location");
+		if (location == null) {
+			throw new IllegalArgumentException("未设定文件存储位置!");
+		}
+		while (location.endsWith(File.separator)) {
+			location = location.substring(0, location.length() - 1);
+		}
+		File file = new File(location);
+		if (file.exists()) {
+			if (!file.isDirectory()) {
+				throw new IOException("存储目标不能为非文件夹!");
+			}
+		} else if (!file.mkdirs()) {
+			throw new IOException("创建文件夹失败!");
+		}
+	}
+
+	@Override
+	public void save(byte[] content, String name) throws IOException {
+		File file = new File(location + File.separator + name);
+		FileUtils.writeByteArrayToFile(file, content);
+	}
+
+	@Override
+	public byte[] fetch(String name) throws IOException {
+		File file = new File(location + File.separator + name);
+		return FileUtils.readFileToByteArray(file);
+	}
+}

+ 78 - 0
src/main/java/com/moekr/moocoder/logic/storage/impl/OssStorageProvider.java

@@ -0,0 +1,78 @@
+package com.moekr.moocoder.logic.storage.impl;
+
+import com.aliyun.oss.ClientConfiguration;
+import com.aliyun.oss.ClientException;
+import com.aliyun.oss.OSSClient;
+import com.aliyun.oss.ServiceException;
+import com.aliyun.oss.common.auth.CredentialsProvider;
+import com.aliyun.oss.common.auth.DefaultCredentialProvider;
+import com.aliyun.oss.model.GetObjectRequest;
+import com.aliyun.oss.model.OSSObject;
+import com.aliyun.oss.model.ObjectMetadata;
+import com.aliyun.oss.model.PutObjectRequest;
+import com.moekr.moocoder.logic.storage.StorageProvider;
+import org.apache.commons.io.output.ByteArrayOutputStream;
+import org.springframework.util.Assert;
+
+import java.io.ByteArrayInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.Map;
+
+public class OssStorageProvider implements StorageProvider {
+	private OSSClient client;
+	private String bucket;
+
+	@Override
+	public void initialize(Map<String, String> properties) throws IOException {
+		String endpoint = properties.get("endpoint");
+		String accessKeyId = properties.get("access-key-id");
+		String accessKeySecret = properties.get("access-key-secret");
+		String useCname = properties.get("use-cname");
+		bucket = properties.get("bucket");
+		Assert.notNull(endpoint, "未提供阿里云OSS的Endpoint!");
+		Assert.notNull(accessKeyId, "未提供阿里云OSS的AccessKeyId!");
+		Assert.notNull(accessKeySecret, "未提供阿里云OSS的AccessKeySecret!");
+		Assert.notNull(bucket, "未提供阿里云OSS的Bucket!");
+		CredentialsProvider provider = new DefaultCredentialProvider(accessKeyId, accessKeySecret);
+		ClientConfiguration config = new ClientConfiguration();
+		config.setSupportCname("true".equals(useCname));
+		client = new OSSClient(endpoint, provider, config);
+		try {
+			Assert.isTrue(client.doesBucketExist(bucket), "Bucket [" + bucket + "] 不存在!");
+		} catch (ServiceException | ClientException e) {
+			throw new IOException(e);
+		}
+	}
+
+	@Override
+	public void save(byte[] content, String name) throws IOException {
+		ObjectMetadata metadata = new ObjectMetadata();
+		metadata.setContentLength(content.length);
+		PutObjectRequest request = new PutObjectRequest(bucket, name, new ByteArrayInputStream(content), metadata);
+		try {
+			client.putObject(request);
+		} catch (ServiceException | ClientException e) {
+			throw new IOException(e);
+		}
+	}
+
+	@Override
+	public byte[] fetch(String name) throws IOException {
+		GetObjectRequest request = new GetObjectRequest(bucket, name);
+		OSSObject object;
+		try {
+			object = client.getObject(request);
+		} catch (ServiceException e) {
+			if ("NoSuchKey".equals(e.getErrorCode())) {
+				throw new FileNotFoundException(name);
+			}
+			throw new IOException(e);
+		} catch (ClientException e) {
+			throw new IOException(e);
+		}
+		ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+		outputStream.write(object.getObjectContent());
+		return outputStream.toByteArray();
+	}
+}

+ 112 - 0
src/main/java/com/moekr/moocoder/logic/storage/impl/QiniuStorageProvider.java

@@ -0,0 +1,112 @@
+package com.moekr.moocoder.logic.storage.impl;
+
+import com.moekr.moocoder.logic.storage.StorageProvider;
+import com.qiniu.common.QiniuException;
+import com.qiniu.common.Zone;
+import com.qiniu.http.Response;
+import com.qiniu.storage.BucketManager;
+import com.qiniu.storage.Configuration;
+import com.qiniu.storage.UploadManager;
+import com.qiniu.util.Auth;
+import org.apache.commons.io.output.ByteArrayOutputStream;
+import org.apache.http.HttpStatus;
+import org.apache.http.StatusLine;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.springframework.util.Assert;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.net.URLEncoder;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+public class QiniuStorageProvider implements StorageProvider {
+	private String bucket;
+	private String domain;
+	private Auth auth;
+	private UploadManager manager;
+	private CloseableHttpClient client;
+
+	@Override
+	public void initialize(Map<String, String> properties) throws IOException {
+		String accessKey = properties.get("access-key");
+		String secretKey = properties.get("secret-key");
+		String zone = properties.get("zone");
+		String autoZone = properties.get("auto-zone");
+		bucket = properties.get("bucket");
+		domain = properties.get("domain");
+		Assert.notNull(accessKey, "未提供七牛云存储的AccessKey!");
+		Assert.notNull(secretKey, "未提供七牛云存储的SecretKey!");
+		Assert.notNull(bucket, "未提供七牛云存储的Bucket!");
+		Assert.notNull(domain, "未提供七牛云存储的Domain!");
+		auth = Auth.create(accessKey, secretKey);
+		Configuration config = new Configuration("true".equals(autoZone) ? Zone.autoZone() : zone(zone));
+		BucketManager manager = new BucketManager(auth, config);
+		try {
+			List<String> buckets = Arrays.asList(manager.buckets());
+			Assert.isTrue(!buckets.contains(bucket), "Bucket [" + bucket + "] 不存在!");
+		} catch (QiniuException e) {
+			throw new IOException(e);
+		}
+		this.manager = new UploadManager(config);
+		client = HttpClients.createDefault();
+	}
+
+	@Override
+	public void save(byte[] content, String name) throws IOException {
+		String token = auth.uploadToken(bucket);
+		try {
+			Response response = manager.put(content, name, token);
+			if (!response.isOK()) {
+				throw new IOException(response.error);
+			}
+		} catch (QiniuException e) {
+			throw new IOException(e);
+		}
+	}
+
+	@Override
+	public byte[] fetch(String name) throws IOException {
+		String baseUrl = domain + "/" + URLEncoder.encode(name, "UTF-8");
+		String privateUrl = auth.privateDownloadUrl(baseUrl);
+		HttpGet request = new HttpGet(privateUrl);
+		try (CloseableHttpResponse response = client.execute(request)) {
+			StatusLine statusLine = response.getStatusLine();
+			if (statusLine.getStatusCode() == HttpStatus.SC_OK) {
+				ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+				outputStream.write(response.getEntity().getContent());
+				return outputStream.toByteArray();
+			} else if (statusLine.getStatusCode() == HttpStatus.SC_NOT_FOUND) {
+				throw new FileNotFoundException(name);
+			} else {
+				throw new IOException(statusLine.getReasonPhrase());
+			}
+		}
+	}
+
+	private Zone zone(String zone) {
+		Assert.notNull(zone, "未提供七牛云存储的Zone!");
+		switch (zone) {
+			case "z0":
+			case "hd":
+				return Zone.zone0();
+			case "z1":
+			case "hb":
+				return Zone.zone1();
+			case "z2":
+			case "hn":
+				return Zone.zone2();
+			case "na0":
+			case "bm":
+				return Zone.zoneNa0();
+			case "as0":
+			case "dny":
+				return Zone.zoneAs0();
+		}
+		throw new IllegalArgumentException("七牛云存储的Zone信息无效!");
+	}
+}

+ 50 - 0
src/main/java/com/moekr/moocoder/logic/vo/CodeVO.java

@@ -0,0 +1,50 @@
+package com.moekr.moocoder.logic.vo;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.moekr.moocoder.util.serializer.TimestampLocalDateTimeSerializer;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Map;
+
+@Data
+public class CodeVO {
+    private Integer examId;
+    @JsonProperty("fetch_at")
+    @JsonSerialize(using = TimestampLocalDateTimeSerializer.class)
+    private LocalDateTime fetchAt;
+    @JsonProperty("start_at")
+    @JsonSerialize(using = TimestampLocalDateTimeSerializer.class)
+    private LocalDateTime startAt;
+    @JsonProperty("end_at")
+    @JsonSerialize(using = TimestampLocalDateTimeSerializer.class)
+    private LocalDateTime endAt;
+    private List<NestedCodeItemVO> codeList;
+
+    public CodeVO(Integer examId, LocalDateTime fetchAt, LocalDateTime startAt, LocalDateTime endAt, List<NestedCodeItemVO> codeList){
+        this.examId = examId;
+        this.fetchAt = fetchAt;
+        this.startAt = startAt;
+        this.endAt = endAt;
+        this.codeList = codeList;
+    }
+
+    @Data
+    public static class NestedCodeItemVO {
+        private Integer questionId;
+        private String language;
+        private String projectName;
+        private Map<String, String> editable;
+        private Map<String, String> uneditable;
+
+        public NestedCodeItemVO(Integer questionId, String language, String projectName, Map<String, String> editable, Map<String, String> uneditable) {
+            this.questionId = questionId;
+            this.language = language;
+            this.projectName = projectName;
+            this.editable = editable;
+            this.uneditable = uneditable;
+        }
+    }
+}

+ 89 - 0
src/main/java/com/moekr/moocoder/logic/vo/CommitVO.java

@@ -0,0 +1,89 @@
+package com.moekr.moocoder.logic.vo;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.moekr.moocoder.data.entity.*;
+import com.moekr.moocoder.util.enums.BuildStatus;
+import com.moekr.moocoder.util.serializer.TimestampLocalDateTimeSerializer;
+import lombok.Data;
+import org.springframework.beans.BeanUtils;
+
+import java.time.LocalDateTime;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+@Data
+public class CommitVO {
+	private Integer id;
+	private String hash;
+	private boolean finished;
+	private Integer score;
+	@JsonProperty("created_at")
+	@JsonSerialize(using = TimestampLocalDateTimeSerializer.class)
+	private LocalDateTime createdAt;
+	private NestedResultVO result;
+	private Set<NestedRecordVO> records;
+
+	public CommitVO(Commit commit) {
+		BeanUtils.copyProperties(commit, this);
+		this.result = new NestedResultVO(commit.getResult());
+		this.records = commit.getRecords().stream().map(NestedRecordVO::new).collect(Collectors.toSet());
+	}
+
+	@Data
+	private static class NestedRecordVO {
+		private Integer id;
+		private BuildStatus status;
+		private Integer score;
+		private NestedProblemVO problem;
+
+		NestedRecordVO(Record record) {
+			BeanUtils.copyProperties(record, this);
+			this.problem = new NestedProblemVO(record.getProblem());
+		}
+
+		@Data
+		private static class NestedProblemVO {
+			private Integer id;
+			private String name;
+
+			NestedProblemVO(Problem problem) {
+				BeanUtils.copyProperties(problem, this);
+			}
+		}
+	}
+
+	@Data
+	private static class NestedResultVO {
+		private Integer id;
+		private Integer score;
+		private NestedExamVO exam;
+		private NestedUserVO owner;
+
+		NestedResultVO(Result result) {
+			BeanUtils.copyProperties(result, this);
+			this.exam = new NestedExamVO(result.getExam());
+			this.owner = new NestedUserVO(result.getOwner());
+		}
+
+		@Data
+		private static class NestedExamVO {
+			private Integer id;
+			private String name;
+
+			NestedExamVO(Exam exam) {
+				BeanUtils.copyProperties(exam, this);
+			}
+		}
+
+		@Data
+		private static class NestedUserVO {
+			private Integer id;
+			private String username;
+
+			NestedUserVO(User user) {
+				BeanUtils.copyProperties(user, this);
+			}
+		}
+	}
+}

+ 28 - 0
src/main/java/com/moekr/moocoder/logic/vo/CourseVO.java

@@ -0,0 +1,28 @@
+package com.moekr.moocoder.logic.vo;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.moekr.moocoder.data.entity.Course;
+import lombok.Data;
+
+/**
+ * @author XiangzheXu
+ * create-time: 2018/12/9
+ */
+@Data
+public class CourseVO {
+
+    @JsonProperty("id")
+    private Integer id;
+
+    @JsonProperty("name")
+    private String name;
+
+    @JsonProperty("code")
+    private String code;
+
+    public CourseVO(Course course) {
+        this.code = course.getCode();
+        this.name = course.getName();
+        this.id = course.getId();
+    }
+}

+ 62 - 0
src/main/java/com/moekr/moocoder/logic/vo/ExamVO.java

@@ -0,0 +1,62 @@
+package com.moekr.moocoder.logic.vo;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.moekr.moocoder.data.entity.Course;
+import com.moekr.moocoder.data.entity.Exam;
+import com.moekr.moocoder.data.entity.User;
+import com.moekr.moocoder.util.enums.ExamStatus;
+import com.moekr.moocoder.util.serializer.TimestampLocalDateTimeSerializer;
+import lombok.Data;
+import org.springframework.beans.BeanUtils;
+
+import java.time.LocalDateTime;
+
+@Data
+public class ExamVO {
+	private Integer id;
+	private String name;
+	@JsonProperty("created_at")
+	@JsonSerialize(using = TimestampLocalDateTimeSerializer.class)
+	private LocalDateTime createdAt;
+	@JsonProperty("start_at")
+	@JsonSerialize(using = TimestampLocalDateTimeSerializer.class)
+	private LocalDateTime startAt;
+	@JsonProperty("end_at")
+	@JsonSerialize(using = TimestampLocalDateTimeSerializer.class)
+	private LocalDateTime endAt;
+	private ExamStatus status;
+	private NestedUserVO creator;
+	private boolean joined;
+	private Integer courseId;
+	@JsonProperty("course")
+	private String courseName;
+
+	public ExamVO(Exam exam) {
+		BeanUtils.copyProperties(exam, this);
+		this.creator = new NestedUserVO(exam.getCreator());
+		if (this.status == ExamStatus.AVAILABLE) {
+			LocalDateTime now = LocalDateTime.now();
+			if (now.isBefore(startAt)) {
+				this.status = ExamStatus.READY;
+			} else if (now.isAfter(endAt)) {
+				this.status = ExamStatus.FINISHED;
+			}
+		}
+	}
+
+	public ExamVO(Exam exam, Course course) {
+		this(exam);
+		this.courseName = course.getName();
+	}
+
+	@Data
+	private static class NestedUserVO {
+		private Integer id;
+		private String username;
+
+		NestedUserVO(User user) {
+			BeanUtils.copyProperties(user, this);
+		}
+	}
+}

+ 70 - 0
src/main/java/com/moekr/moocoder/logic/vo/JoinedExamVO.java

@@ -0,0 +1,70 @@
+package com.moekr.moocoder.logic.vo;
+
+import com.moekr.moocoder.data.entity.Course;
+import com.moekr.moocoder.data.entity.Exam;
+import com.moekr.moocoder.data.entity.Problem;
+import com.moekr.moocoder.data.entity.Result;
+import com.moekr.moocoder.util.enums.ExamStatus;
+import com.moekr.moocoder.util.enums.ProblemType;
+import com.moekr.moocoder.util.enums.UserRole;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.springframework.beans.BeanUtils;
+
+import java.util.Set;
+import java.util.stream.Collectors;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class JoinedExamVO extends ExamVO {
+	private String url;
+	private Set<NestedProblemVO> problems;
+	private NestedResultVO result;
+
+	public JoinedExamVO(Exam exam) {
+		super(exam);
+		init(exam);
+	}
+
+	private void init(Exam exam) {
+		this.problems = exam.getProblems().stream().map(NestedProblemVO::new).collect(Collectors.toSet());
+	}
+
+	public JoinedExamVO(Exam exam, Course course) {
+		super(exam, course);
+		init(exam);
+	}
+
+	public JoinedExamVO(Exam exam, String url, Result result,Course course) {
+		this(exam,course);
+		this.setJoined(true);
+		if (result.getOwner().getRole() == UserRole.TEACHER || this.getStatus() == ExamStatus.AVAILABLE) {
+			this.url = url;
+		} else {
+			this.problems = null;
+		}
+		this.result = new NestedResultVO(result);
+	}
+
+	@Data
+	private static class NestedProblemVO {
+		private Integer id;
+		private String name;
+		private ProblemType type;
+		private String description;
+
+		NestedProblemVO(Problem problem) {
+			BeanUtils.copyProperties(problem, this);
+		}
+	}
+
+	@Data
+	private static class NestedResultVO {
+		private Integer id;
+		private Integer score;
+
+		NestedResultVO(Result result) {
+			BeanUtils.copyProperties(result, this);
+		}
+	}
+}

+ 56 - 0
src/main/java/com/moekr/moocoder/logic/vo/ProblemVO.java

@@ -0,0 +1,56 @@
+package com.moekr.moocoder.logic.vo;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.moekr.moocoder.data.entity.Problem;
+import com.moekr.moocoder.data.entity.User;
+import com.moekr.moocoder.util.enums.ProblemType;
+import com.moekr.moocoder.util.serializer.TimestampLocalDateTimeSerializer;
+import lombok.Data;
+import org.springframework.beans.BeanUtils;
+
+import java.time.LocalDateTime;
+import java.util.Set;
+
+@Data
+public class ProblemVO {
+	private Integer id;
+	private String name;
+	private ProblemType type;
+	private String description;
+	@JsonProperty("public_files")
+	private Set<String> publicFiles;
+	@JsonProperty("protected_files")
+	private Set<String> protectedFiles;
+	@JsonProperty("private_files")
+	private Set<String> privateFiles;
+	@JsonProperty("created_at")
+	@JsonSerialize(using = TimestampLocalDateTimeSerializer.class)
+	private LocalDateTime createdAt;
+	@JsonProperty("modified_at")
+	@JsonSerialize(using = TimestampLocalDateTimeSerializer.class)
+	private LocalDateTime modifiedAt;
+	private boolean deprecated;
+	private NestedUserVO creator;
+
+	public ProblemVO(Problem problem) {
+		BeanUtils.copyProperties(problem, this);
+		this.creator = problem.getCreator() == null ? null : new NestedUserVO(problem.getCreator());
+	}
+
+	@JsonIgnore
+	public String getUniqueName() {
+		return (id + "-" + name).toLowerCase();
+	}
+
+	@Data
+	private static class NestedUserVO {
+		private Integer id;
+		private String username;
+
+		NestedUserVO(User user) {
+			BeanUtils.copyProperties(user, this);
+		}
+	}
+}

+ 77 - 0
src/main/java/com/moekr/moocoder/logic/vo/RecordVO.java

@@ -0,0 +1,77 @@
+package com.moekr.moocoder.logic.vo;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.moekr.moocoder.data.entity.Commit;
+import com.moekr.moocoder.data.entity.Record;
+import com.moekr.moocoder.data.entity.Record.Failure;
+import com.moekr.moocoder.util.enums.BuildStatus;
+import com.moekr.moocoder.util.serializer.TimestampLocalDateTimeSerializer;
+import lombok.Data;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.BeanUtils;
+
+import java.time.LocalDateTime;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+@Data
+public class RecordVO {
+	private Integer id;
+	private Integer number;
+	private BuildStatus status;
+	@JsonProperty("console_output")
+	private String consoleOutput;
+	private Integer score;
+	private Set<NestedFailureVO> failures;
+	private NestedCommitVO commit;
+
+	public RecordVO(Record record) {
+		String[] passStatus = new String[]{"PASSED", "FIXED"};
+		BeanUtils.copyProperties(record, this, "consoleOutput");
+		this.consoleOutput = convertConsoleOutput(record.getConsoleOutput());
+		this.failures = record.getFailures().stream()
+				.filter((failure) -> {return !StringUtils.equalsAnyIgnoreCase(failure.getStatus(), passStatus);})
+				.map(NestedFailureVO::new).collect(Collectors.toSet());
+		this.commit = new NestedCommitVO(record.getCommit());
+	}
+
+	private String convertConsoleOutput(String consoleOutput) {
+		String beginIndicator = "==CONSOLE OUTPUT BEGIN==";
+		String endIndicator = "==CONSOLE OUTPUT END==";
+		int begin = consoleOutput.indexOf(beginIndicator);
+		if (begin == -1) {
+			begin = 0;
+		} else {
+			begin = begin + beginIndicator.length() + 2;
+		}
+		int end = consoleOutput.lastIndexOf(endIndicator);
+		if (end == -1) {
+			end = consoleOutput.length();
+		}
+		return consoleOutput.substring(begin, end);
+	}
+
+	@Data
+	private static class NestedFailureVO {
+		private String name;
+		private String details;
+		private String trace;
+
+		NestedFailureVO(Failure failure) {
+			BeanUtils.copyProperties(failure, this, "status");
+		}
+	}
+
+	@Data
+	private static class NestedCommitVO {
+		private Integer id;
+		@JsonProperty("created_at")
+		@JsonSerialize(using = TimestampLocalDateTimeSerializer.class)
+		private LocalDateTime createdAt;
+
+		NestedCommitVO(Commit commit) {
+			BeanUtils.copyProperties(commit, this);
+		}
+	}
+}

+ 90 - 0
src/main/java/com/moekr/moocoder/logic/vo/ResultVO.java

@@ -0,0 +1,90 @@
+package com.moekr.moocoder.logic.vo;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.moekr.moocoder.data.entity.*;
+import com.moekr.moocoder.util.serializer.TimestampLocalDateTimeSerializer;
+import lombok.Data;
+import org.springframework.beans.BeanUtils;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Data
+public class ResultVO {
+	private Integer id;
+	private Integer score;
+	@JsonProperty("last_commit_at")
+	@JsonSerialize(using = TimestampLocalDateTimeSerializer.class)
+	private LocalDateTime lastCommitAt;
+	private boolean deleted;
+	private NestedExamVO exam;
+	private NestedUserVO owner;
+	private List<NestedCommitVO> commits;
+
+	public ResultVO(Result result, boolean withCommits) {
+		BeanUtils.copyProperties(result, this);
+		this.exam = new NestedExamVO(result.getExam());
+		this.owner = new NestedUserVO(result.getOwner());
+		if (withCommits) {
+			this.commits = result.getCommitList().stream().map(NestedCommitVO::new).collect(Collectors.toList());
+		}
+	}
+
+    public ResultVO(Result result, Course course, boolean withCommits) {
+        BeanUtils.copyProperties(result, this);
+        this.exam = new NestedExamVO(result.getExam(), course);
+        this.owner = new NestedUserVO(result.getOwner());
+        if (withCommits) {
+            this.commits = result.getCommitList().stream().map(NestedCommitVO::new).collect(Collectors.toList());
+        }
+    }
+
+	// 提供给ResultFileController获取内部用户名
+	@JsonIgnore
+	public String getUsername() {
+		return owner.username;
+	}
+
+	@Data
+	private static class NestedExamVO {
+		private Integer id;
+		private String name;
+		private String course;
+
+		NestedExamVO(Exam exam) {
+			BeanUtils.copyProperties(exam, this);
+		}
+
+        NestedExamVO(Exam exam, Course course) {
+            BeanUtils.copyProperties(exam, this);
+            this.course = course.getName();
+        }
+	}
+
+	@Data
+	private static class NestedUserVO {
+		private Integer id;
+		private String username;
+
+		NestedUserVO(User user) {
+			BeanUtils.copyProperties(user, this);
+		}
+	}
+
+	@Data
+	private static class NestedCommitVO {
+		private Integer id;
+		private boolean finished;
+		private Integer score;
+		@JsonProperty("created_at")
+		@JsonSerialize(using = TimestampLocalDateTimeSerializer.class)
+		private LocalDateTime createdAt;
+
+		NestedCommitVO(Commit commit) {
+			BeanUtils.copyProperties(commit, this);
+		}
+	}
+}

+ 14 - 0
src/main/java/com/moekr/moocoder/logic/vo/StatisticVO.java

@@ -0,0 +1,14 @@
+package com.moekr.moocoder.logic.vo;
+
+import lombok.Data;
+
+@Data
+public class StatisticVO {
+	private Integer studentCount;
+	private Integer teacherCount;
+	private Integer examinationCount;
+	private Integer resultCount;
+	private Integer recordCount;
+	private Integer javaProblemCount;
+	private Integer pythonProblemCount;
+}

+ 34 - 0
src/main/java/com/moekr/moocoder/logic/vo/UserVO.java

@@ -0,0 +1,34 @@
+package com.moekr.moocoder.logic.vo;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.moekr.moocoder.data.entity.User;
+import com.moekr.moocoder.util.enums.UserRole;
+import com.moekr.moocoder.util.serializer.TimestampLocalDateTimeSerializer;
+import lombok.Data;
+import org.springframework.beans.BeanUtils;
+
+import java.time.LocalDateTime;
+
+@Data
+public class UserVO {
+	private Integer id;
+	private String username;
+	private String email;
+	private String phone;
+	private UserRole role;
+	@JsonProperty("created_at")
+	@JsonSerialize(using = TimestampLocalDateTimeSerializer.class)
+	private LocalDateTime createdAt;
+
+	private UserVO() {
+		this.id = 0;
+		this.username = "管理员";
+	}
+
+	public UserVO(User user) {
+		BeanUtils.copyProperties(user, this);
+	}
+
+	public static final UserVO ADMIN = new UserVO();
+}

+ 22 - 0
src/main/java/com/moekr/moocoder/logic/vo/internal/ExamTestCaseVO.java

@@ -0,0 +1,22 @@
+package com.moekr.moocoder.logic.vo.internal;
+
+import com.moekr.moocoder.data.dao.RecordDAO;
+import lombok.Data;
+
+import java.util.List;
+import java.util.Map;
+
+@Data
+public class ExamTestCaseVO {
+    private int examId;
+    private Map<Integer, String> problemNameMap;
+    private Map<Integer, List<String>> testCaseMap;
+
+    public ExamTestCaseVO(int examId, Map<Integer, String> problemNameMap, Map<Integer, List<String>> testCaseMap){
+        this.examId = examId;
+        this.problemNameMap = problemNameMap;
+        this.testCaseMap = testCaseMap;
+    }
+
+
+}

+ 116 - 0
src/main/java/com/moekr/moocoder/logic/vo/internal/ResultDetailVO.java

@@ -0,0 +1,116 @@
+package com.moekr.moocoder.logic.vo.internal;
+
+import com.aliyun.oss.ServiceException;
+import com.moekr.moocoder.data.dao.RecordDAO.TestCaseResultInterface;
+import com.moekr.moocoder.data.entity.Record.Failure;
+import com.moekr.moocoder.util.enums.BuildStatus;
+import lombok.Data;
+import org.apache.commons.lang3.StringUtils;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.springframework.beans.BeanUtils;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+@Data
+public class ResultDetailVO {
+    private Integer userId;
+    private String username;
+    private Integer score;
+    private Map<Integer, NestedRecordVO> recordVOMap;
+
+    /**
+     * 对于某一道题,如果该学生没有提交记录(或者是没有运行完的提交记录),则此处recordVOMap里会没有对应题目的记录。
+     * 因此需要传入一个题目数量的值来计算加权成绩。
+     * @param testCaseResultInterfaces
+     * @param problemCount
+     */
+    public ResultDetailVO(List<TestCaseResultInterface> testCaseResultInterfaces, int problemCount){
+        if (testCaseResultInterfaces.size()==0)
+            throw new ServiceException("统计成绩时错误:The size of testCaseResultInterfaces is 0.");
+        this.userId = testCaseResultInterfaces.get(0).getUserId();
+        this.username = testCaseResultInterfaces.get(0).getUsername();
+        this.recordVOMap = testCaseResultInterfaces.stream().collect(Collectors.toMap(TestCaseResultInterface::getProblem, NestedRecordVO::new));
+        this.score = this.recordVOMap.values().stream().mapToInt(NestedRecordVO::getScore).sum() / problemCount;
+    }
+
+
+    @Data
+    public static class NestedRecordVO {
+        private BuildStatus status;
+        private Map<String, NestedFailureVO> failureCaseMap;
+        private String consoleOutput;
+        private Integer score;
+
+        NestedRecordVO(TestCaseResultInterface testCaseResult) {
+            try {
+                this.status = BuildStatus.valueOf(testCaseResult.getStatus());
+            } catch (IllegalArgumentException e) {}
+            if (this.status == BuildStatus.UNSTABLE || this.status == BuildStatus.SUCCESS) {
+                // 对编译成功的情况,记录错误的测试用例。
+                // 其他情况不记录测试用例情况。
+                String[] passStatus = new String[]{"PASSED", "FIXED"};
+                if (testCaseResult.getTestCaseResult() != null) {
+                    List<Failure> failures = convertToEntityAttribute(testCaseResult.getTestCaseResult());
+                    try {
+                        this.failureCaseMap = failures.stream()
+                                .filter((result) -> {
+                                    return !StringUtils.equalsAnyIgnoreCase(result.getStatus(), passStatus);
+                                })
+                                .collect(Collectors.toMap(Failure::getName, NestedFailureVO::new));
+                    } catch (Exception e) {}
+                }
+            }
+            this.consoleOutput = convertConsoleOutput(testCaseResult.getConsoleOutput());
+            this.score = testCaseResult.getScore();
+        }
+
+        private List<Failure> convertToEntityAttribute(String dbData) {
+            List<Failure> failures = new ArrayList<>();
+            JSONArray array = new JSONArray(dbData);
+            for (Object object : array) {
+                if (object instanceof JSONObject) {
+                    try {
+                        Failure failure = new Failure();
+                        failure.setName(((JSONObject) object).getString("name"));
+                        failure.setStatus(((JSONObject) object).getString("status"));
+                        failure.setDetails(((JSONObject) object).getString("details"));
+                        failure.setTrace(((JSONObject) object).getString("trace"));
+                        failures.add(failure);
+                    } catch (JSONException ignore) { }
+                }
+            }
+            return failures;
+        }
+
+        private String convertConsoleOutput(String consoleOutput) {
+            String beginIndicator = "==CONSOLE OUTPUT BEGIN==";
+            String endIndicator = "==CONSOLE OUTPUT END==";
+            int begin = consoleOutput.indexOf(beginIndicator);
+            if (begin == -1) {
+                begin = 0;
+            } else {
+                begin = begin + beginIndicator.length() + 2;
+            }
+            int end = consoleOutput.lastIndexOf(endIndicator);
+            if (end == -1) {
+                end = consoleOutput.length();
+            }
+            return consoleOutput.substring(begin, end);
+        }
+    }
+
+    @Data
+    public static class NestedFailureVO {
+        private String name;
+        private String details;
+        private String trace;
+
+        NestedFailureVO(Failure failure) {
+            BeanUtils.copyProperties(failure, this, "status");
+        }
+    }
+
+}

+ 88 - 0
src/main/java/com/moekr/moocoder/util/ApplicationProperties.java

@@ -0,0 +1,88 @@
+package com.moekr.moocoder.util;
+
+import lombok.Data;
+import org.apache.commons.lang3.StringUtils;
+import org.hibernate.validator.constraints.URL;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.validation.annotation.Validated;
+
+import javax.validation.Valid;
+import javax.validation.constraints.Email;
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.Pattern;
+import java.util.HashMap;
+import java.util.Map;
+
+@Data
+@Configuration
+@ConfigurationProperties("moocoder")
+@Validated
+public class ApplicationProperties {
+	@Pattern(regexp = "^[a-zA-Z0-9]+$")
+	@NotEmpty
+	private String secret;
+	@Valid
+	private Gitlab gitlab = new Gitlab();
+	@Valid
+	private Jenkins jenkins = new Jenkins();
+	@Valid
+	private Docker docker = new Docker();
+	@Valid
+	private Mail mail = new Mail();
+	@Valid
+	private Storage storage = new Storage();
+
+	@Data
+	public static class Gitlab {
+		@URL
+		@NotEmpty
+		private String host;
+		private String gitProxy;
+		@URL
+		@NotEmpty
+		private String webHookProxy = "http://localhost:3000";
+		@NotEmpty
+		private String username = "root";
+		@NotEmpty
+		private String token;
+
+		public String getGitProxy() {
+			return StringUtils.defaultString(gitProxy, host);
+		}
+	}
+
+	@Data
+	public static class Jenkins {
+		@URL
+		@NotEmpty
+		private String host;
+		@NotEmpty
+		private String username = "root";
+		@NotEmpty
+		private String token;
+	}
+
+	@Data
+	public static class Docker {
+		@NotEmpty
+		private String host = "unix:///var/run/docker.sock";
+		@NotEmpty
+		private String registry;
+	}
+
+	@Data
+	public static class Mail {
+		@Email
+		@NotEmpty
+		private String from;
+		@NotEmpty
+		private String personal = "MOOCODER";
+	}
+
+	@Data
+	public static class Storage {
+		private String type = "local";
+		private Map<String, String> properties = new HashMap<>();
+	}
+}

+ 6 - 0
src/main/java/com/moekr/moocoder/util/Method.java

@@ -0,0 +1,6 @@
+package com.moekr.moocoder.util;
+
+@FunctionalInterface
+public interface Method {
+	void invoke() throws Exception;
+}

+ 35 - 0
src/main/java/com/moekr/moocoder/util/ToolKit.java

@@ -0,0 +1,35 @@
+package com.moekr.moocoder.util;
+
+import org.apache.commons.lang3.RandomStringUtils;
+import org.springframework.http.HttpStatus;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.UUID;
+
+public abstract class ToolKit {
+	public static final String VERSION = "0.4.5";
+
+	public static String format(Exception exception) {
+		return "[" + exception.getClass().getName() + "]:" + exception.getMessage();
+	}
+
+	public static HttpStatus httpStatus(HttpServletRequest request) {
+		Integer statusCode = (Integer) request.getAttribute("javax.servlet.error.status_code");
+		if (statusCode == null) {
+			return HttpStatus.INTERNAL_SERVER_ERROR;
+		}
+		try {
+			return HttpStatus.valueOf(statusCode);
+		} catch (Exception e) {
+			return HttpStatus.INTERNAL_SERVER_ERROR;
+		}
+	}
+
+	public static String randomUUID() {
+		return UUID.randomUUID().toString().replace("-", "");
+	}
+
+	public static String randomPassword() {
+		return RandomStringUtils.randomAlphanumeric(12);
+	}
+}

+ 26 - 0
src/main/java/com/moekr/moocoder/util/editors/DefaultNumberEditor.java

@@ -0,0 +1,26 @@
+package com.moekr.moocoder.util.editors;
+
+import java.beans.PropertyEditorSupport;
+
+public class DefaultNumberEditor extends PropertyEditorSupport {
+	private final int defaultValue;
+
+	public DefaultNumberEditor(int defaultValue) {
+		this.defaultValue = defaultValue;
+	}
+
+	@Override
+	public void setAsText(String s) throws IllegalArgumentException {
+		try {
+			setValue(Integer.valueOf(s));
+		} catch (NumberFormatException e) {
+			setValue(defaultValue);
+		}
+	}
+
+	@Override
+	public String getJavaInitializationString() {
+		Object var1 = this.getValue();
+		return var1 != null ? var1.toString() : "null";
+	}
+}

+ 18 - 0
src/main/java/com/moekr/moocoder/util/editors/PageNumberEditor.java

@@ -0,0 +1,18 @@
+package com.moekr.moocoder.util.editors;
+
+public class PageNumberEditor extends DefaultNumberEditor {
+	private static final int DEFAULT_PAGE_NUMBER = 1;
+
+	public PageNumberEditor() {
+		super(DEFAULT_PAGE_NUMBER);
+	}
+
+	@Override
+	public void setValue(Object value) {
+		if (value instanceof Integer) {
+			super.setValue((Integer) value - 1);
+		} else {
+			super.setValue(value);
+		}
+	}
+}

+ 22 - 0
src/main/java/com/moekr/moocoder/util/editors/RangeNumberEditor.java

@@ -0,0 +1,22 @@
+package com.moekr.moocoder.util.editors;
+
+public class RangeNumberEditor extends DefaultNumberEditor {
+	private final int min;
+	private final int max;
+
+	public RangeNumberEditor(int min, int max, int defaultValue) {
+		super(defaultValue);
+		this.min = min;
+		this.max = max;
+	}
+
+	@Override
+	public void setAsText(String s) throws IllegalArgumentException {
+		try {
+			int value = Integer.valueOf(s);
+			setValue(Math.max(min, Math.min(max, value)));
+		} catch (NumberFormatException e) {
+			super.setAsText(s);
+		}
+	}
+}

+ 28 - 0
src/main/java/com/moekr/moocoder/util/enums/BuildStatus.java

@@ -0,0 +1,28 @@
+package com.moekr.moocoder.util.enums;
+
+public enum BuildStatus {
+	/**
+	 * 构建成功,表示没有错误
+	 */
+	SUCCESS,
+	/**
+	 * 构建不稳定,表示有测试错误等非致命错误
+	 */
+	UNSTABLE,
+	/**
+	 * 构建失败,表示有语法错误等致命错误
+	 */
+	FAILURE,
+	/**
+	 * 构建超时,表示运行测试时超时
+	 */
+	TIMEOUT,
+	/**
+	 * 等待构建,表示构建尚未开始
+	 */
+	WAITING,
+	/**
+	 * 正在构建,表示构建正在进行
+	 */
+	RUNNING
+}

+ 34 - 0
src/main/java/com/moekr/moocoder/util/enums/ExamStatus.java

@@ -0,0 +1,34 @@
+package com.moekr.moocoder.util.enums;
+
+/**
+ * 考试状态
+ * 考试生命周期中对外表现的状态的顺序为PREPARING->READY->AVAILABLE->FINISHED->CLOSED
+ * 考试生命周期中持久化的状态的顺序为PREPARING->AVAILABLE->CLOSED
+ * UNAVAILABLE表示考试初始化失败
+ */
+public enum ExamStatus {
+	/**
+	 * 正在准备试题,通常是正在PUSH试题
+	 */
+	PREPARING,
+	/**
+	 * 考试不可用,通常是PUSH试题失败
+	 */
+	UNAVAILABLE,
+	/**
+	 * 考试已准备好,在考试尚未开始时使用,不持久化到数据库中
+	 */
+	READY,
+	/**
+	 * 考试可用,表示考试正在进行,持久化时考试从准备好到考试被关闭均为此状态
+	 */
+	AVAILABLE,
+	/**
+	 * 考试已完成,在考试结束后使用,不持久化到数据库中
+	 */
+	FINISHED,
+	/**
+	 * 考试已关闭,表示考试生命周期结束,所有试卷均被存档
+	 */
+	CLOSED
+}

+ 16 - 0
src/main/java/com/moekr/moocoder/util/enums/FileType.java

@@ -0,0 +1,16 @@
+package com.moekr.moocoder.util.enums;
+
+public enum FileType {
+	/**
+	 * 可修改的文件,表示学生可以修改该文件,进行测试时应用学生的文件替换试题中的文件
+	 */
+	PUBLIC,
+	/**
+	 * 不可修改的文件,表示学生不可以修改该文件,进行测试时保持试题文件不变化
+	 */
+	PROTECTED,
+	/**
+	 * 隐藏文件,表示学生无法看到该文件,通常用于测试用例等
+	 */
+	PRIVATE
+}

+ 58 - 0
src/main/java/com/moekr/moocoder/util/enums/ProblemType.java

@@ -0,0 +1,58 @@
+package com.moekr.moocoder.util.enums;
+
+import com.moekr.moocoder.util.problem.evaluator.*;
+import com.moekr.moocoder.util.problem.helper.*;
+import lombok.Getter;
+
+@Getter
+public enum ProblemType {
+	/**
+	 * 普通Java题目,统计测试用例通过情况
+	 */
+	JAVA("Java", "JAVA", "TEST",
+			new JavaHelper(), new BasicEvaluator()),
+	/**
+	 * 带数据库的Java题目,统计测试用例通过情况
+	 */
+	JAVA_DATABASE("Java", "JAVA", "TEST",
+			new JavaDatabaseHelper(), new BasicEvaluator()),
+	/**
+	 * 普通Python题目,统计测试用例通过情况
+	 */
+	PYTHON("Python", "PYTHON", "TEST",
+			new PythonHelper(), new PythonEvaluator()),
+	/**
+	 * Java测试覆盖率题目,统计测试覆盖率
+	 */
+	JAVA_COVERAGE("Java Coverage", "JAVA", "COVERAGE",
+			new JavaCoverageHelper(), new BasicCoverageEvaluator()),
+	/**
+	 * Python测试覆盖率题目,统计测试覆盖率
+	 */
+	PYTHON_COVERAGE("Python Coverage", "PYTHON", "COVERAGE",
+			new PythonCoverageHelper(), new BasicCoverageEvaluator()),
+	/**
+	 * Java变异覆盖题目,统计变异覆盖率
+	 */
+	JAVA_MUTATION("Java Mutation", "JAVA", "MUTATION",
+			new JavaMutationHelper(), new BasicMutationEvaluator());
+
+	private final String name;
+	private final String language;
+	private final String target;
+	private final ProblemHelper helper;
+	private final Evaluator evaluator;
+
+	ProblemType(String name, String language, String target, ProblemHelper helper, Evaluator evaluator) {
+		this.name = name;
+		this.language = language;
+		this.target = target;
+		this.helper = helper;
+		this.evaluator = evaluator;
+	}
+
+	@Override
+	public String toString() {
+		return name;
+	}
+}

+ 23 - 0
src/main/java/com/moekr/moocoder/util/enums/UserRole.java

@@ -0,0 +1,23 @@
+package com.moekr.moocoder.util.enums;
+
+public enum UserRole {
+	/**
+	 * 学生角色
+	 */
+	STUDENT("学生"),
+	/**
+	 * 教师角色
+	 */
+	TEACHER("教师");
+
+	private final String name;
+
+	UserRole(String name) {
+		this.name = name;
+	}
+
+	@Override
+	public String toString() {
+		return name;
+	}
+}

+ 15 - 0
src/main/java/com/moekr/moocoder/util/exceptions/AccessDeniedException.java

@@ -0,0 +1,15 @@
+package com.moekr.moocoder.util.exceptions;
+
+import org.springframework.http.HttpStatus;
+
+public class AccessDeniedException extends ServiceException {
+	private static final HttpStatus HTTP_STATUS = HttpStatus.FORBIDDEN;
+
+	public AccessDeniedException() {
+		this(HTTP_STATUS.getReasonPhrase());
+	}
+
+	public AccessDeniedException(String message) {
+		super(HTTP_STATUS.value(), message);
+	}
+}

Некоторые файлы не были показаны из-за большого количества измененных файлов