DESKTOP-USV654P\pc il y a 1 an
commit
ba057a16d9
100 fichiers modifiés avec 7251 ajouts et 0 suppressions
  1. 25 0
      .dockerignore
  2. 63 0
      .gitattributes
  3. 258 0
      .gitignore
  4. 624 0
      README_API.md
  5. 149 0
      YGNT.Exam.sln
  6. 2 0
      YGNT.Exam.sln.DotSettings
  7. 37 0
      build/build-mvc.ps1
  8. 12 0
      docker/mvc/docker-compose.yml
  9. 1 0
      docker/mvc/down.ps1
  10. 1 0
      docker/mvc/up.ps1
  11. 38 0
      src/QuestionSwap/OfficeAppHelp/Help/DllImportHelp.cs
  12. 33 0
      src/QuestionSwap/OfficeAppHelp/Help/FileHelp.cs
  13. 130 0
      src/QuestionSwap/OfficeAppHelp/Help/IntHelp.cs
  14. 26 0
      src/QuestionSwap/OfficeAppHelp/Help/PathHelp.cs
  15. 179 0
      src/QuestionSwap/OfficeAppHelp/Help/StringHelp.cs
  16. 65 0
      src/QuestionSwap/OfficeAppHelp/OfficeApp.cs
  17. 26 0
      src/QuestionSwap/OfficeAppHelp/OfficeAppHelp.csproj
  18. 31 0
      src/QuestionSwap/OfficeAppHelp/OfficeType.cs
  19. 26 0
      src/QuestionSwap/OfficeAppHelp/Word/ColorEx.cs
  20. 117 0
      src/QuestionSwap/OfficeAppHelp/Word/DocEx.cs
  21. 256 0
      src/QuestionSwap/OfficeAppHelp/Word/RangeEx.cs
  22. 70 0
      src/QuestionSwap/OfficeAppHelp/Word/TableEx.cs
  23. 343 0
      src/QuestionSwap/OfficeAppHelp/Word/WordApp.cs
  24. 11 0
      src/QuestionSwap/OfficeOpenXmlHelp/OfficeOpenXmlHelp.csproj
  25. 91 0
      src/QuestionSwap/OfficeOpenXmlHelp/Word/WordOpenXml.cs
  26. 38 0
      src/QuestionSwap/QuestionSwapDoc/Help/HttpHelp.cs
  27. 24 0
      src/QuestionSwap/QuestionSwapDoc/QuestionSwapDoc.csproj
  28. 390 0
      src/QuestionSwap/QuestionSwapDoc/SwapQuestion.cs
  29. 22 0
      src/QuestionSwap/QuestionSwapDoc/SwapQuestionModel/QueDoc.cs
  30. 117 0
      src/QuestionSwap/QuestionSwapDoc/SwapQuestionModel/QueItem.cs
  31. 196 0
      src/QuestionSwap/QuestionSwapDoc/SwapQuestionModel/QueItemRange.cs
  32. 22 0
      src/QuestionSwap/QuestionSwapDoc/SwapQuestionModel/QueItemType.cs
  33. 22 0
      src/QuestionSwap/QuestionSwapDoc/SwapQuestionModel/QueTypeString.cs
  34. 1061 0
      src/QuestionSwap/QuestionSwapDoc/SwapWord.cs
  35. 18 0
      src/QuestionSwap/QuestionSwapDoc/SwapWordModel/DaTiKaStyle.cs
  36. 26 0
      src/QuestionSwap/QuestionSwapDoc/SwapWordModel/PaperDaTiKa.cs
  37. 21 0
      src/QuestionSwap/QuestionSwapDoc/SwapWordModel/PaperQue.cs
  38. 48 0
      src/QuestionSwap/QuestionSwapDoc/SwapWordModel/SwapDaAnConfig.cs
  39. 93 0
      src/QuestionSwap/QuestionSwapDoc/SwapWordModel/SwapDaTiKaConfig.cs
  40. 60 0
      src/QuestionSwap/QuestionSwapDoc/SwapWordModel/SwapQue.cs
  41. 55 0
      src/QuestionSwap/QuestionSwapDoc/SwapWordModel/SwapQueConfig.cs
  42. 91 0
      src/QuestionSwap/QuestionSwapDoc/SwapWordModel/SwapQueContent.cs
  43. 129 0
      src/QuestionSwap/QuestionSwapDoc/SwapWordModel/SwapTiXin.cs
  44. 42 0
      src/QuestionSwap/QuestionSwapDoc/SwapWordModel/SwapTiXinDaTiKaType.cs
  45. 108 0
      src/QuestionSwap/QuestionSwapDoc/SwapWordModel/SwapTiXins.cs
  46. 21 0
      src/QuestionSwap/QuestionSwapDoc/SwapWordModel/SwapWordCreateQueResult.cs
  47. 88 0
      src/QuestionSwap/QuestionSwapDoc/SwapWordModel/SwapWordJoinQueItem.cs
  48. 14 0
      src/QuestionSwap/QuestionSwapDoc/SwapWordModel/SwapWordResultType.cs
  49. 70 0
      src/QuestionSwap/QuestionSwapDoc/SwapWordModel/ZhuanDinXian.cs
  50. 28 0
      src/QuestionSwap/QuestionSwapDoc/使用方法.txt
  51. 10 0
      src/YGNT.Exam.Application/AppConsts.cs
  52. 64 0
      src/YGNT.Exam.Application/Authorization/AbpLoginResultTypeHelper.cs
  53. 65 0
      src/YGNT.Exam.Application/Authorization/Accounts/AccountAppService.cs
  54. 12 0
      src/YGNT.Exam.Application/Authorization/Accounts/Dto/IsTenantAvailableInput.cs
  55. 19 0
      src/YGNT.Exam.Application/Authorization/Accounts/Dto/IsTenantAvailableOutput.cs
  56. 51 0
      src/YGNT.Exam.Application/Authorization/Accounts/Dto/RegisterInput.cs
  57. 7 0
      src/YGNT.Exam.Application/Authorization/Accounts/Dto/RegisterOutput.cs
  58. 9 0
      src/YGNT.Exam.Application/Authorization/Accounts/Dto/TenantAvailabilityState.cs
  59. 13 0
      src/YGNT.Exam.Application/Authorization/Accounts/IAccountAppService.cs
  60. 16 0
      src/YGNT.Exam.Application/Configuration/ConfigurationAppService.cs
  61. 11 0
      src/YGNT.Exam.Application/Configuration/Dto/ChangeUiThemeInput.cs
  62. 10 0
      src/YGNT.Exam.Application/Configuration/IConfigurationAppService.cs
  63. 14 0
      src/YGNT.Exam.Application/Configuration/Ui/UiThemeInfo.cs
  64. 36 0
      src/YGNT.Exam.Application/Configuration/Ui/UiThemes.cs
  65. 47 0
      src/YGNT.Exam.Application/ExamAppServiceBase.cs
  66. 31 0
      src/YGNT.Exam.Application/ExamApplicationModule.cs
  67. 59 0
      src/YGNT.Exam.Application/Extension/ResultHelper.cs
  68. 23 0
      src/YGNT.Exam.Application/Messages/Dto/UserMessageDto.cs
  69. 7 0
      src/YGNT.Exam.Application/Messages/Dto/UserMessageInfoDto.cs
  70. 22 0
      src/YGNT.Exam.Application/Messages/Dto/UserMessageSearchInputDto.cs
  71. 48 0
      src/YGNT.Exam.Application/Messages/IUserMessageAppService.cs
  72. 102 0
      src/YGNT.Exam.Application/Messages/UserMessageAppService.cs
  73. 29 0
      src/YGNT.Exam.Application/MultiTenancy/Dto/CreateTenantDto.cs
  74. 11 0
      src/YGNT.Exam.Application/MultiTenancy/Dto/PagedTenantResultRequestDto.cs
  75. 22 0
      src/YGNT.Exam.Application/MultiTenancy/Dto/TenantDto.cs
  76. 10 0
      src/YGNT.Exam.Application/MultiTenancy/ITenantAppService.cs
  77. 123 0
      src/YGNT.Exam.Application/MultiTenancy/TenantAppService.cs
  78. 311 0
      src/YGNT.Exam.Application/Net/MimeTypes/MimeTypeNames.cs
  79. 18 0
      src/YGNT.Exam.Application/Properties/AssemblyInfo.cs
  80. 40 0
      src/YGNT.Exam.Application/Roles/Dto/CreateRoleDto.cs
  81. 11 0
      src/YGNT.Exam.Application/Roles/Dto/FlatPermissionDto.cs
  82. 13 0
      src/YGNT.Exam.Application/Roles/Dto/GetRoleForEditOutput.cs
  83. 10 0
      src/YGNT.Exam.Application/Roles/Dto/GetRolesInput.cs
  84. 10 0
      src/YGNT.Exam.Application/Roles/Dto/PagedRoleResultRequestDto.cs
  85. 25 0
      src/YGNT.Exam.Application/Roles/Dto/PermissionDto.cs
  86. 42 0
      src/YGNT.Exam.Application/Roles/Dto/RoleDto.cs
  87. 35 0
      src/YGNT.Exam.Application/Roles/Dto/RoleEditDto.cs
  88. 34 0
      src/YGNT.Exam.Application/Roles/Dto/RoleListDto.cs
  89. 29 0
      src/YGNT.Exam.Application/Roles/Dto/RoleMapProfile.cs
  90. 16 0
      src/YGNT.Exam.Application/Roles/IRoleAppService.cs
  91. 161 0
      src/YGNT.Exam.Application/Roles/RoleAppService.cs
  92. 14 0
      src/YGNT.Exam.Application/Sessions/Dto/ApplicationInfoDto.cs
  93. 11 0
      src/YGNT.Exam.Application/Sessions/Dto/GetCurrentLoginInformationsOutput.cs
  94. 14 0
      src/YGNT.Exam.Application/Sessions/Dto/TenantLoginInfoDto.cs
  95. 18 0
      src/YGNT.Exam.Application/Sessions/Dto/UserLoginInfoDto.cs
  96. 11 0
      src/YGNT.Exam.Application/Sessions/ISessionAppService.cs
  97. 39 0
      src/YGNT.Exam.Application/Sessions/SessionAppService.cs
  98. 19 0
      src/YGNT.Exam.Application/Users/Dto/ChangePasswordDto.cs
  99. 10 0
      src/YGNT.Exam.Application/Users/Dto/ChangeUserLanguageDto.cs
  100. 71 0
      src/YGNT.Exam.Application/Users/Dto/CreateUserDto.cs

+ 25 - 0
.dockerignore

@@ -0,0 +1,25 @@
+**/.classpath
+**/.dockerignore
+**/.env
+**/.git
+**/.gitignore
+**/.project
+**/.settings
+**/.toolstarget
+**/.vs
+**/.vscode
+**/*.*proj.user
+**/*.dbmdl
+**/*.jfm
+**/azds.yaml
+**/bin
+**/charts
+**/docker-compose*
+**/Dockerfile*
+**/node_modules
+**/npm-debug.log
+**/obj
+**/secrets.dev.yaml
+**/values.dev.yaml
+LICENSE
+README.md

+ 63 - 0
.gitattributes

@@ -0,0 +1,63 @@
+###############################################################################
+# Set default behavior to automatically normalize line endings.
+###############################################################################
+* text=auto
+
+###############################################################################
+# Set default behavior for command prompt diff.
+#
+# This is need for earlier builds of msysgit that does not have it on by
+# default for csharp files.
+# Note: This is only used by command line
+###############################################################################
+#*.cs     diff=csharp
+
+###############################################################################
+# Set the merge driver for project and solution files
+#
+# Merging from the command prompt will add diff markers to the files if there
+# are conflicts (Merging from VS is not affected by the settings below, in VS
+# the diff markers are never inserted). Diff markers may cause the following 
+# file extensions to fail to load in VS. An alternative would be to treat
+# these files as binary and thus will always conflict and require user
+# intervention with every merge. To do so, just uncomment the entries below
+###############################################################################
+#*.sln       merge=binary
+#*.csproj    merge=binary
+#*.vbproj    merge=binary
+#*.vcxproj   merge=binary
+#*.vcproj    merge=binary
+#*.dbproj    merge=binary
+#*.fsproj    merge=binary
+#*.lsproj    merge=binary
+#*.wixproj   merge=binary
+#*.modelproj merge=binary
+#*.sqlproj   merge=binary
+#*.wwaproj   merge=binary
+
+###############################################################################
+# behavior for image files
+#
+# image files are treated as binary by default.
+###############################################################################
+#*.jpg   binary
+#*.png   binary
+#*.gif   binary
+
+###############################################################################
+# diff behavior for common document formats
+# 
+# Convert binary document formats to text before diffing them. This feature
+# is only available from the command line. Turn it on by uncommenting the 
+# entries below.
+###############################################################################
+#*.doc   diff=astextplain
+#*.DOC   diff=astextplain
+#*.docx  diff=astextplain
+#*.DOCX  diff=astextplain
+#*.dot   diff=astextplain
+#*.DOT   diff=astextplain
+#*.pdf   diff=astextplain
+#*.PDF   diff=astextplain
+#*.rtf   diff=astextplain
+#*.RTF   diff=astextplain

+ 258 - 0
.gitignore

@@ -0,0 +1,258 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+
+# User-specific files
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+bld/
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+
+# Visual Studio 2015 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUNIT
+*.VisualState.xml
+TestResult.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# DNX
+project.lock.json
+artifacts/
+
+*_i.c
+*_p.c
+*_i.h
+*.ilk
+*.meta
+*.obj
+*.pch
+*.pdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*.log
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+*.VC.db
+*.VC.VC.opendb
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# JustCode is a .NET coding add-in
+.JustCode
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+wwwroot/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# TODO: Comment the next line if you want to checkin your web deploy settings
+# but database connection strings (with potential passwords) will be unencrypted
+*.pubxml
+*.publishproj
+
+# Microsoft Azure Web App publish settings. Comment the next line if you want to
+# checkin your Azure Web App publish settings, but sensitive information contained
+# in these scripts will be unencrypted
+PublishScripts/
+
+# NuGet Packages
+*.nupkg
+# The packages folder can be ignored because of Package Restore
+**/packages/*
+# except build/, which is used as an MSBuild target.
+!**/packages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/packages/repositories.config
+# NuGet v3's project.json files produces more ignoreable files
+*.nuget.props
+*.nuget.targets
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Windows Store app package directories and files
+AppPackages/
+BundleArtifacts/
+Package.StoreAssociation.xml
+_pkginfo.txt
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!*.[Cc]ache/
+
+# Others
+ClientBin/
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.pfx
+*.publishsettings
+node_modules/
+orleans.codegen.cs
+
+# Since there are multiple workflows, uncomment next line to ignore bower_components
+# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
+#bower_components/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+
+# SQL Server files
+*.mdf
+*.ldf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+.paket/paket.exe
+paket-files/
+
+# FAKE - F# Make
+.fake/
+
+# JetBrains Rider
+.idea/
+*.sln.iml
+
+
+YGNT.QuestionLibrary.Application.xml
+YGNT.Exam.Web.Host.xml
+YGNT.Exam.Application.xml

+ 624 - 0
README_API.md

@@ -0,0 +1,624 @@
+
+# 组卷系统对外开放文档 v1.0.0
+
+## 1 规范说明
+
+### 1.1 通信协议
+
+HTTPS || HTTP协议
+
+### 1.2 请求方法
+本文档的接口遵循RESTful设计风格。
+
+### 1.3 字符编码
+HTTP通讯及报文BASE64编码均采用UTF-8字符集编码格式。
+
+### 1.4 格式说明
+元素出现要求说明:
+
+符号				|说明
+:----:			|:---
+R				|报文中该元素必须出现(Required)
+O				|报文中该元素可选出现(Optional)
+C				|报文中该元素在一定条件下出现(Conditional)
+
+### 1.5 报文规范说明
+
+1. 报文规范仅针对交易请求数据进行描述;  
+
+2. 报文规范中请求报文的内容为Https请求报文中RequestData值的明文内容;
+
+3. 报文规范分为请求报文和响应报文。请求报文描述由发起方,响应报文由报文接收方响应。
+
+### 1.6 请求报文结构
+接口只接收两个参数 **RequestData** 和 **SignData** ,其中RequestData的值为请求内容,SignData的值为签名内容。
+
+#### 1.6.1 参数说明
+**RequestData(请求内容):** 其明文为每次请求的具体参数,采用 JSON 格式,依次经过 DES 加密(以UTF-8编码、BASE64编码输出结果)和 URLEncode 后,作为 RequestData 的值。  
+
+**SignData(签名内容):** 请求参数(明文)的MD5加密字符串,用于校验RequestData是否合法。
+
+#### 1.6.2 请求内容(RequestData)明文结构说明
+
+采用JSON格式,其中包含Header(公有参数)、Body(私有参数)节点:
+
+名称		|描述											|备注
+:---	|:---											|:---
+公共参数	|每个接口都包含的通用参数,以JSON格式存放在Header属性	|详见以下公共参数说明
+私有参数	|每个接口特有的参数,以JSON格式存放在Body属性		    |详见每个接口定义
+
+**公共参数说明:**
+
+公共参数(Header)是用于标识产品及接口鉴权的参数,每次请求均需要携带这些参数:
+
+参数名称				|类型		|出现要求	|描述  
+:----				|:---		|:------	|:---	
+authorization		|string		|R			|用户登录后token,没有登录则为空字符串
+
+## 2. 接口定义
+
+### 2.1 试题难度
+- **接口说明:** 试题难度
+- **接口地址:** /api/services/QuestionLibrary/QuestionBaseData/GetDifficulty
+
+#### 2.1.1 请求参数
+
+参数名称						 |类型		    |出现要求	|描述  
+:----						 |:---		    |:------	|:---	
+Header						 | 		|R			|请求报文头
+ authorization			 |string		|R			|用户登录后token,没有登录则为空字符串
+
+
+请求示例:
+
+```
+{
+    "Header":{
+        "authorization":"",
+    },
+}
+
+```
+
+
+#### 2.1.2 返回结果
+
+参数名称						    |类型		|出现要求	|描述  
+:----						    |:---		|:------	|:---	
+success						    |bool		|R			|响应码,true为成功,false失败
+error						    |string		|R			| 
+result						    |array		|R			| 
+ name      			    |string		|R			|名称
+ value             	    |string		|R			|ID
+
+示例:
+
+```
+{
+    "success":true,
+    "error":null,
+    "result":[{
+        "name":'容易',
+        "value":1,
+    }]
+}
+```
+
+
+
+### 2.2 学科信息
+- **接口说明:** 学科信息
+- **接口地址:** /api/services/QuestionLibrary/Discipline/GetList
+
+#### 2.2.1 请求参数
+
+参数名称						 |类型		    |出现要求	|描述
+:----						 |:---		    |:------	|:---	
+Header						 | 		|R			|请求报文头
+ authorization			 |string		|R			|用户登录后token,没有登录则为空字符串
+Body						 | 		|R			| 
+ EducationCategoryId 	 |int   		|O			|教育类别Id
+ KeyWord            	 |string		|O			|学科名称
+ SkipCount          	 |int   		|R			|开始记录数
+ MaxResultCount    	 |int   		|R			|记录的条数
+
+请求示例:
+
+```
+
+{
+    "Header":{
+        "authorization":"",
+    },
+    "Body":{
+        "EducationCategoryId":null,
+        "KeyWord":null,
+        "SkipCount":0,
+        "MaxResultCount":9999
+    }
+}
+
+```
+
+
+#### 2.2.2 返回结果
+
+参数名称						    |类型		|出现要求	|描述
+:----						    |:---		|:------	|:---	
+success						    |bool		|R			|响应码,true为成功,false失败
+error						    |string		|R			| 
+result						    |object		|R			| 
+ totalCount                |int	    |R			|总记录数
+ items                     |array	    |R			| 
+  id                  |int	    |R			|学科ID
+  name                |string	    |R			|学科名称
+  educationCategoryId |int	    |R			|教育类别Id
+  EducationCategory   |string	    |R			|教育类别名称
+
+
+示例:
+
+```
+{
+    "success":true,
+    "error":null,
+    "result":{
+        "totalCount":1,
+        "items":[{
+            "id":10,
+            "name":"数学",
+            "educationCategoryId":4,
+            "EducationCategory":"高中"
+        }],
+    }
+}
+```
+
+
+### 2.3 教材信息
+- **接口说明:** 教材信息
+- **接口地址:** /api/services/QuestionLibrary/Textbook/GetList
+
+#### 2.3.1 请求参数
+
+参数名称						    |类型		    |出现要求	|描述
+:----						    |:---		    |:------	|:---	
+Header						    | 		    |R			|请求报文头
+ authorization			    |string		    |R			|用户登录后token,没有登录则为空字符串
+Body						    | 		    |R			| 
+ educationCategoryId       |int	        |O			|教育类别Id
+ DisciplineId       	    |int   		    |O			|学科id
+ IsActive            	    |bool  		    |O			|是否激活
+ SkipCount          	    |int   		    |R			|开始记录数
+ MaxResultCount    	    |int   		    |R			|记录的条数
+
+请求示例:
+
+```
+
+{
+    "Header":{
+        "authorization":"",
+    },
+    "Body":{
+        "EducationCategoryId":null,
+        "DisciplineId":10,
+        "IsActive":true,
+        "SkipCount":0,
+        "MaxResultCount":9999
+    }
+}
+
+```
+
+
+#### 2.3.2 返回结果
+
+参数名称						    |类型		|出现要求	|描述
+:----						    |:---		|:------	|:---	
+success						    |bool		|R			|响应码,true为成功,false失败
+error						    |string		|R			| 
+result						    |object		|R			| 
+ totalCount                |int	    |R			|总记录数
+ items                     |array	    |R			| 
+  id                  |int	    |R			|教材ID
+  name                |string	    |R			|教材名称
+  isActive            |bool	    |R			|是否激活
+  educationCategory   |string	    |R			|教育类别名称
+  disciplineId        |int	    |R			|学科ID
+  discipline          |string	    |R			|学科名称
+
+示例:
+
+```
+{
+    "success":true,
+    "error":null,
+    "result":{
+        "totalCount":1,
+        "items":[{
+            "id":2,
+            "name":"教材1",
+            "isActive":4,
+            "educationCategory":"高中",
+            "disciplineId":10,
+            "discipline":"数学"
+        }],
+    }
+}
+```
+
+### 2.4 章节信息
+- **接口说明:** 章节信息
+- **接口地址:** /api/services/QuestionLibrary/Chapter/GetList
+
+#### 2.4.1 请求参数
+
+参数名称						    |类型		    |出现要求	|描述
+:----						    |:---		    |:------	|:---	
+Header						    | 		    |R			|请求报文头
+ authorization			    |string		    |R			|用户登录后token,没有登录则为空字符串
+Body						    | 		    |R			| 
+ ParentId                  |int	        |O			|父章节Id
+ TextbookId       	        |int   		    |R			|教材id
+ KeyWord            	    |string  		|O			|章节名称
+ SkipCount          	    |int   		    |R			|开始记录数
+ MaxResultCount    	    |int   		    |R			|记录的条数
+
+请求示例:
+
+```
+
+{
+    "Header":{
+        "authorization":"",
+    },
+    "Body":{
+        "ParentId":null,
+        "TextbookId":2,
+        "KeyWord":null,
+        "SkipCount":0,
+        "MaxResultCount":9999
+    }
+}
+
+```
+
+
+#### 2.4.2 返回结果
+
+参数名称						    |类型		|出现要求	|描述
+:----						    |:---		|:------	|:---	
+success						    |bool		|R			|响应码,true为成功,false失败
+error						    |string		|R			| 
+result						    |object		|R			| 
+ totalCount                |int	    |R			|总记录数
+ items                     |array	    |R			| 
+  id                  |int	    |R			|章节ID
+  name                |string	    |R			|章节名称
+  parentId            |int	    |R			|父章节Id
+  disciplineId        |int	    |R			|学科ID
+  discipline          |string	    |R			|学科名称
+  textbookId          |string	    |R			|教材id
+  textbook            |string	    |R			|教材名称
+  path                |string	    |R			|章节名称路径
+  hasChild            |true	    |R			|是否有子章节
+
+
+示例:
+
+```
+{
+    "success":true,
+    "error":null,
+    "result":{
+        "totalCount":1,
+        "items":[{
+            "id":33,
+            "name":"章节1",
+            "parentId":null,
+            "disciplineId":10,
+            "discipline":"数学",
+            "textbookId":2,
+            "textbook":"教材1",
+            "path":"章节1",
+            "hasChild":true
+        }],
+    }
+}
+```
+
+### 2.5 学科试题题型
+- **接口说明:** 学科试题题型
+- **接口地址:** /api/services/QuestionLibrary/DisciplineQuestionType/GetList
+
+#### 2.5.1 请求参数
+
+参数名称						    |类型		    |出现要求	|描述
+:----						    |:---		    |:------	|:---	
+Header						    | 		    |R			|请求报文头
+ authorization			    |string		    |R			|用户登录后token,没有登录则为空字符串
+Body						    | 		    |R			| 
+ disciplineId              |int	        |R			|学科id
+
+请求示例:
+
+```
+
+{
+    "Header":{
+        "authorization":"",
+    },
+    "Body":{
+        "disciplineId":10
+    }
+}
+
+```
+
+
+#### 2.5.2 返回结果
+
+参数名称						    |类型		|出现要求	|描述
+:----						    |:---		|:------	|:---	
+success						    |bool		|R			|响应码,true为成功,false失败
+error						    |string		|R			| 
+result						    |array		|R			| 
+ id                        |int	    |R			|试题题型Id
+ name                      |string	    |R			|试题题型名称
+ discipline                |string	    |R			|学科名称
+ questionCategory          |string	    |R			|试题类别 代码定义请见“附录A 试题类别说明”
+ questionType              |string	    |R			|试题类型
+
+
+示例:
+
+```
+{
+    "success":true,
+    "error":null,
+    "result":[{
+        "id":18,
+        "name":"单选题1",
+        "discipline":"数学",
+        "questionCategory":0,
+        "questionType":"单选题"
+    }]
+}
+```
+
+
+### 2.6 知识点
+- **接口说明:** 知识点
+- **接口地址:** /api/services/QuestionLibrary/KnowledgePoint/GetList
+
+#### 2.6.1 请求参数
+
+参数名称						    |类型		    |出现要求	|描述
+:----						    |:---		    |:------	|:---	
+Header						    | 		    |R			|请求报文头
+ authorization			    |string		    |R			|用户登录后token,没有登录则为空字符串
+Body						    | 		    |R			| 
+ ParentId                  |int	        |O			|父知识点Id
+ DisciplineId       	    |int   		    |O			|学科id
+ CognitiveAbilityId       	|int   		    |O			|认知能力Id
+ KeyWord            	    |string  		|O			|知识点名称
+ SkipCount          	    |int   		    |R			|开始记录数
+ MaxResultCount    	    |int   		    |R			|记录的条数
+
+请求示例:
+
+```
+
+{
+    "Header":{
+        "authorization":"",
+    },
+    "Body":{
+        "ParentId":null,
+        "DisciplineId":10,
+        "CognitiveAbilityId":null,
+        "KeyWord":null,
+        "SkipCount":0,
+        "MaxResultCount":9999
+    }
+}
+
+```
+
+
+#### 2.6.2 返回结果
+
+参数名称						    |类型		|出现要求	|描述
+:----						    |:---		|:------	|:---	
+success						    |bool		|R			|响应码,true为成功,false失败
+error						    |string		|R			| 
+result						    |object		|R			| 
+ totalCount                |int	    |R			|总记录数
+ items                     |array	    |R			| 
+  id                  |int	    |R			|知识点ID
+  name                |string	    |R			|知识点名称
+  parentId            |int	    |R			|父知识点Id
+  disciplineId        |int	    |R			|学科ID
+  discipline          |string	    |R			|学科名称
+  cognitiveAbilityId  |string	    |R			|认知能力Id
+  cognitiveAbility    |string	    |R			|认知能力称
+  path                |string	    |R			|章知识点名称路径
+  hasChild            |true	    |R			|是否有子知识点
+
+
+示例:
+
+```
+{
+    "success":true,
+    "error":null,
+    "result":{
+        "totalCount":1,
+        "items":[{
+            "id":110,
+            "name":"三角函数的相关概念",
+            "parentId":null,
+            "disciplineId":10,
+            "discipline":"数学",
+            "cognitiveAbilityId":0,
+            "cognitiveAbility":null,
+            "path":"三角函数的相关概念",
+            "hasChild":true
+        }],
+    }
+}
+```
+
+
+### 2.7 试题列表
+- **接口说明:** 试题列表
+- **接口地址:** /api/services/QuestionLibrary/Question/GetList
+
+#### 2.7.1 请求参数
+
+参数名称						    |类型		    |出现要求	|描述
+:----						    |:---		    |:------	|:---	
+Header						    | 		    |R			|请求报文头
+ authorization			    |string		    |R			|用户登录后token,没有登录则为空字符串
+Body						    | 		    |R			| 
+ DisciplineId       	    |int   		    |R			|学科id
+ QuestionTypeId       	    |int   		    |O			|题型Id
+ DisciplineQuestionTypeId  |int   		    |O			|学科细分题型Id
+ Difficulty            	|int      		|O			|试题难度 代码定义请见“附录B 试题难度说明”
+ IsActive            	    |bool      		|O			|是否激活
+&emsp;KnowledgePointIdList      |array<long>    |O			|知识点id
+&emsp;ChapterId       	        |long   		|O			|章节id
+&emsp;IncludeChildChapter       |bool   		|O			|是否包括子章节(默认包括)
+&emsp;IncludeAncestor           |bool  		    |O			|是否包括父级知识点(默认不包括)
+&emsp;StartAriseTime            |datetime   	|O			|出题年份(范围开始)
+&emsp;EndAriseTime              |datetime  		|O			|出题年份(范围结束)
+&emsp;KeyWord            	    |string  		|O			|试题简称
+&emsp;SkipCount          	    |int   		    |R			|开始记录数
+&emsp;MaxResultCount    	    |int   		    |R			|记录的条数
+
+请求示例:
+
+```
+
+{
+    "Header":{
+        "authorization":"",
+    },
+    "Body":{
+        "DisciplineId":10,
+        "QuestionTypeId":null,
+        "DisciplineQuestionTypeId":null,
+        "Difficulty":null,
+        "IsActive":null,
+        "KnowledgePointIdList":null,
+        "ChapterId":null,
+        "IncludeChildChapter":true,
+        "IncludeAncestor":false,
+        "StartAriseTime":null,
+        "EndAriseTime":null,
+        "KeyWord":null,
+        "SkipCount":0,
+        "MaxResultCount":15
+    }
+}
+
+```
+
+
+#### 2.7.2 返回结果
+
+参数名称						            |类型		|出现要求	|描述
+:----						            |:---		|:------	|:---	
+success						            |bool		|R			|响应码,true为成功,false失败
+error						            |string		|R			|&nbsp;
+result						            |object		|R			|&nbsp;
+&emsp;totalCount                        |int	    |R			|总记录数
+&emsp;items                             |array	    |R			|&nbsp;
+&emsp;&emsp;id                          |int	    |R			|试题ID
+&emsp;&emsp;shortName                   |string	    |R			|试题简称
+&emsp;&emsp;analysis                    |string	    |R			|解析html
+&emsp;&emsp;answer                      |string	    |R			|答案html
+&emsp;&emsp;ariseTime                   |datetime	|R			|出题年份
+&emsp;&emsp;chapter                     |string	    |R			|章节
+&emsp;&emsp;checkPoints                 |string	    |R			|考点
+&emsp;&emsp;convertFailMessage          |string	    |R			|转换失败信息
+&emsp;&emsp;difficulty                  |string	    |R			|难度
+&emsp;&emsp;discipline                  |string	    |R			|学科名称
+&emsp;&emsp;disciplineId                |string	    |R			|学科ID
+&emsp;&emsp;disciplineQuestionType      |string	    |R			|学科细分题型
+&emsp;&emsp;disciplineQuestionTypeId    |int	    |R			|学科细分题型Id
+&emsp;&emsp;html                        |string	    |R			|试题html
+&emsp;&emsp;questionCategory            |int	    |R			|题型类别 代码定义请见“附录A 试题类别说明”
+&emsp;&emsp;questionConvertState        |int	    |R			|试题转换状态 代码定义请见“附录C 试题转换状态说明”
+&emsp;&emsp;questionType                |string	    |R			|题型名称
+&emsp;&emsp;questionTypeId              |int	    |R			|题型Id
+&emsp;&emsp;knowledgePoints             |array	    |R			|&nbsp;
+&emsp;&emsp;&emsp;id                    |int	    |R			|知识点名称ID
+&emsp;&emsp;&emsp;name                  |string	    |R			|知识点名称
+
+示例:
+
+```
+{
+    "success":true,
+    "error":null,
+    "result":{
+        "totalCount":1,
+        "items":[{
+            "id":382,
+            "shortName":"一幅完整的机械零件制图需包括的要素有( )。",
+            "analysis":"<html><head><style></style></head><body></body></html>",
+            "answer":"A,B,C,D,E",
+            "ariseTime":null,
+            "chapter":"章节2",
+            "checkPoints":null,
+            "convertFailMessage":null,
+            "difficulty":"较易",
+            "discipline":"数学",
+            "disciplineId":10,
+            "disciplineQuestionType":"多选12",
+            "disciplineQuestionTypeId":21,
+            "html":"<html><head><style></style></head><body></body></html>",
+            "questionCategory":0,
+            "questionConvertState":0,
+            "questionType":"选择题",
+            "questionTypeId":1,
+            "knowledgePoints":[{
+                “id”:110,
+                "name":"三角函数的相关概念"
+            }]
+        }]
+    }
+}
+```
+
+
+## 3 附录A 试题类别说明
+
+响应码	|说明  
+:----	|:---
+0		|客观题
+1		|判断题
+2		|非客观题
+
+
+## 4 附录B 试题难度说明
+
+响应码	|说明
+:----	|:---
+1		|容易
+2		|较易
+3		|中等
+4		|较难
+5		|困难
+
+
+## 5 附录C 试题转换状态说明
+
+响应码	|说明
+:----	|:---
+0		|正常
+1		|转换中
+2		|转换失败
+3		|解析替换失败

+ 149 - 0
YGNT.Exam.sln

@@ -0,0 +1,149 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 16
+VisualStudioVersion = 16.0.30804.86
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{AFAA0841-BD93-466F-B8F4-FB4EEC86F1FC}"
+	ProjectSection(SolutionItems) = preProject
+		组卷系统交接文档.txt = 组卷系统交接文档.txt
+	EndProjectSection
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{F10AA149-2626-486E-85BB-9CD5365F3016}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YGNT.Exam.Core", "src\YGNT.Exam.Core\YGNT.Exam.Core.csproj", "{0FA75A5B-AB83-4FD0-B545-279774C01E87}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YGNT.Exam.Application", "src\YGNT.Exam.Application\YGNT.Exam.Application.csproj", "{3870C648-4AEA-4B85-BA3F-F2F63B96136A}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YGNT.Exam.Tests", "test\YGNT.Exam.Tests\YGNT.Exam.Tests.csproj", "{0D4C5D00-C144-4213-A007-4B8944113AB1}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YGNT.Exam.Migrator", "src\YGNT.Exam.Migrator\YGNT.Exam.Migrator.csproj", "{880B3591-E057-46FE-B525-10BD83828B93}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YGNT.Exam.Web.Host", "src\YGNT.Exam.Web.Host\YGNT.Exam.Web.Host.csproj", "{38E184BD-E874-4633-A947-AED4FDB73F40}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YGNT.Exam.Web.Core", "src\YGNT.Exam.Web.Core\YGNT.Exam.Web.Core.csproj", "{22CFE0D2-8DCA-42D7-AD7D-784C3862493F}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YGNT.Exam.EntityFrameworkCore", "src\YGNT.Exam.EntityFrameworkCore\YGNT.Exam.EntityFrameworkCore.csproj", "{E0580562-F8F2-4EBB-B07A-ABFC6F2C314F}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "YGNT.QuestionLibrary", "YGNT.QuestionLibrary", "{3DA0CA28-56BE-4E2B-907D-4168DC2B0E71}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YGNT.QuestionLibrary.Application", "src\YGNT.QuestionLibrary\YGNT.QuestionLibrary.Application\YGNT.QuestionLibrary.Application.csproj", "{A40583CE-C106-4EDF-B339-F4C083F5B919}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YGNT.QuestionLibrary.Core", "src\YGNT.QuestionLibrary\YGNT.QuestionLibrary.Core\YGNT.QuestionLibrary.Core.csproj", "{B076677D-6EDC-42E8-A7E5-0A1AC1B841CE}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YGNT.QuestionLibrary.EntityFrameworkCore", "src\YGNT.QuestionLibrary\YGNT.QuestionLibrary.EntityFrameworkCore\YGNT.QuestionLibrary.EntityFrameworkCore.csproj", "{D23FC75B-F14C-4CB1-B27A-8EA67B608228}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "YGNT.File", "YGNT.File", "{55E03917-CB98-416E-9D5B-041636D2FF97}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YGNT.File.Application", "src\YGNT.File\YGNT.File.Application\YGNT.File.Application.csproj", "{FDD31808-39E1-4067-87F5-C137F5755286}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YGNT.File.Core", "src\YGNT.File\YGNT.File.Core\YGNT.File.Core.csproj", "{7C6BC894-7A5F-42DE-BD41-8640DF0E7117}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "QuestionSwap", "QuestionSwap", "{8AA0CF7A-0652-495B-B9C7-15E96F6DD55D}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OfficeAppHelp", "src\QuestionSwap\OfficeAppHelp\OfficeAppHelp.csproj", "{68122189-1BDA-4D23-BE1A-DDC43553191F}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QuestionSwapDoc", "src\QuestionSwap\QuestionSwapDoc\QuestionSwapDoc.csproj", "{0A102450-9744-4993-9DE9-3418662F0FB0}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YGNT.Infrastructure", "src\YGNT.Infrastructure\YGNT.Infrastructure.csproj", "{B46A73E8-C7F7-4A9E-B2B3-6CF5DC06D23B}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OfficeOpenXmlHelp", "src\QuestionSwap\OfficeOpenXmlHelp\OfficeOpenXmlHelp.csproj", "{C10E08DA-AAF1-41C5-B559-9A92D4B96B21}"
+EndProject
+Global
+	GlobalSection(SolutionConfigurationPlatforms) = preSolution
+		Debug|Any CPU = Debug|Any CPU
+		Release|Any CPU = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(ProjectConfigurationPlatforms) = postSolution
+		{0FA75A5B-AB83-4FD0-B545-279774C01E87}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{0FA75A5B-AB83-4FD0-B545-279774C01E87}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{0FA75A5B-AB83-4FD0-B545-279774C01E87}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{0FA75A5B-AB83-4FD0-B545-279774C01E87}.Release|Any CPU.Build.0 = Release|Any CPU
+		{3870C648-4AEA-4B85-BA3F-F2F63B96136A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{3870C648-4AEA-4B85-BA3F-F2F63B96136A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{3870C648-4AEA-4B85-BA3F-F2F63B96136A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{3870C648-4AEA-4B85-BA3F-F2F63B96136A}.Release|Any CPU.Build.0 = Release|Any CPU
+		{0D4C5D00-C144-4213-A007-4B8944113AB1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{0D4C5D00-C144-4213-A007-4B8944113AB1}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{0D4C5D00-C144-4213-A007-4B8944113AB1}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{0D4C5D00-C144-4213-A007-4B8944113AB1}.Release|Any CPU.Build.0 = Release|Any CPU
+		{880B3591-E057-46FE-B525-10BD83828B93}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{880B3591-E057-46FE-B525-10BD83828B93}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{880B3591-E057-46FE-B525-10BD83828B93}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{880B3591-E057-46FE-B525-10BD83828B93}.Release|Any CPU.Build.0 = Release|Any CPU
+		{38E184BD-E874-4633-A947-AED4FDB73F40}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{38E184BD-E874-4633-A947-AED4FDB73F40}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{38E184BD-E874-4633-A947-AED4FDB73F40}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{38E184BD-E874-4633-A947-AED4FDB73F40}.Release|Any CPU.Build.0 = Release|Any CPU
+		{22CFE0D2-8DCA-42D7-AD7D-784C3862493F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{22CFE0D2-8DCA-42D7-AD7D-784C3862493F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{22CFE0D2-8DCA-42D7-AD7D-784C3862493F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{22CFE0D2-8DCA-42D7-AD7D-784C3862493F}.Release|Any CPU.Build.0 = Release|Any CPU
+		{E0580562-F8F2-4EBB-B07A-ABFC6F2C314F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{E0580562-F8F2-4EBB-B07A-ABFC6F2C314F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{E0580562-F8F2-4EBB-B07A-ABFC6F2C314F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{E0580562-F8F2-4EBB-B07A-ABFC6F2C314F}.Release|Any CPU.Build.0 = Release|Any CPU
+		{A40583CE-C106-4EDF-B339-F4C083F5B919}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{A40583CE-C106-4EDF-B339-F4C083F5B919}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{A40583CE-C106-4EDF-B339-F4C083F5B919}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{A40583CE-C106-4EDF-B339-F4C083F5B919}.Release|Any CPU.Build.0 = Release|Any CPU
+		{B076677D-6EDC-42E8-A7E5-0A1AC1B841CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{B076677D-6EDC-42E8-A7E5-0A1AC1B841CE}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{B076677D-6EDC-42E8-A7E5-0A1AC1B841CE}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{B076677D-6EDC-42E8-A7E5-0A1AC1B841CE}.Release|Any CPU.Build.0 = Release|Any CPU
+		{D23FC75B-F14C-4CB1-B27A-8EA67B608228}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{D23FC75B-F14C-4CB1-B27A-8EA67B608228}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{D23FC75B-F14C-4CB1-B27A-8EA67B608228}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{D23FC75B-F14C-4CB1-B27A-8EA67B608228}.Release|Any CPU.Build.0 = Release|Any CPU
+		{FDD31808-39E1-4067-87F5-C137F5755286}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{FDD31808-39E1-4067-87F5-C137F5755286}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{FDD31808-39E1-4067-87F5-C137F5755286}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{FDD31808-39E1-4067-87F5-C137F5755286}.Release|Any CPU.Build.0 = Release|Any CPU
+		{7C6BC894-7A5F-42DE-BD41-8640DF0E7117}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{7C6BC894-7A5F-42DE-BD41-8640DF0E7117}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{7C6BC894-7A5F-42DE-BD41-8640DF0E7117}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{7C6BC894-7A5F-42DE-BD41-8640DF0E7117}.Release|Any CPU.Build.0 = Release|Any CPU
+		{68122189-1BDA-4D23-BE1A-DDC43553191F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{68122189-1BDA-4D23-BE1A-DDC43553191F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{68122189-1BDA-4D23-BE1A-DDC43553191F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{68122189-1BDA-4D23-BE1A-DDC43553191F}.Release|Any CPU.Build.0 = Release|Any CPU
+		{0A102450-9744-4993-9DE9-3418662F0FB0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{0A102450-9744-4993-9DE9-3418662F0FB0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{0A102450-9744-4993-9DE9-3418662F0FB0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{0A102450-9744-4993-9DE9-3418662F0FB0}.Release|Any CPU.Build.0 = Release|Any CPU
+		{B46A73E8-C7F7-4A9E-B2B3-6CF5DC06D23B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{B46A73E8-C7F7-4A9E-B2B3-6CF5DC06D23B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{B46A73E8-C7F7-4A9E-B2B3-6CF5DC06D23B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{B46A73E8-C7F7-4A9E-B2B3-6CF5DC06D23B}.Release|Any CPU.Build.0 = Release|Any CPU
+		{C10E08DA-AAF1-41C5-B559-9A92D4B96B21}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{C10E08DA-AAF1-41C5-B559-9A92D4B96B21}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{C10E08DA-AAF1-41C5-B559-9A92D4B96B21}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{C10E08DA-AAF1-41C5-B559-9A92D4B96B21}.Release|Any CPU.Build.0 = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(SolutionProperties) = preSolution
+		HideSolutionNode = FALSE
+	EndGlobalSection
+	GlobalSection(NestedProjects) = preSolution
+		{0FA75A5B-AB83-4FD0-B545-279774C01E87} = {AFAA0841-BD93-466F-B8F4-FB4EEC86F1FC}
+		{3870C648-4AEA-4B85-BA3F-F2F63B96136A} = {AFAA0841-BD93-466F-B8F4-FB4EEC86F1FC}
+		{0D4C5D00-C144-4213-A007-4B8944113AB1} = {F10AA149-2626-486E-85BB-9CD5365F3016}
+		{880B3591-E057-46FE-B525-10BD83828B93} = {AFAA0841-BD93-466F-B8F4-FB4EEC86F1FC}
+		{38E184BD-E874-4633-A947-AED4FDB73F40} = {AFAA0841-BD93-466F-B8F4-FB4EEC86F1FC}
+		{22CFE0D2-8DCA-42D7-AD7D-784C3862493F} = {AFAA0841-BD93-466F-B8F4-FB4EEC86F1FC}
+		{E0580562-F8F2-4EBB-B07A-ABFC6F2C314F} = {AFAA0841-BD93-466F-B8F4-FB4EEC86F1FC}
+		{3DA0CA28-56BE-4E2B-907D-4168DC2B0E71} = {AFAA0841-BD93-466F-B8F4-FB4EEC86F1FC}
+		{A40583CE-C106-4EDF-B339-F4C083F5B919} = {3DA0CA28-56BE-4E2B-907D-4168DC2B0E71}
+		{B076677D-6EDC-42E8-A7E5-0A1AC1B841CE} = {3DA0CA28-56BE-4E2B-907D-4168DC2B0E71}
+		{D23FC75B-F14C-4CB1-B27A-8EA67B608228} = {3DA0CA28-56BE-4E2B-907D-4168DC2B0E71}
+		{55E03917-CB98-416E-9D5B-041636D2FF97} = {AFAA0841-BD93-466F-B8F4-FB4EEC86F1FC}
+		{FDD31808-39E1-4067-87F5-C137F5755286} = {55E03917-CB98-416E-9D5B-041636D2FF97}
+		{7C6BC894-7A5F-42DE-BD41-8640DF0E7117} = {55E03917-CB98-416E-9D5B-041636D2FF97}
+		{8AA0CF7A-0652-495B-B9C7-15E96F6DD55D} = {AFAA0841-BD93-466F-B8F4-FB4EEC86F1FC}
+		{68122189-1BDA-4D23-BE1A-DDC43553191F} = {8AA0CF7A-0652-495B-B9C7-15E96F6DD55D}
+		{0A102450-9744-4993-9DE9-3418662F0FB0} = {8AA0CF7A-0652-495B-B9C7-15E96F6DD55D}
+		{B46A73E8-C7F7-4A9E-B2B3-6CF5DC06D23B} = {AFAA0841-BD93-466F-B8F4-FB4EEC86F1FC}
+		{C10E08DA-AAF1-41C5-B559-9A92D4B96B21} = {8AA0CF7A-0652-495B-B9C7-15E96F6DD55D}
+	EndGlobalSection
+	GlobalSection(ExtensibilityGlobals) = postSolution
+		SolutionGuid = {AB2316B1-1074-464A-A19B-79A8A521DA04}
+	EndGlobalSection
+EndGlobal

+ 2 - 0
YGNT.Exam.sln.DotSettings

@@ -0,0 +1,2 @@
+<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
+	<s:Boolean x:Key="/Default/UserDictionary/Words/=Consts/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

+ 37 - 0
build/build-mvc.ps1

@@ -0,0 +1,37 @@
+# COMMON PATHS
+
+$buildFolder = (Get-Item -Path "./" -Verbose).FullName
+$slnFolder = Join-Path $buildFolder "../"
+$outputFolder = Join-Path $buildFolder "outputs"
+$webMvcFolder = Join-Path $slnFolder "src/YGNT.Exam.Web.Mvc"
+
+## CLEAR ######################################################################
+
+Remove-Item $outputFolder -Force -Recurse -ErrorAction Ignore
+New-Item -Path $outputFolder -ItemType Directory
+
+## RESTORE NUGET PACKAGES #####################################################
+
+Set-Location $slnFolder
+dotnet restore
+
+## PUBLISH WEB MVC PROJECT ###################################################
+
+Set-Location $webMvcFolder
+dotnet publish --output (Join-Path $outputFolder "Mvc")
+
+## CREATE DOCKER IMAGES #######################################################
+
+# Mvc
+Set-Location (Join-Path $outputFolder "Mvc")
+
+docker rmi abp/mvc -f
+docker build -t abp/mvc .
+
+## DOCKER COMPOSE FILES #######################################################
+
+Copy-Item (Join-Path $slnFolder "docker/mvc/*.*") $outputFolder
+
+## FINALIZE ###################################################################
+
+Set-Location $outputFolder

+ 12 - 0
docker/mvc/docker-compose.yml

@@ -0,0 +1,12 @@
+version: '2'
+
+services:
+
+    abp_mvc:
+        image: abp/mvc
+        environment:
+            - ASPNETCORE_ENVIRONMENT=Staging
+        ports:
+            - "9903:80"
+        volumes:
+            - "./Mvc-Logs:/app/App_Data/Logs"

+ 1 - 0
docker/mvc/down.ps1

@@ -0,0 +1 @@
+docker-compose down -v --rmi local

+ 1 - 0
docker/mvc/up.ps1

@@ -0,0 +1 @@
+docker-compose up -d

+ 38 - 0
src/QuestionSwap/OfficeAppHelp/Help/DllImportHelp.cs

@@ -0,0 +1,38 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace OfficeAppHelp.Help
+{
+    public static class DllImportHelp
+    {
+        /// <summary>
+        /// net core 没有GetActiveObject()方法 所以调用dll
+        /// </summary>
+        /// <param name="progId"></param>
+        /// <returns></returns>
+        public static object GetActiveObject(string progId)
+        {
+            if (progId == null)
+                return null;
+
+            var hr = CLSIDFromProgIDEx(progId, out var clsid);
+            if (hr < 0)
+                return null;
+
+            hr = GetActiveObject(clsid, IntPtr.Zero, out var obj);
+            if (hr < 0)
+                return null;
+            return obj;
+        }
+
+        [DllImport("ole32")]
+        private static extern int CLSIDFromProgIDEx([MarshalAs(UnmanagedType.LPWStr)] string lpszProgID, out Guid lpclsid);
+
+        [DllImport("oleaut32")]
+        private static extern int GetActiveObject([MarshalAs(UnmanagedType.LPStruct)] Guid rclsid, IntPtr pvReserved, [MarshalAs(UnmanagedType.IUnknown)] out object ppunk);
+    }
+}

+ 33 - 0
src/QuestionSwap/OfficeAppHelp/Help/FileHelp.cs

@@ -0,0 +1,33 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Security.Cryptography;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace OfficeAppHelp.Help
+{
+    public static class FileHelp
+    {
+        /// <summary>
+        /// 获取文件MD5值
+        /// </summary>
+        /// <param name="fileName">文件绝对路径</param>
+        /// <returns>MD5值</returns>
+        public static string GetFileMD5(string fileName)
+        {
+            FileStream file = new FileStream(fileName, FileMode.Open);
+            MD5 md5 = new MD5CryptoServiceProvider();
+            byte[] retVal = md5.ComputeHash(file);
+            file.Close();
+
+            StringBuilder sb = new StringBuilder();
+            for (int i = 0; i < retVal.Length; i++)
+            {
+                sb.Append(retVal[i].ToString("x2"));
+            }
+            return sb.ToString();
+        }
+    }
+}

+ 130 - 0
src/QuestionSwap/OfficeAppHelp/Help/IntHelp.cs

@@ -0,0 +1,130 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace OfficeAppHelp.Help
+{
+    public static class IntHelp
+    {
+        /// <summary>
+        /// 转中文形式(如:一百零一)
+        /// </summary>
+        /// <param name="number">要转换的数字</param>
+        /// <returns>返回数字对应的中文。</returns>
+        public static string ToCNString(this int number)
+        {
+            int absNumber = Math.Abs(number);
+
+            string result = string.Empty;
+
+            string cnString = "零一二三四五六七八九";
+            string unitString = " 十百千";
+            string bigUnitString = " 万亿兆吉";
+            string absNumberSrting = absNumber.ToString();
+            // 分割数量
+            int partitionCount = 4;
+            // 开始分割索引
+            int sIndex = 0;
+            // 总分割次数
+            int count = Convert.ToInt32(Math.Ceiling(absNumberSrting.Length * 1.0d / partitionCount));
+            for (int i = 0; i < count; i++)
+            {
+                // 获取要转换的短数字字符串
+                int tIndex = (count - i) * partitionCount;
+                int subLength = partitionCount;
+                if (tIndex > absNumberSrting.Length && i == 0)// 第一次进入,查询最后一页的数据条数
+                {
+                    subLength = partitionCount - (tIndex - absNumberSrting.Length);
+                }
+                string tempNumberString = Convert.ToInt32(absNumberSrting.Substring(sIndex, subLength)).ToString();
+                sIndex += subLength;
+
+                for (int j = 0; j < tempNumberString.Length; j++)
+                {
+                    if (i > 0 && i < count - 1)// 不是最后一页
+                    {
+                        if (j == 0 && tempNumberString.Length < partitionCount)
+                        {
+                            result += cnString[0];
+                        }
+                    }
+                    else if (i == count - 1 && count > 1)
+                    {
+                        if (j == 0 && tempNumberString.Length < partitionCount)
+                        {
+                            if (result.Length > 0 && result[result.Length - 1].Equals(cnString[0]) == false)
+                                result += cnString[0];
+                        }
+                    }
+                    if (Convert.ToInt32(tempNumberString) == 0 && (result.Length <= 0 || result[result.Length - 1].Equals(cnString[0]) == false))
+                    {
+                        result += cnString[0];
+                        break;
+                    }
+                    int unitIdx = -1;
+                    // 不到最后一位数,计算小单位索引
+                    if (tempNumberString.Length - (j + 1) != 0)// 小单位
+                    {
+                        int tempIdx = j;
+                        if (tempNumberString.Length != partitionCount)
+                        {
+                            tempIdx += partitionCount - tempNumberString.Length;
+                        }
+                        unitIdx = (unitString.Length - 1) - tempIdx;
+                    }
+                    // 数字转中文
+                    int cnIdx = Convert.ToInt32(tempNumberString[j].ToString());
+                    result += $"{cnString[cnIdx]}{(unitIdx != -1 ? unitString[unitIdx].ToString() : string.Empty)}";
+                    // 看看后面的字符串是否全是0,全是0这一页加个单位转换就结束了,不是的话,就加个零,然后把剩余的数字拿来重新骚整一下。
+                    string tempString = tempNumberString.Substring(j + 1);
+                    if (string.IsNullOrEmpty(tempString) == false)
+                    {
+                        int temp = Convert.ToInt32(tempString);
+                        if (temp > 0)
+                        {
+                            if (tempString.Equals(temp.ToString()) == false)
+                            {
+                                result += cnString[0];
+                                tempNumberString = temp.ToString();
+                                j = -1;
+                            }
+                        }
+                        else
+                        {
+                            // 加个单位退出循环
+                            break;
+                        }
+                    }
+                }
+                if (i != count - 1)// 大单位
+                {
+                    result += bigUnitString[count - (i + 1)];
+                }
+                // 如果后面全是0,整个转换就完成了
+                string surplusString = absNumberSrting.Substring(subLength);
+                if (string.IsNullOrEmpty(surplusString) == false)
+                {
+                    if (surplusString.Length > 7 && Convert.ToInt64(surplusString) == 0)
+                    {
+                        break;
+                    }
+                    else if (surplusString.Length <= 7 && Convert.ToInt32(surplusString) == 0)
+                    {
+                        break;
+                    }
+                }
+            }
+            if (number < 0)
+            {
+                result = $"负{result}";
+            }
+            if (number >= 10 && number < 20)
+            {
+                return result.Substring(1);
+            }
+            return result;
+        }
+    }
+}

+ 26 - 0
src/QuestionSwap/OfficeAppHelp/Help/PathHelp.cs

@@ -0,0 +1,26 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace OfficeAppHelp.Help
+{
+    public static class PathHelp
+    {
+        /// <summary>
+        /// 更改文件名不包含扩展名
+        /// </summary>
+        /// <param name="path"></param>
+        /// <returns></returns>
+        public static string ChangeFileNameWithoutExtension(string path, string fileName)
+        {
+            var n1 = Path.GetExtension(path);
+            var n2 = Path.GetDirectoryName(path);
+            var n3 = Path.GetFileNameWithoutExtension(fileName);
+            var n4 = Path.Combine(n2, n3 + n1);
+            return n4;
+        }
+    }
+}

+ 179 - 0
src/QuestionSwap/OfficeAppHelp/Help/StringHelp.cs

@@ -0,0 +1,179 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+
+namespace OfficeAppHelp.Help
+{
+    public static class StringHelp
+    {
+
+        /// <summary>
+        /// 得到编号 如 "1."或"  2. ";
+        /// </summary>
+        /// <param name="txt"></param>
+        /// <param name="isTrim">是否移除前后空格 返回如:"1."或"2."</param>
+        /// <returns></returns>
+        public static string GetSerialNumber(this string txt, bool isTrim = true)
+        {
+            if (string.IsNullOrWhiteSpace(txt))
+                return string.Empty;
+
+            char[] empty = new char[] { ' ', '\t' };
+            string regexSerialNumber = @"^[\d]+[\.\,\:\-\.\,\:\-\、]";
+
+            string newTxt = txt.TrimStart(empty);
+
+            ////^[\d]+\.|^[a-zA-Z]+\.
+            //StringBuilder splitReguler = new StringBuilder();
+            ////需要匹配的序列 符号
+            //string mChar = string.Join("\\",
+            //    new string[]
+            //    {
+            //        ".", ",", ":", "-",//半角
+            //        ".",",",":","-",//全角
+            //        "、",//中文
+            //    });
+
+
+            ////匹配数字序号 1. 10.
+            //splitReguler.AppendFormat(@"^[\d]+[\{0}]", mChar);
+            ////匹配字母序号 a. A.
+            ////splitReguler.AppendFormat(@"|^[a-zA-Z][\{0}]", mChar);
+
+            //string splitChar = Regex.Match(newTxt, splitReguler.ToString()).Value;
+            string splitChar = Regex.Match(newTxt, regexSerialNumber).Value;
+            if (string.IsNullOrEmpty(splitChar))
+                return string.Empty;
+
+            if (isTrim)
+            {
+                return splitChar;
+            }
+            else
+            {
+                //取 开头的空格
+                string te = txt.Substring(0, txt.Length - newTxt.Length);
+                //取 去掉编号后的空格
+                string newTxt2 = newTxt.Substring(splitChar.Length);
+                string newTxt3 = newTxt2.TrimStart(empty);
+                string te2 = newTxt2.Substring(0, newTxt2.Length - newTxt3.Length);
+
+                return te + splitChar + te2;
+            }
+
+        }
+
+        /// <summary>
+        /// 移除符号(只包含:字母,数字,中文)
+        /// </summary>
+        /// <param name="txt"></param>
+        /// <returns></returns>
+        public static string RemoveSymbol(this string txt)
+        {
+            return string.Join(null, txt.RegexMatche("[A-Za-z0-9\u4e00-\u9fa5-]+"));
+        }
+
+        /// <summary>
+        /// 在指定的输入字符串中搜索指定的正则表达式的所有匹配项
+        /// </summary>
+        /// <param name="txt"></param>
+        /// <param name="pattern">正则表达式</param>
+        /// <returns></returns>
+        public static List<string> RegexMatche(this string txt, string pattern)
+        {
+            List<string> strRet = new List<string>();
+            MatchCollection results = Regex.Matches(txt, pattern);
+            foreach (var v in results)
+            {
+                strRet.Add(v.ToString());
+            }
+            return strRet;
+        }
+
+        /// <summary>
+        /// 移除转义字符
+        /// </summary>
+        /// <param name="sourceString">待处理字符串</param>
+        /// <returns>清空转义字符后的字符串</returns>
+        public static string RemoveESC(this string txt)
+        {
+            string strRet = "";
+            for (int i = 0; i < txt.Length; i++)
+            {
+                int Unicode = txt[i];
+                if (Unicode > 31 && Unicode != 127)
+                {
+                    strRet += txt[i].ToString();
+                }
+            }
+            return strRet;
+        }
+
+        /// <summary>
+        /// 得到所有的大写字母
+        /// </summary>
+        /// <param name="txt"></param>
+        /// <returns></returns>
+        public static List<string> GetUpperLetter(this string txt)
+        {
+            return txt.RegexMatche("[A-Z]");
+        }
+
+        /// <summary>
+        /// 得到所有的大写字母
+        /// </summary>
+        /// <param name="txt"></param>
+        /// <param name="separator">分隔符</param>
+        /// <returns></returns>
+        public static string GetUpperLetter(this string txt, string separator)
+        {
+            return string.Join(separator, txt.RegexMatche("[A-Z]"));
+        }
+
+        /// <summary>
+        /// 字符串转为Base64字符串
+        /// </summary>
+        /// <param name="str"></param>
+        /// <returns></returns>
+        public static string ToBase64(this string str)
+        {
+            byte[] b = Encoding.UTF8.GetBytes(str);
+            return Convert.ToBase64String(b, Base64FormattingOptions.None);
+        }
+
+        /// <summary>
+        /// Base64字符串转为字符串
+        /// </summary>
+        /// <param name="base64str">Base64字符串</param>
+        /// <returns></returns>
+        public static string Base64ToString(this string base64str)
+        {
+            byte[] c = Convert.FromBase64String(base64str);
+            return Encoding.UTF8.GetString(c);
+        }
+
+        /// <summary>
+        /// Base64字符串转为字符串
+        /// </summary>
+        /// <param name="base64str"></param>
+        /// <param name="isOk">是否成功</param>
+        /// <returns>成功返回结果,不成功返回Empty</returns>
+        public static string Base64ToStringTry(this string base64str, out bool isOk)
+        {
+            try
+            {
+                isOk = true;
+                return Base64ToString(base64str);
+            }
+            catch
+            {
+                isOk = false;
+                return string.Empty;
+            }
+        }
+
+    }
+}

+ 65 - 0
src/QuestionSwap/OfficeAppHelp/OfficeApp.cs

@@ -0,0 +1,65 @@
+using OfficeAppHelp.Help;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace OfficeAppHelp
+{
+    /// <summary>
+    /// office应用
+    /// </summary>
+    public class OfficeApp
+    {
+        /// <summary>
+        /// WPS 2019+窗体模式 是否为整合模式
+        /// </summary>
+        private bool? IsWpsFormMergeModel
+        {
+            get
+            {
+                return null;
+            }
+        }
+
+        /// <summary>
+        /// 得到进程中的word应用
+        /// </summary>
+        /// <param name="appType">0没有找到 1 office 2 wps</param>
+        /// <returns>word应用</returns>
+        public static object GetProcessesWordApp(out OfficeType officeType)
+        {
+            officeType = OfficeType.Null;
+
+            if (Process.GetProcessesByName("wps").Count() > 0)
+            {
+                //WPS Office V9
+                if (Type.GetTypeFromProgID("KWPS.Application") != null)
+                {
+                    officeType = OfficeType.WPS;
+                    return DllImportHelp.GetActiveObject("KWPS.Application");
+                }
+                //WPS Office V8
+                else if (Type.GetTypeFromProgID("WPS.Application") != null)
+                {
+                    officeType = OfficeType.WPS;
+                    return DllImportHelp.GetActiveObject("WPS.Application");
+                }
+                else
+                    return null;
+            }
+            //MS Office
+            else if (Process.GetProcessesByName("WINWORD").Count() > 0)
+            {
+                officeType = OfficeType.Microsoft;
+                return DllImportHelp.GetActiveObject("Word.Application");
+            }
+            else
+            {
+                return null;
+            }
+        }
+    }
+}

+ 26 - 0
src/QuestionSwap/OfficeAppHelp/OfficeAppHelp.csproj

@@ -0,0 +1,26 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+	  <TargetFrameworks>net45;netstandard20</TargetFrameworks>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
+  </ItemGroup>
+
+  <ItemGroup Condition="'$(TargetFramework)' == 'netstandard20'">
+    <PackageReference Include="System.Text.Encoding.CodePages">
+      <Version>5.0.0</Version>
+    </PackageReference>
+  </ItemGroup>
+
+  <ItemGroup>
+    <Reference Include="Interop.Microsoft.Office.Core">
+      <HintPath>..\..\dll\Interop.Microsoft.Office.Core.dll</HintPath>
+    </Reference>
+    <Reference Include="Interop.Microsoft.Office.Interop.Word">
+      <HintPath>..\..\dll\Interop.Microsoft.Office.Interop.Word.dll</HintPath>
+    </Reference>
+  </ItemGroup>
+
+</Project>

+ 31 - 0
src/QuestionSwap/OfficeAppHelp/OfficeType.cs

@@ -0,0 +1,31 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace OfficeAppHelp
+{
+    /// <summary>
+    /// office程序类型
+    /// </summary>
+    public enum OfficeType
+    {
+        /// <summary>
+        /// 为空的
+        /// </summary>
+        Null,
+        /// <summary>
+        /// WPS Office
+        /// </summary>
+        WPS,
+        /// <summary>
+        /// Microsoft Office
+        /// </summary>
+        Microsoft,
+        /// <summary>
+        /// 未知的程序
+        /// </summary>
+        Unknown,
+    }
+}

+ 26 - 0
src/QuestionSwap/OfficeAppHelp/Word/ColorEx.cs

@@ -0,0 +1,26 @@
+using Microsoft.Office.Interop.Word;
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace OfficeAppHelp.Word
+{
+    /// <summary>
+    /// 颜色扩展
+    /// </summary>
+    public static class ColorEx
+    {
+        /// <summary>
+        /// 转为Word颜色
+        /// </summary>
+        /// <param name="c">颜色</param>
+        /// <returns>WdColor类型的颜色</returns>
+        public static WdColor ToWordColor(this Color c)
+        {
+            return (WdColor)(c.R + 0x100 * c.G + 0x10000 * c.B);
+        }
+    }
+}

+ 117 - 0
src/QuestionSwap/OfficeAppHelp/Word/DocEx.cs

@@ -0,0 +1,117 @@
+using Microsoft.Office.Interop.Word;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace OfficeAppHelp.Word
+{
+    /// <summary>
+    /// 文档扩展
+    /// </summary>
+    public static class DocEx
+    {
+        /// <summary>
+        /// 根据页码获取页内容
+        /// </summary>
+        /// <param name="pageNumber">页码(非调整后的页码)</param>
+        /// <returns>页内容</returns>
+        public static Range GetPageRange(this Document doc, int pageNumber)
+        {
+            int numberOfPages = (int)doc.Content.Information[WdInformation.wdNumberOfPagesInDocument];
+
+            //超出页码范围
+            if (pageNumber > numberOfPages)
+                return null;
+
+            int Start, End;
+
+            Range rangeStart = doc.GoTo(WdGoToItem.wdGoToPage, WdGoToDirection.wdGoToAbsolute, pageNumber);
+            Start = rangeStart.Start;
+
+            //只有一页
+            int countPlusOne = pageNumber + 1;
+            if (countPlusOne > numberOfPages)
+                End = doc.Range().End;
+            else
+            {
+                Range rangeEnd = doc.GoTo(WdGoToItem.wdGoToPage, WdGoToDirection.wdGoToAbsolute, countPlusOne);
+                End = rangeEnd.End;
+            }
+
+            return doc.Range(Start, End);
+        }
+
+        /// <summary>
+        /// 获取页内容
+        /// </summary>
+        /// <param name="pageNumber">页码(非调整后的页码)</param>
+        /// <returns>页内容</returns>
+        public static List<Range> GetPageRanges(this Document doc)
+        {
+            //int numberOfPages = (int)doc.Content.Information[WdInformation.wdNumberOfPagesInDocument];
+            int numberOfPages = (int)doc.Content.get_Information(WdInformation.wdActiveEndPageNumber);
+
+            List<Range> rs = new List<Range>();
+            for (int pageNumber = 1; pageNumber <= numberOfPages; pageNumber++)
+            {
+                int Start, End;
+
+                Range rangeStart = doc.GoTo(WdGoToItem.wdGoToPage, WdGoToDirection.wdGoToAbsolute, pageNumber);
+                Start = rangeStart.Start;
+
+                //只有一页
+                int countPlusOne = pageNumber + 1;
+                if (countPlusOne > numberOfPages)
+                    End = doc.Range().End;
+                else
+                {
+                    Range rangeEnd = doc.GoTo(WdGoToItem.wdGoToPage, WdGoToDirection.wdGoToAbsolute, countPlusOne);
+                    End = rangeEnd.End;
+                }
+
+                rs.Add(doc.Range(Start, End));
+            }
+            return rs;
+        }
+
+        /// <summary>
+        /// 得到文档的末尾
+        /// </summary>
+        /// <param name="doc"></param>
+        /// <returns></returns>
+        public static Range RangeEnd(this Document doc)
+        {
+            int end = doc.Content.End;
+            return doc.Range(end - 1, end);
+        }
+
+        /// <summary>
+        /// 文档末尾的空段落数量
+        /// </summary>
+        /// <param name="doc"></param>
+        /// <param name="count">末尾的空段落数量</param>
+        public static void ParagraphEndCount(this Document doc, int count)
+        {
+            if (count < 0)
+                return;
+
+            while (true)
+            {
+                var pRange = doc.Paragraphs.Last.Range;
+                if (pRange.IsNullOrSpace())
+                    pRange.Delete();
+                else
+                    break;
+            }
+
+            if (count == 0)
+                return;
+
+            string han = new string('\n', count);
+            doc.Content.InsertAfter(han);
+        }
+
+    }
+}

+ 256 - 0
src/QuestionSwap/OfficeAppHelp/Word/RangeEx.cs

@@ -0,0 +1,256 @@
+using Microsoft.Office.Core;
+using Microsoft.Office.Interop.Word;
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Shape = Microsoft.Office.Interop.Word.Shape;
+
+namespace OfficeAppHelp.Word
+{
+    /// <summary>
+    /// 文档范围扩展
+    /// </summary>
+    public static class RangeEx
+    {
+        /// <summary>
+        /// 统一设置颜色(文字,下划线,音调符号,表格边框)
+        /// </summary>
+        public static void SetColor(this Range range, WdColor wdColor)
+        {
+            range.Font.Color = wdColor;
+            range.Font.DiacriticColor = wdColor;
+            range.Font.UnderlineColor = wdColor;
+            foreach (Table table in range.Tables)
+            {
+                table.Borders.InsideColor = wdColor;
+                table.Borders.OutsideColor = wdColor;
+            }
+        }
+
+        /// <summary>
+        /// 是否无内容
+        /// </summary>
+        public static bool IsNull(this Range range)
+        {
+            //对公式等检测
+            if (range.Text.Length == range.End - range.Start)
+            {
+                //对图片等检测
+                string txt = range.Text.Replace("\r", "").Replace("\n", "").Replace("\v", "");
+                if (string.IsNullOrEmpty(txt))
+                    return true;
+            }
+            return false;
+        }
+
+        /// <summary>
+        /// 是否无内容或者空格组成
+        /// </summary>
+        public static bool IsNullOrSpace(this Range range)
+        {
+            //对公式等检测
+            if (range.Text.Length == range.End - range.Start)
+            {
+                //对图片等检测
+                string txt = range.Text.Trim();
+                if (string.IsNullOrWhiteSpace(txt))
+                    return true;
+            }
+            return false;
+        }
+
+        /// <summary>
+        /// 查找所有
+        /// </summary>
+        /// <param name="range"></param>
+        /// <param name="findText">查找内容</param>
+        /// <param name="isWildcard">是否使用通配符</param>
+        public static List<Range> FindAll(this Range range, string findText, bool isWildcard = false)
+        {
+            int start = range.Start;
+            int end = range.End;
+
+            List<Range> ranges = new List<Range>();
+
+            range.Find.Execute(FindText: findText, MatchCase: true, MatchWildcards: isWildcard);
+            while (range.Find.Found)
+            {
+                //搜索会改变range,这里做了一个超出范围的判断
+                if (range.Start > end)
+                    break;
+
+                ranges.Add(range.Document.Range(range.Start, range.End));
+                range.Find.Execute(FindText: findText, MatchCase: true, MatchWildcards: isWildcard);
+            }
+
+            //对原来的range还原
+            range.SetRange(start, end);
+            return ranges;
+        }
+
+        /// <summary>
+        /// 查找第一个
+        /// </summary>
+        /// <param name="range"></param>
+        /// <param name="findText">查找内容</param>
+        /// <param name="isWildcard">是否使用通配符</param>
+        /// <returns>没有找到为空</returns>
+        public static Range FindFirst(this Range range, string findText, bool isWildcard = false)
+        {
+            int start = range.Start;
+            int end = range.End;
+
+            bool isOk = range.Find.Execute(FindText: findText, MatchCase: true, MatchWildcards: isWildcard);
+            if (isOk)
+            {
+                var newRange = range.Document.Range(range.Start, range.End);
+                range.SetRange(start, end);
+                return newRange;
+            }
+            else
+                return null;
+        }
+
+        /// <summary>
+        /// 替换
+        /// </summary>
+        /// <param name="range"></param>
+        /// <param name="findText">查找内容</param>
+        /// <param name="replacementText">替换内容</param>
+        public static bool Replace(this Range range, string findText, string newValue)
+        {
+            return range.Find.Execute
+                (
+                FindText: findText,
+                MatchCase: true,
+                MatchWildcards: true,
+                Wrap: WdFindWrap.wdFindStop,
+                ReplaceWith: newValue,
+                Replace: WdReplace.wdReplaceAll
+                );
+        }
+
+        /// <summary>
+        /// 得到非嵌入型的形状
+        /// </summary>
+        /// <param name="range"></param>
+        public static List<Shape> GetNotWrapInlineShape(this Range range)
+        {
+            if (range == null)
+                return null;
+
+            List<Shape> shapes = new List<Shape>();
+            foreach (Shape shape in range.ShapeRange)
+            {
+                if (shape.WrapFormat.Type != WdWrapType.wdWrapInline)
+                {
+                    shapes.Add(shape);
+                }
+            }
+            return shapes;
+        }
+
+        /// <summary>
+        /// 转为图片流(执行“Image.FromStream(Stream)”可为图片)
+        /// </summary>
+        /// <param name="range"></param>
+        public static Stream ToImageStream(this Range range)
+        {
+            if (range == null)
+                return null;
+
+            var bits = (byte[])range.EnhMetaFileBits;
+            return new MemoryStream(bits);
+        }
+
+        /// <summary>
+        /// 是否在表格中
+        /// </summary>
+        /// <param name="range"></param>
+        /// <returns></returns>
+        public static bool IsInTable(this Range range)
+        {
+            if (range == null)
+                return false;
+
+            foreach (Table table in range.Tables)
+                if (range.InRange(table.Range))
+                    return true;
+
+            return false;
+        }
+
+        /// <summary>
+        /// 得到页码
+        /// </summary>
+        /// <param name="range"></param>
+        /// <param name="isAdjusted">是否返回调整后的页码(插入-页码-页码...-起始页码)</param>
+        /// <returns></returns>
+        public static List<int> GetPageNumber(this Range range, bool isAdjusted = true)
+        {
+            List<int> ints = new List<int>();
+            if (range == null)
+                return ints;
+
+            int go = 0;
+            int to = 0;
+            Range startRange = range.Document.Range(range.Start, range.Start);
+            if (isAdjusted)
+            {
+                go = (int)startRange.get_Information(WdInformation.wdActiveEndAdjustedPageNumber);
+                to = (int)range.get_Information(WdInformation.wdActiveEndAdjustedPageNumber);
+            }
+            else
+            {
+                go = (int)startRange.get_Information(WdInformation.wdActiveEndPageNumber);
+                to = (int)range.get_Information(WdInformation.wdActiveEndPageNumber);
+            }
+
+            for (; go <= to; go++)
+                ints.Add(go);
+            return ints;
+        }
+
+        /// <summary>
+        /// 得到页码(最开始的一个)
+        /// </summary>
+        /// <param name="range"></param>
+        /// <param name="isAdjusted">是否返回调整后的页码(插入-页码-页码...-起始页码)</param>
+        /// <returns>range为空返回-1</returns>
+        public static int GetPageNumberStart(this Range range, bool isAdjusted = true)
+        {
+            if (range == null)
+                return -1;
+
+            Range startRange = range.Document.Range(range.Start, range.Start);
+            if (isAdjusted)
+                return (int)startRange.get_Information(WdInformation.wdActiveEndAdjustedPageNumber);
+            else
+                return (int)startRange.get_Information(WdInformation.wdActiveEndPageNumber);
+
+        }
+
+        /// <summary>
+        /// 得到页码(最后的一个)
+        /// </summary>
+        /// <param name="range"></param>
+        /// <param name="isAdjusted">是否返回调整后的页码(插入-页码-页码...-起始页码)</param>
+        /// <returns>range为空返回-1</returns>
+        public static int GetPageNumberEnd(this Range range, bool isAdjusted = true)
+        {
+            if (range == null)
+                return -1;
+
+            if (isAdjusted)
+                return (int)range.get_Information(WdInformation.wdActiveEndAdjustedPageNumber);
+            else
+                return (int)range.get_Information(WdInformation.wdActiveEndPageNumber);
+
+        }
+
+    }
+}

+ 70 - 0
src/QuestionSwap/OfficeAppHelp/Word/TableEx.cs

@@ -0,0 +1,70 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.Office.Interop.Word;
+
+namespace OfficeAppHelp.Word
+{
+    /// <summary>
+    /// 表格扩展
+    /// </summary>
+    public static class TableEx
+    {
+        /// <summary>
+        /// 得到表格的宽度
+        /// </summary>
+        /// <param name="table">表格,是标准长方形没有进行合并单元格的表格</param>
+        /// <returns></returns>
+        public static float GetWidth(this Table table)
+        {
+            float f = 0f;
+            for (int i = 1; i <= table.Columns.Count; i++)
+            {
+                f += table.Columns[i].Width;
+            }
+            return f;
+        }
+
+        /// <summary>
+        /// 得到表格的宽度
+        /// </summary>
+        /// <param name="table">表格,是标准长方形没有进行合并单元格的表格</param>
+        /// <returns></returns>
+        public static float GetHeight(this Table table)
+        {
+            float f = 0f;
+            for (int i = 1; i <= table.Rows.Count; i++)
+            {
+                f += table.Rows[i].Height;
+            }
+            return f;
+        }
+
+        /// <summary>
+        /// 依次将文本添加到单元格
+        /// </summary>
+        /// <param name="table">表格</param>
+        /// <param name="texts">文本集合</param>
+        public static void AddText(this Table table, params string[] texts)
+        {
+            if (table == null || texts == null)
+                return;
+
+            int i = 0;
+            foreach (Row row in table.Rows)
+            {
+                foreach (Cell cell in row.Cells)
+                {
+                    cell.Range.Text = texts[i];
+                    i++;
+
+                    if (i >= texts.Length)
+                        return;
+                }
+            }
+        }
+
+    }
+}

+ 343 - 0
src/QuestionSwap/OfficeAppHelp/Word/WordApp.cs

@@ -0,0 +1,343 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Xml.Linq;
+using Microsoft.Office.Interop.Word;
+//using Word = Microsoft.Office.Interop.Word;
+//using Office = Microsoft.Office.Core;
+//using Microsoft.Office.Tools.Word;
+//using Microsoft.Office.Tools;
+//using Microsoft.Office.Core;
+using System.IO;
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+using Range = Microsoft.Office.Interop.Word.Range;
+using Microsoft.Office.Core;
+//using Microsoft.Vbe.Interop;
+
+namespace OfficeAppHelp.Word
+{
+    /// <summary>
+    /// Word应用
+    /// </summary>
+    public class WordApp
+    {
+        /// <summary>
+        /// 查找符:软回车,同html中的br
+        /// </summary>
+        public const string Find_BlankRow = "^l";
+        /// <summary>
+        /// 查找符:制表符,相当于按了一下TAB键
+        /// </summary>
+        public const string Find_Tab = "^t";
+        /// <summary>
+        /// 查找符:段落
+        /// </summary>
+        public const string Find_Paragraph = "^p";
+        /// <summary>
+        /// 查找符:段落 通配符
+        /// </summary>
+        public const string Find_ParagraphASCII = "^13";
+
+        private OfficeType _AppType = OfficeType.Null;
+        /// <summary>
+        /// Office类型
+        /// </summary>
+        public OfficeType AppType
+        {
+            get
+            {
+                if (_AppType != OfficeType.Null)
+                    return _AppType;
+
+                if (File.Exists(App.Path + "/wps.exe") || File.Exists(App.Path + "wpsoffice.exe"))
+                    _AppType = OfficeType.WPS;
+                else if (File.Exists(App.Path + "/MSOHTMED.EXE") || File.Exists(App.Path + "MSQRY32.EXE"))
+                    _AppType = OfficeType.Microsoft;
+                else
+                    _AppType = OfficeType.Unknown;
+
+                return _AppType;
+            }
+            private set { _AppType = value; }
+        }
+
+        /// <summary>
+        /// 整个应用程序
+        /// </summary>
+        public Application App { get; private set; }
+
+        /// <summary>
+        /// 活动中(正在操作)的Word文档
+        /// </summary>
+        public Document Doc { get; set; }
+
+        /// <summary>
+        /// 是否存在文件路径
+        /// </summary>
+        public bool IsExistFilePath { get => Doc.FullName.Contains(':'); }
+
+        ///// <summary>
+        ///// 初始化程序和活动中的word (vsto)
+        ///// </summary>
+        //public WordApp()
+        //{
+        //    App = Globals.ThisAddIn.Application;
+        //    try
+        //    {
+        //        Doc = App.ActiveDocument;
+        //    }
+        //    catch { }
+        //}
+
+        /// <summary>
+        /// 初始化word程序
+        /// </summary>
+        public WordApp()
+        {
+            var wordObj = OfficeApp.GetProcessesWordApp(out OfficeType officeType);
+            if (wordObj == null)
+                App = new Application();
+            else
+            {
+                App = (Application)wordObj;
+                AppType = officeType;
+            }
+
+            Doc = null;
+        }
+
+        /// <summary>
+        /// 初始化word程序
+        /// </summary>
+        /// <param name="fileName">文档路径</param>
+        /// <param name="isReadOnly">是否只读模式打开</param>
+        /// <param name="password">打开所需要的密码</param>
+        public WordApp(string fileName, bool isReadOnly = false, string password = "")
+        {
+            var wordObj = OfficeApp.GetProcessesWordApp(out OfficeType officeType);
+            if (wordObj == null)
+                App = new Application();
+            else
+            {
+                App = (Application)wordObj;
+                AppType = officeType;
+            }
+
+            //Doc = App.Documents.Open(fileName, PasswordDocument: "1");
+            Doc = App.Documents.OpenNoRepairDialog(fileName, ConfirmConversions: false, ReadOnly: isReadOnly, PasswordDocument: password);
+        }
+
+        /// <summary>
+        /// 初始化程序,需要有活动中的word
+        /// </summary>
+        public WordApp(Application app)
+        {
+            App = app;
+
+            try
+            {
+                Doc = app.ActiveDocument;
+            }
+            catch { }
+        }
+
+        /// <summary>
+        /// 初始化word程序
+        /// </summary>
+        public WordApp(Document doc)
+        {
+            if (doc != null)
+            {
+                App = doc.Application;
+                Doc = doc;
+            }
+        }
+
+        /// <summary>
+        /// 全新的文档
+        /// </summary>
+        /// <param name="paperSize">纸张大小</param>
+        /// <param name="orientation">纸张方向</param>
+        public void NewDoc(WdPaperSize paperSize = WdPaperSize.wdPaperA4, WdOrientation orientation = WdOrientation.wdOrientPortrait)
+        {
+            Close();
+            Doc = App.Documents.Add();
+            if (paperSize != WdPaperSize.wdPaperA4 || orientation != WdOrientation.wdOrientPortrait)
+            {
+                Doc.PageSetup.PaperSize = paperSize;
+                Doc.PageSetup.Orientation = orientation;
+            }
+        }
+
+        /// <summary>
+        /// 得到文档的数量
+        /// </summary>
+        public int GetDocCount()
+        {
+            if (App == null)
+                return 0;
+            return App.Documents.Count;
+        }
+
+        /// <summary>
+        /// 替换
+        /// </summary>
+        /// <param name="findText">查找内容</param>
+        /// <param name="newValue">替换内容</param>
+        public bool Replace(string findText, string newValue)
+        {
+            return RangeEx.Replace(Doc.Content, findText, newValue);
+        }
+
+        public Range InsertHtmlFile(string htmlFile)
+        {
+            //插入富文本控件
+            ContentControl contentControl = Doc.ContentControls.Add(WdContentControlType.wdContentControlRichText);
+            return null;
+        }
+
+        ///// <summary>
+        /////保存为图片
+        ///// </summary>
+        //public void SaveImg()
+        //{
+        //    for (int i = 1; i <= Doc.ActiveWindow.Panes[1].Pages.Count; i++)
+        //    {
+        //        var bits = (byte[])Doc.ActiveWindow.Panes[1].Pages[i].EnhMetaFileBits;
+        //        System.IO.MemoryStream ms = new System.IO.MemoryStream(bits);
+        //        var ret = System.Drawing.Image.FromStream(ms);
+        //        ret.Save(string.Format(@"G:\{0}_{1}.jpg", "我的文档", i));
+        //    }
+        //}
+
+        /// <summary>
+        /// 在文档前面加文本(使文档发生变化,为了解决保存自定义属性的问题)
+        /// </summary>
+        public void AddNullText()
+        {
+            Doc.Range(0, 0).Text = "1";
+            Doc.Range(0, 1).Text = "";
+        }
+
+        public byte[] ReadAllBytes()
+        {
+            //文件上传的解决办法代码
+            var ipersistfile = (System.Runtime.InteropServices.ComTypes.IPersistFile)Doc;
+            string tempfile = Path.Combine("C:/" + new Random().Next(111111, 999999) + ".docx");
+            ipersistfile.Save(tempfile, true);
+            byte[] vs = File.ReadAllBytes(tempfile);
+            File.Delete(tempfile);
+            return vs;
+            //using (var stream = File.Open(tempfile, FileMode.Open, FileAccess.Read, FileShare.Read))
+            //{
+            //    byte[] by = new byte[stream.Length];
+            //    stream.Read(by, 0, Convert.ToInt32(stream.Length));
+            //    string name = stream.Name;
+            //    //do something with the stream
+            //}
+        }
+
+        /// <summary>
+        /// 保存或者另存为
+        /// </summary>
+        /// <param name="fileName">另存为的绝对路径+文件名,为null为保存文档</param>
+        public void Save(string fileName = null)
+        {
+            if (string.IsNullOrEmpty(fileName))
+                Doc.Save();
+            else
+                Doc.SaveAs2(fileName, WdSaveFormat.wdFormatXMLDocument, AddToRecentFiles: false);
+        }
+
+        /// <summary>
+        /// 保存或者另存为当前文档(到默认路径或桌面)(针对于wps的优化)
+        /// </summary>
+        public void Save2(string path = null)
+        {
+            AddNullText();//word模拟修改状态,以用于保存文档和保存自定义属性
+            if (path != null)
+                Doc.SaveAs(path);
+            else
+            {
+                if (IsExistFilePath)
+                    Doc.Save();
+                else
+                {
+                    string dir = string.Format(@"{0}\{1}_{2}.docx",
+                        Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory),
+                        Doc.FullName,
+                        DateTime.Now.ToString("yyyyMMddHHmmss"));
+                    Doc.SaveAs(dir);
+                }
+            }
+        }
+
+        /// <summary>
+        /// 设置自定义属性(不存在将添加)
+        /// </summary>
+        /// <param name="name">名字</param>
+        /// <param name="val">值</param>
+        public void SetProperty(string name, string val = "")
+        {
+            DocumentProperties properties = (DocumentProperties)Doc.CustomDocumentProperties;
+            try//存在
+            {
+                DocumentProperty property = properties[name];
+                properties[name].Value = val;//存在就更新
+            }
+            catch//不存在
+            {
+                properties.Add(name, false, MsoDocProperties.msoPropertyTypeString, val);
+            }
+        }
+
+        /// <summary>
+        /// 获取文档自定义属性
+        /// </summary>
+        public string GetPropertyVal(string name)
+        {
+            DocumentProperties properties = (DocumentProperties)Doc.CustomDocumentProperties;
+            try//存在
+            {
+                DocumentProperty property = properties[name];
+                return (string)properties[name].Value;
+            }
+            catch//不存在
+            {
+                return string.Empty;
+            }
+        }
+
+        /// <summary>
+        /// 获取文档属性
+        /// </summary>
+        public dynamic GetDocProperty(string key)
+        {
+            DocumentProperties attrs = (DocumentProperties)Doc.BuiltInDocumentProperties;
+            try
+            {
+                DocumentProperty attr = attrs[key];
+                return attr.Value == null ? string.Empty : attr.Value;
+            }
+            catch
+            {
+                return string.Empty;
+            }
+        }
+
+        /// <summary>
+        /// 关闭当前文档
+        /// </summary>
+        public void Close()
+        {
+            if (Doc != null)
+                Doc.Close(WdSaveOptions.wdDoNotSaveChanges);
+
+            Doc = null;
+        }
+
+    }
+}

+ 11 - 0
src/QuestionSwap/OfficeOpenXmlHelp/OfficeOpenXmlHelp.csproj

@@ -0,0 +1,11 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+	<PropertyGroup>
+		<TargetFrameworks>net45;netstandard20</TargetFrameworks>
+	</PropertyGroup>
+
+	<ItemGroup>
+		<PackageReference Include="DocumentFormat.OpenXml" Version="2.12.1" />
+		<PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
+	</ItemGroup>
+</Project>

+ 91 - 0
src/QuestionSwap/OfficeOpenXmlHelp/Word/WordOpenXml.cs

@@ -0,0 +1,91 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Text.RegularExpressions;
+
+using System.IO;
+using DocumentFormat.OpenXml;
+using DocumentFormat.OpenXml.Packaging;
+using DocumentFormat.OpenXml.Wordprocessing;
+using A = DocumentFormat.OpenXml.Drawing;
+using DW = DocumentFormat.OpenXml.Drawing.Wordprocessing;
+using PIC = DocumentFormat.OpenXml.Drawing.Pictures;
+using System.Linq;
+using System.Xml;
+using System.Xml.Linq;
+
+namespace OfficeOpenXmlHelp.Word
+{
+    /// <summary>
+    /// word帮助类,完成后请调用Dispose()
+    /// </summary>
+    public class WordOpenXml
+    {
+        /// <summary>
+        /// 获取的 WordprocessingDocument 对象
+        /// </summary>
+        private WordprocessingDocument wordDoc;
+
+        /// <summary>
+        /// 初始化一个word
+        /// </summary>
+        /// <param name="fileStream"></param>
+        public WordOpenXml(Stream fileStream, bool isEditable)
+        {
+            wordDoc = WordprocessingDocument.Open(fileStream, isEditable);
+        }
+
+        /// <summary>
+        ///  搜索和替换文档<w:t></w:t>的内容。
+        ///  快速方便地搜索和替换。 因为它检索的字符串格式的 XML 文档,它可能不可靠。 根据正则表达式,可能会无意中替换 XML 标记并损坏文档。
+        /// </summary>
+        /// <param name="oldValue">查找字符串</param>
+        /// <param name="newValue">替换字符串</param>
+        public void Replace(string oldValue, string newValue)
+        {
+            Replace(new Dictionary<string, string>()
+            {
+                { oldValue, newValue }
+            });
+        }
+
+        /// <summary>
+        ///  搜索和替换文档<w:t></w:t>的内容。
+        ///  快速方便地搜索和替换。 因为它检索的字符串格式的 XML 文档,它可能不可靠。 根据正则表达式,可能会无意中替换 XML 标记并损坏文档。
+        /// </summary>
+        /// <param name="ky">多个替换内容</param>
+        public void Replace(Dictionary<string, string> ky)
+        {
+            string docText = string.Empty;
+            //在打开文件进行编辑之后,可使用 StreamReader 对象读取该文件。
+            using (StreamReader sr = new StreamReader(wordDoc.MainDocumentPart.GetStream()))
+            {
+                docText = sr.ReadToEnd();
+            }
+
+            foreach (var item in ky)
+            {
+                //创建正则表达式对象,再将该文本值替换文本
+                //文本在xml中为: <w:t>Title</w:t>
+                //加上前后匹配,增加可靠性
+                Regex regexText = new Regex("t>" + item.Key + "</w");
+                docText = regexText.Replace(docText, "t>" + item.Value + "</w");
+            }
+
+            //保存
+            using (StreamWriter sw = new StreamWriter(wordDoc.MainDocumentPart.GetStream(FileMode.Create)))
+            {
+                sw.Write(docText);
+            }
+        }
+
+        /// <summary>
+        /// 保存+关闭
+        /// </summary>
+        public void Save()
+        {
+            wordDoc.Dispose();
+        }
+
+    }
+}

+ 38 - 0
src/QuestionSwap/QuestionSwapDoc/Help/HttpHelp.cs

@@ -0,0 +1,38 @@
+using HtmlAgilityPack;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+
+namespace QuestionSwapDoc.Help
+{
+    public static class HttpHelp
+    {
+        /// <summary>
+        /// 为html中的连接添加左边的路径为绝对路径
+        /// </summary>
+        /// <param name="htmlText"></param>
+        /// <returns></returns>
+        public static string AddLeftLinkPath(string htmlText, string imgLinkLeftPath)
+        {
+            //html解析器
+            HtmlDocument htmlDocument = new HtmlDocument();
+            htmlDocument.LoadHtml(htmlText);
+            var imgs = htmlDocument.DocumentNode.SelectNodes("//img");
+            if (imgs != null)
+            {
+                foreach (var item in imgs)
+                {
+                    var imgSrc = item.Attributes["src"];
+                    var newPath = Path.Join(imgLinkLeftPath, imgSrc.Value);
+                    if (File.Exists(newPath))
+                    {
+                        imgSrc.Value = newPath;
+                    }
+                }
+            }
+
+            return htmlDocument.DocumentNode.OuterHtml;
+        }
+    }
+}

+ 24 - 0
src/QuestionSwap/QuestionSwapDoc/QuestionSwapDoc.csproj

@@ -0,0 +1,24 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>netcoreapp3.1</TargetFramework>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="HtmlAgilityPack" Version="1.11.29" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\OfficeAppHelp\OfficeAppHelp.csproj" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <Reference Include="Interop.Microsoft.Office.Core">
+      <HintPath>..\..\dll\Interop.Microsoft.Office.Core.dll</HintPath>
+    </Reference>
+    <Reference Include="Interop.Microsoft.Office.Interop.Word">
+      <HintPath>..\..\dll\Interop.Microsoft.Office.Interop.Word.dll</HintPath>
+    </Reference>
+  </ItemGroup>
+
+</Project>

+ 390 - 0
src/QuestionSwap/QuestionSwapDoc/SwapQuestion.cs

@@ -0,0 +1,390 @@
+using Microsoft.Office.Interop.Word;
+using OfficeAppHelp.Help;
+using OfficeAppHelp.Word;
+using QuestionSwapDoc.SwapQuestionModel;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using Range = Microsoft.Office.Interop.Word.Range;
+
+namespace QuestionSwapDoc
+{
+    /// <summary>
+    /// 输出为试题
+    /// </summary>
+    public class SwapQuestion
+    {
+        //判断题的答案解析字符串
+        public static readonly string[] PanDtTrue = new string[] { "对", "正确", "√", "✔", };
+        public static readonly string[] PanDtFlase = new string[] { "错", "错误", "×", "✘", };
+        //选择题的选项前缀字符串
+        public static readonly string[] XuanXianQianZui = new string[] { "A.", "B.", "C.", "D.", "E.", "F.", "G.", "H.", "I.", "J.", "K." };
+
+        /// <summary>
+        /// 得到文档解析出来的试题
+        /// </summary>
+        /// <param name="fileName">所有可以用word打开的文件的全路径【如:"D:\word.docx"】</param>
+        /// <param name="xmlDirectory">xml文件文件夹(文件名称随机方式)【为空为文档名加parse,如:"D:\wordparse"】</param>
+        /// <param name="linkDirectory">html关联文件文件夹(文件名称md5方式)【为空为文档名加parse,如:"D:\wordparse"】</param>
+        /// <param name="imgLinkLeftPath">html中img中src的前缀【为空:[img src='md5.png'];假如为‘imgfiles’:[img src='imgfiles\md5.png']】</param>
+        /// <param name="queTypeString">题型字符串。用来告诉解析器那些字符串属于那些题型。</param>
+        /// <returns></returns>
+        public static QueDoc GetQue(string fileName, string xmlDirectory = "", string linkDirectory = "", string imgLinkLeftPath = "", QueTypeString queTypeString = null)
+        {
+            if (queTypeString == null)
+                queTypeString = new QueTypeString();
+            if (queTypeString.XuanZheStrings == null)
+                queTypeString.XuanZheStrings = new string[] { };
+            if (queTypeString.PanDuanStrings == null)
+                queTypeString.PanDuanStrings = new string[] { };
+
+            var p1 = Path.GetDirectoryName(fileName);//D:\
+            var p2 = Path.GetFileNameWithoutExtension(fileName) + "parse";//wordparse
+            var p3 = Path.Combine(p1, p2);//D:\wordparse
+            if (string.IsNullOrWhiteSpace(xmlDirectory))
+                xmlDirectory = p3;
+            if (string.IsNullOrWhiteSpace(linkDirectory))
+                linkDirectory = p3;
+
+            WordApp wordApp = null;
+            try
+            {
+                wordApp = new WordApp(fileName, true, "123");
+                return GetQue(wordApp, xmlDirectory, linkDirectory, imgLinkLeftPath, queTypeString);
+            }
+            catch (Exception ex)
+            {
+                throw ex;
+            }
+            finally
+            {
+                wordApp?.Close();
+            }
+        }
+        private static QueDoc GetQue(WordApp wordApp, string xmlDirectory, string linkDirectory, string imgLinkLeftPath, QueTypeString queTypeString)
+        {
+            QueDoc queDoc = new QueDoc();
+
+            //浮动元素警告
+            var wiWar = wordApp.Doc.Content.GetNotWrapInlineShape()
+                .Select(o => o.Anchor.GetPageNumberStart())
+                .GroupBy(o => o)
+                .Select(o => $"第{o.Key}页,有{o.Count()}个非“嵌入型”元素,可能存在解析不出来的风险。");
+            queDoc.Warning.AddRange(wiWar);
+            //空文档错误
+            if (wordApp.Doc.Content.IsNull())
+            {
+                queDoc.Warning.Add("这是一个空文档,无法解析为试题。");
+                return queDoc;
+            }
+            //tab警告
+            if (wordApp.Doc.Content.FindFirst(WordApp.Find_Tab, true) != null)
+                queDoc.Warning.Add("文档中发现[Tab]按键,可能会影响解析布局。");
+
+            DisposeListFormat(wordApp.Doc);
+            wordApp.Doc.Content.SetColor(WdColor.wdColorBlack);
+            wordApp.Doc.Content.InsertParagraphAfter();
+            wordApp.Doc.Content.Replace(WordApp.Find_BlankRow, WordApp.Find_Paragraph);
+            wordApp.Doc.Content.Replace(WordApp.Find_Tab, "    ");//为了兼容性
+            wordApp.Doc.Content.CharacterWidth = WdCharacterWidth.wdWidthHalfWidth;//全文档改为半角
+
+
+            //上一行是否为空行
+            bool isTopLine = true;
+            //题型
+            string queType = string.Empty;
+            //难易程度(1容易 2较易 3中等 4较难 5困难)
+            int? queDiff = null;
+            //题的标记信息
+            string queData = string.Empty;
+            //解析标识
+            QueItemType queParseItem = QueItemType.空行;
+            //当前选项的索引
+            int danXuanXian = 0;
+
+            List<QueItem> ques = new List<QueItem>();
+            QueItem que = null;
+
+            foreach (Paragraph paragraph in wordApp.Doc.Paragraphs)
+            {
+                Range range = paragraph.Range;
+
+                //当前行 是否为空行
+                bool isInLine = range.IsNullOrSpace();
+
+                //连续空行
+                if (isInLine && isTopLine)
+                {
+                    //跳过
+                    isTopLine = isInLine;
+                    que = null;
+                    continue;
+                }
+                //空行
+                else if (isInLine && !isTopLine)
+                {
+                    //标记结尾
+                    SetEnd(ref que, queParseItem, range.Start, queTypeString);
+                    if (que != null)
+                    {
+                        ques.Add(que);
+                        que = null;
+                    }
+
+                    queParseItem = QueItemType.空行;
+                    isTopLine = isInLine;
+                    continue;
+                }
+                //新的内容(题干或标记)开始
+                else if (!isInLine && isTopLine)
+                {
+                    //题干或者标记
+                    string tgOrbj = range.Text.Trim();
+
+                    //1.对标记处理
+                    if (tgOrbj.StartsWith("题型:") || tgOrbj.StartsWith("题型:") || tgOrbj.StartsWith("【题型】"))
+                    {
+                        if (tgOrbj.StartsWith("【题型】"))
+                            queType = tgOrbj.Substring(4).Trim();
+                        else
+                            queType = tgOrbj.Substring(3).Trim();
+
+                        isTopLine = true;
+                        continue;
+                    }
+                    else if (tgOrbj.StartsWith("难易程度:") || tgOrbj.StartsWith("难易程度:") || tgOrbj.StartsWith("【难易程度】"))
+                    {
+                        if (tgOrbj.StartsWith("【难易程度】"))
+                            queDiff = QueItem.ToNanYiInt(tgOrbj.Substring(6).Trim());
+                        else
+                            queDiff = QueItem.ToNanYiInt(tgOrbj.Substring(5).Trim());
+
+                        isTopLine = true;
+                        continue;
+                    }
+                    else if (tgOrbj.StartsWith("【题编码信息】"))
+                    {
+                        queData = tgOrbj.Substring(7).Base64ToStringTry(out bool tgIsOk);
+                        isTopLine = true;
+                        continue;
+                    }
+
+                    //2.初始化 标记
+                    queParseItem = QueItemType.题干;
+                    que = new QueItem()
+                    {
+                        TiXin = queType,
+                        NanYi = queDiff,
+                    };
+                    que.TiRange.Start = range.Start;
+                    que.TiGanRange.Start = range.Start;
+                    que.Data = queData;
+                    queData = string.Empty;
+                }
+                //正在试题中间
+                else if (!isInLine && !isTopLine)
+                {
+                    string txbj = range.Text.Trim();
+                    //是否为选择题
+                    bool isXx = queTypeString.XuanZheStrings.Contains(queType);
+                    //可能在选项的位置
+                    if (isXx && (queParseItem == QueItemType.题干 || queParseItem == QueItemType.选项))
+                    {
+                        if (queParseItem == QueItemType.题干)
+                            danXuanXian = 0;
+
+                        //确定在选项的位置
+                        if (txbj.StartsWith(XuanXianQianZui[danXuanXian]))
+                        {
+                            //给题干标记结束
+                            if (que.TiGanRange.End == null)
+                                que.TiGanRange.End = range.Start - 1;
+
+                            var r1 = range.FindFirst(XuanXianQianZui[danXuanXian]);
+
+                            queParseItem = QueItemType.选项;
+                            danXuanXian++;
+                            bool isTop = true;
+                            int t = r1.End;
+                            while (isTop)
+                            {
+                                var findRange = range.FindFirst(XuanXianQianZui[danXuanXian]);
+                                if (findRange != null)
+                                {
+                                    Range r2 = wordApp.Doc.Range(t, findRange.Start);
+                                    que.XuanXianRanges.Add(new QueItemRange(t, findRange.Start));
+
+                                    t = findRange.End;
+                                    danXuanXian++;
+                                }
+                                else
+                                {
+                                    Range r2 = wordApp.Doc.Range(t, range.End);
+                                    que.XuanXianRanges.Add(new QueItemRange(t, range.End - 1));
+
+                                    isTop = false;
+                                }
+                            }
+                        }
+                    }
+                    //答案
+                    if (txbj.StartsWith("答案:") || txbj.StartsWith("答案:") || txbj.StartsWith("【答案】"))
+                    {
+                        que.DaAnRange.Start = range.FindFirst("答案").End + 1;
+
+                        //标记结尾
+                        SetEnd(ref que, queParseItem, range.Start, queTypeString);
+
+                        queParseItem = QueItemType.答案;
+                    }
+                    //解析
+                    else if (txbj.StartsWith("解析:") || txbj.StartsWith("解析:") || txbj.StartsWith("【解析】"))
+                    {
+                        que.JieXiRange.Start = range.FindFirst("解析").End + 1;
+
+                        //标记结尾
+                        SetEnd(ref que, queParseItem, range.Start, queTypeString);
+
+                        queParseItem = QueItemType.解析;
+                    }
+
+                }
+
+                //上一行 = 当前行
+                isTopLine = isInLine;
+            }
+
+            //把定位的标记转为文件
+            DocToFile(wordApp.Doc, xmlDirectory, linkDirectory, imgLinkLeftPath, queTypeString, ref ques);
+
+            //选择题的选项为空
+            if (ques.Any(o => queTypeString.XuanZheStrings.Contains(o.TiXin) && (o.XuanXianRanges == null || !o.XuanXianRanges.Any())))
+                queDoc.Warning.Add("发现有无法解析出选项的选择题,请注意选项的编号是否为字母加点开头,如“A.”。");
+
+            queDoc.QueItems = ques;
+            return queDoc;
+        }
+
+
+        /// <summary>
+        /// 把定位的标记转为文件
+        /// </summary>
+        private static void DocToFile(Document Doc, string xmlDirectory, string linkDirectory, string imgLinkLeftPath, QueTypeString queTypeString, ref List<QueItem> ques)
+        {
+            Directory.CreateDirectory(xmlDirectory);
+            Directory.CreateDirectory(linkDirectory);
+
+            foreach (var que in ques)
+            {
+                //标记所有题的位置
+                que.TiAllRange.Start = que.TiRange.Start;
+                que.TiAllRange.End = new List<int?>() { que.JieXiRange.End, que.DaAnRange.End, que.TiRange.End }.Max();
+
+                //填充文件
+                que.TiAllRange.Fill(Doc, xmlDirectory, linkDirectory, imgLinkLeftPath);
+                que.TiRange.Fill(Doc, xmlDirectory, linkDirectory, imgLinkLeftPath);
+                que.TiGanRange.Fill(Doc, xmlDirectory, linkDirectory, imgLinkLeftPath);
+                que.XuanXianRanges.ForEach(o => o.Fill(Doc, xmlDirectory, linkDirectory, imgLinkLeftPath));
+
+                //是否为选择题
+                if (queTypeString.XuanZheStrings.Contains(que.TiXin))
+                {
+                    que.TiStr = que.TiGanRange.GetRange(Doc)?.Text.RemoveESC().Trim().Replace("  ", " ");
+                    que.DaAnRange.FillXx(Doc);
+                }
+                //是否判断题
+                else if (queTypeString.PanDuanStrings.Contains(que.TiXin))
+                {
+                    que.TiStr = que.TiRange.GetRange(Doc)?.Text.RemoveESC().Trim().Replace("  ", " ");
+                    que.DaAnRange.FillPd(Doc, PanDtTrue, PanDtFlase);
+                }
+                else
+                {
+                    que.TiStr = que.TiRange.GetRange(Doc)?.Text.RemoveESC().Trim().Replace("  ", " ");
+                    que.DaAnRange.Fill(Doc, xmlDirectory, linkDirectory, imgLinkLeftPath);
+                }
+
+                que.JieXiRange.Fill(Doc, xmlDirectory, linkDirectory, imgLinkLeftPath);
+            }
+
+        }
+        /// <summary>
+        /// 处理编号
+        /// </summary>
+        /// <param name="Doc"></param>
+        private static void DisposeListFormat(Document Doc)
+        {
+            //上一行是否为空行
+            bool isTopLine = true;
+
+            foreach (Paragraph paragraph in Doc.Paragraphs)
+            {
+                Range range = paragraph.Range;
+                bool isInLine = range.IsNullOrSpace();
+
+                //对题干处理
+                //新的内容(题干或标记)开始
+                if (!isInLine && isTopLine)
+                {
+                    //查找编号(标准的)
+                    if (!string.IsNullOrEmpty(range.ListFormat.ListString))
+                    {
+                        //删除题干的编号(标准的)
+                        range.ListFormat.RemoveNumbers();
+                    }
+                    else
+                    {
+                        //查找编号(非标准的)
+                        string txt = range.Text.GetSerialNumber(false);
+                        if (!string.IsNullOrEmpty(txt))
+                        {
+                            Range serialRange = range.FindFirst(txt);
+                            //Range serialRange = Doc.Range(range.Start, range.Start + txt.Length);
+                            var sss = serialRange.Text;
+                            serialRange.Delete();
+                        }
+                    }
+                }
+
+                //上一行 = 当前行
+                isTopLine = isInLine;
+            }
+
+            //全文编号改为字符串
+            Doc.Content.ListFormat.ConvertNumbersToText();
+
+        }
+        /// <summary>
+        /// 标记结尾
+        /// </summary>
+        private static void SetEnd(ref QueItem que, QueItemType queParseItem, int rangeStart, QueTypeString queTypeString)
+        {
+            if (que == null || queParseItem == QueItemType.空行)
+                return;
+
+            rangeStart--;
+
+            //标记结尾
+            if (queParseItem == QueItemType.题干)
+                if (queTypeString.XuanZheStrings.Contains(que.TiXin))//选择题
+                {
+                    if (que.XuanXianRanges == null || !que.XuanXianRanges.Any())
+                        que.TiRange.End = rangeStart;
+                    que.TiGanRange.End = rangeStart;
+                }
+                else
+                    que.TiRange.End = rangeStart;
+            else if (queParseItem == QueItemType.选项)
+                que.DaAnRange.End = rangeStart;
+            else if (queParseItem == QueItemType.答案)
+                que.DaAnRange.End = rangeStart;
+            else if (queParseItem == QueItemType.解析)
+                que.JieXiRange.End = rangeStart;
+
+            if (que.TiRange.End == null && queParseItem != QueItemType.题干)
+                que.TiRange.End = rangeStart;
+        }
+
+    }
+}

+ 22 - 0
src/QuestionSwap/QuestionSwapDoc/SwapQuestionModel/QueDoc.cs

@@ -0,0 +1,22 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace QuestionSwapDoc.SwapQuestionModel
+{
+    /// <summary>
+    /// 试题文档
+    /// </summary>
+    public class QueDoc
+    {
+        /// <summary>
+        /// 解析过程中的警告
+        /// </summary>
+        public List<string> Warning { get; set; } = new List<string>();
+        /// <summary>
+        /// 试题
+        /// </summary>
+        public List<QueItem> QueItems { get; set; } = new List<QueItem>();
+
+    }
+}

+ 117 - 0
src/QuestionSwap/QuestionSwapDoc/SwapQuestionModel/QueItem.cs

@@ -0,0 +1,117 @@
+
+using System;
+using System.Linq;
+using System.Collections.Generic;
+using System.Text;
+
+namespace QuestionSwapDoc.SwapQuestionModel
+{
+    /// <summary>
+    /// 试题
+    /// </summary>
+    public class QueItem
+    {
+        /// <summary>
+        /// 关于此题的标记信息
+        /// </summary>
+        public string Data { get; set; }
+        /// <summary>
+        /// 题型
+        /// </summary>
+        public string TiXin { get; set; }
+        /// <summary>
+        /// 难易程度(1容易 2较易 3中等 4较难 5困难)
+        /// </summary>
+        public int? NanYi { get; set; }
+
+        /// <summary>
+        /// 题所有(题所有=题干+选项+答案+解析)
+        /// </summary>
+        public QueItemRange TiAllRange { get; set; } = new QueItemRange();
+        /// <summary>
+        /// 题(题=题干+选项)
+        /// </summary>
+        public QueItemRange TiRange { get; set; } = new QueItemRange();
+        /// <summary>
+        /// 题的简要文本(用来在数据库中快速搜索题)
+        /// </summary>
+        public string TiStr { get; set; }
+        /// <summary>
+        /// 题干(选择题才有)
+        /// </summary>
+        public QueItemRange TiGanRange { get; set; } = new QueItemRange();
+        /// <summary>
+        /// 选项(选择题才有)
+        /// </summary>
+        public List<QueItemRange> XuanXianRanges { get; set; } = new List<QueItemRange>();
+        /// <summary>
+        /// 答案
+        /// (单选题、多选题为字符串,如"A,B,C")
+        /// (判断题为bool值,如"True")
+        /// </summary>
+        public QueItemRange DaAnRange { get; set; } = new QueItemRange();
+        /// <summary>
+        /// 解析
+        /// </summary>
+        public QueItemRange JieXiRange { get; set; } = new QueItemRange();
+
+
+        /// <summary>
+        /// 指定选项是否为正确答案
+        /// </summary>
+        /// <param name="XuanXianRange">选项</param>
+        /// <returns></returns>
+        public bool IsXuanXianDaAn(QueItemRange XuanXianRange)
+        {
+            var index = XuanXianRanges.IndexOf(XuanXianRange);
+            if (index == -1)
+                return false;
+
+            return IsXuanXianDaAn(index);
+        }
+        /// <summary>
+        /// 指定选项是否为正确答案
+        /// </summary>
+        /// <param name="index">选项的索引</param>
+        /// <returns></returns>
+        public bool IsXuanXianDaAn(int index)
+        {
+            if (index > SwapQuestion.XuanXianQianZui.Length)
+                return false;
+
+            var qianZui = SwapQuestion.XuanXianQianZui[index].Substring(0, 1);
+            return DaAnRange.HtmlText.Contains(qianZui);
+        }
+
+
+        /// <summary>
+        /// 转为难易程度int类型(1容易 2较易 3中等 4较难 5困难)
+        /// </summary>
+        /// <param name="txt">中文或者(1-5的汉字)</param>
+        /// <returns></returns>
+        public static int? ToNanYiInt(string txt)
+        {
+            switch (txt)
+            {
+                case "1":
+                case "容易":
+                    return 1;
+                case "2":
+                case "较易":
+                    return 2;
+                case "3":
+                case "中等":
+                    return 3;
+                case "4":
+                case "较难":
+                    return 4;
+                case "5":
+                case "困难":
+                    return 5;
+                default:
+                    return null;
+            }
+        }
+
+    }
+}

+ 196 - 0
src/QuestionSwap/QuestionSwapDoc/SwapQuestionModel/QueItemRange.cs

@@ -0,0 +1,196 @@
+using HtmlAgilityPack;
+using Microsoft.Office.Interop.Word;
+using OfficeAppHelp.Help;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Text.RegularExpressions;
+using Range = Microsoft.Office.Interop.Word.Range;
+
+namespace QuestionSwapDoc.SwapQuestionModel
+{
+    /// <summary>
+    /// 试题项的一个范围
+    /// </summary>
+    public class QueItemRange
+    {
+        public QueItemRange()
+        {
+            Start = null;
+            End = null;
+        }
+        public QueItemRange(int? start, int? end)
+        {
+            Start = start;
+            End = end;
+        }
+
+
+        /// <summary>
+        /// 开始
+        /// </summary>
+        public int? Start { get; set; }
+        /// <summary>
+        /// 结束
+        /// </summary>
+        public int? End { get; set; }
+        /// <summary>
+        /// 是否为空的或者无效的范围
+        /// </summary>
+        public bool IsNull { get => Start == null || Start < 0 || End == null || End <= 0; }
+        /// <summary>
+        /// html文本
+        /// </summary>
+        public string HtmlText { get; set; }
+        /// <summary>
+        /// xml文件
+        /// </summary>
+        public string XmlFile { get; set; }
+
+
+        /// <summary>
+        /// 得到范围
+        /// </summary>
+        /// <param name="Doc"></param>
+        /// <returns></returns>
+        public Range GetRange(Document Doc)
+        {
+            if (IsNull)
+                return null;
+
+            return Doc.Range(Start, End);
+        }
+
+        /// <summary>
+        /// 填充范围内容
+        /// </summary>
+        /// <param name="Doc"></param>
+        /// <param name="xmlDirectory">xml文件放置文件夹</param>
+        /// <param name="linkDirectory">解析的所有html文件里面的连接文件将放在这里(如图片)</param>
+        /// <param name="imgLinkLeftPath">前缀</param>
+        public void Fill(Document Doc, string xmlDirectory = "", string linkDirectory = "", string imgLinkLeftPath = "")
+        {
+            Range range = GetRange(Doc);
+            if (range == null)
+                return;
+
+            //生成xml
+            if (!string.IsNullOrWhiteSpace(xmlDirectory))
+            {
+                string randomPath = Path.GetRandomFileName().Replace(".", "");
+                string xmlPath = Path.Combine(xmlDirectory, randomPath + ".docx");
+                range.ExportFragment(xmlPath, WdSaveFormat.wdFormatXMLDocument);
+                XmlFile = xmlPath;
+            }
+
+            HtmlText = FillHtml(range, linkDirectory, imgLinkLeftPath);
+        }
+        /// <summary>
+        /// 填充范围内容(Html)
+        /// </summary>
+        /// <param name="linkDirectory">解析的所有html文件里面的连接文件将放在这里(如图片)</param>
+        /// <param name="imgLinkLeftPath">前缀</param>
+        public static string FillHtml(Range range, string linkDirectory = "", string imgLinkLeftPath = "")
+        {
+            //生成html
+            if (!string.IsNullOrWhiteSpace(linkDirectory))
+            {
+                string randomPath = Path.GetRandomFileName().Replace(".", "");
+                string htmlPath = Path.Combine(linkDirectory, randomPath + ".html");
+                range.ExportFragment(htmlPath, WdSaveFormat.wdFormatHTML);
+
+                //注册字符编码
+                Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
+                var htmlText = File.ReadAllText(htmlPath, Encoding.GetEncoding("GB2312"));
+
+                //html解析器
+                HtmlDocument htmlDocument = new HtmlDocument();
+                htmlDocument.LoadHtml(htmlText);
+
+                //删除html节点中的属性
+                htmlDocument.DocumentNode.SelectSingleNode("//html")?.Attributes.RemoveAll();
+                //head节点中只保留style节点
+                var stylehtml = htmlDocument.DocumentNode.SelectSingleNode("//head")?.Descendants("style")?.FirstOrDefault();
+                if (stylehtml != null)
+                {
+                    htmlDocument.DocumentNode.SelectSingleNode("//head")?.RemoveAll();
+                    htmlDocument.DocumentNode.SelectSingleNode("//head")?.InsertAfter(stylehtml, null);
+                }
+                //删除注释
+                htmlDocument.DocumentNode.SelectNodes("//comment()")?.ToList()?.ForEach(o => o.Remove());
+
+                var imgs = htmlDocument.DocumentNode.SelectNodes("//img");
+                if (imgs != null)
+                {
+                    foreach (var item in imgs)
+                    {
+                        var imgSrc = item.Attributes["src"];
+                        var imgPath = Path.Combine(linkDirectory, imgSrc.Value);
+                        if (File.Exists(imgPath))
+                        {
+                            //图片使用链接:
+                            var md5 = FileHelp.GetFileMD5(imgPath);
+                            var newFilName = md5 + Path.GetExtension(imgPath);
+                            var newImgPath = Path.Combine(linkDirectory, newFilName);
+                            //避免重复的文件/图片
+                            if (!File.Exists(newImgPath))
+                                File.Copy(imgPath, newImgPath);
+
+                            var newImgSrc = Path.Combine(imgLinkLeftPath, newFilName);
+                            imgSrc.Value = newImgSrc;
+
+                            ////图片使用base64:
+                            //var b64 = $"data:image/{Path.GetExtension(imgPath).Replace(".", "")};base64,{Convert.ToBase64String(File.ReadAllBytes(imgPath))}";
+                            //imgSrc.Value = b64;
+                        }
+                    }
+                }
+
+
+                //删除html和生成的图片
+                File.Delete(htmlPath);
+                var dirFile = Path.Combine(linkDirectory, randomPath + ".files");
+                if (Directory.Exists(dirFile))
+                    Directory.Delete(dirFile, true);
+
+                //压缩html
+                return htmlDocument.DocumentNode.OuterHtml.Replace("\n", "").Replace("\r", "");
+            }
+            return string.Empty;
+        }
+        /// <summary>
+        /// 填充范围内容(选择题)
+        /// </summary>
+        /// <param name="Doc"></param>
+        public void FillXx(Document Doc)
+        {
+            Range range = GetRange(Doc);
+            if (range == null)
+                return;
+
+            HtmlText = string.Join(",", range.Text.GetUpperLetter().OrderBy(o => o));
+        }
+        /// <summary>
+        /// 填充范围内容(判断题)
+        /// </summary>
+        /// <param name="Doc"></param>
+        /// <param name="itemT">正确的集合</param>
+        /// <param name="itemF">错误的集合</param>
+        public void FillPd(Document Doc, string[] itemT, string[] itemF)
+        {
+            Range range = GetRange(Doc);
+            if (range == null)
+                return;
+
+            var te = range.Text.Trim();
+            if (itemT.Contains(te))
+                HtmlText = true.ToString();
+            else if (itemF.Contains(te))
+                HtmlText = false.ToString();
+
+        }
+
+    }
+}

+ 22 - 0
src/QuestionSwap/QuestionSwapDoc/SwapQuestionModel/QueItemType.cs

@@ -0,0 +1,22 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace QuestionSwapDoc.SwapQuestionModel
+{
+    /// <summary>
+    /// 题解析出现的项的可能类型
+    /// </summary>
+    public enum QueItemType
+    {
+        空行,
+        /// <summary>
+        /// 试题=题干+选项
+        /// </summary>
+        试题,
+        题干,
+        选项,
+        答案,
+        解析,
+    }
+}

+ 22 - 0
src/QuestionSwap/QuestionSwapDoc/SwapQuestionModel/QueTypeString.cs

@@ -0,0 +1,22 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace QuestionSwapDoc.SwapQuestionModel
+{
+    /// <summary>
+    /// 题型字符串
+    /// </summary>
+    public class QueTypeString
+    {
+        /// <summary>
+        /// 选择题字符串(单选+多选)(不能有前后空格)
+        /// </summary>
+        public string [] XuanZheStrings { get; set; } = new string[] { "单选题", "选择题", "单项选择题", "多选题", "多项选择题" };
+        /// <summary>
+        /// 判断题字符串(不能有前后空格)
+        /// </summary>
+        public string[] PanDuanStrings { get; set; } = new string[] { "判断题" };
+
+    }
+}

+ 1061 - 0
src/QuestionSwap/QuestionSwapDoc/SwapWord.cs

@@ -0,0 +1,1061 @@
+using OfficeAppHelp.Word;
+using Microsoft.Office.Core;
+using Microsoft.Office.Interop.Word;
+using System;
+using System.Collections.Generic;
+using System.Text;
+using QuestionSwapDoc.SwapWordModel;
+using System.Linq;
+using System.IO;
+using QuestionSwapDoc.Help;
+using QuestionSwapDoc.SwapQuestionModel;
+using OfficeAppHelp.Help;
+
+namespace QuestionSwapDoc
+{
+    /// <summary>
+    /// 输出为文档
+    /// </summary>
+    public class SwapWord
+    {
+        /// <summary>
+        /// 创建试卷、答案、解析、答题卡
+        /// </summary>
+        /// <param name="saveDirectory">保存文件夹的绝对路径</param>
+        /// <param name="queConfig">试卷配置(为null则返回空数据)</param>
+        /// <param name="daAnConfig">答案解析配置(为null表示没有答案解析)</param>
+        /// <param name="daTiKaConfig">答题卡配置(为null表示没有答题卡)</param>
+        /// <param name="ques">试题集合(为null则返回空数据)</param>
+        public static List<SwapWordCreateQueResult> CreateQue(string saveDirectory, SwapQueConfig queConfig, SwapDaAnConfig daAnConfig, SwapDaTiKaConfig daTiKaConfig, SwapTiXins ques)
+        {
+            List<SwapWordCreateQueResult> swapWordResults = new List<SwapWordCreateQueResult>();
+            if (queConfig == null || ques == null || ques.TiXinList == null || !ques.TiXinList.Any())
+                return swapWordResults;
+
+            WordApp wordApp = new WordApp();
+            if (queConfig.ZhiZhangDaXiao == PaperQue.A4竖向)
+                wordApp.NewDoc();
+            else
+            {
+                wordApp.NewDoc(WdPaperSize.wdPaperA3, WdOrientation.wdOrientLandscape);
+                //分栏
+                var textColumns = wordApp.Doc.PageSetup.TextColumns;
+                textColumns.SetCount(2);
+                textColumns.Spacing = 42;
+            }
+
+            //储存文档默认样式
+            var rangeDocStyte = wordApp.Doc.RangeEnd();
+            var docLineUnitAfter = rangeDocStyte.ParagraphFormat.LineUnitAfter;
+            var docLineUnitBefore = rangeDocStyte.ParagraphFormat.LineUnitBefore;
+            var docFontSize = rangeDocStyte.Font.Size;
+            var docFontBold = rangeDocStyte.Font.Bold;
+            var docFontColor = rangeDocStyte.Font.Color;
+
+            #region 生成试卷
+            //设置试卷头部
+            if (queConfig.ZhuBiaoTi != null)
+            {
+                var range = wordApp.Doc.RangeEnd();
+                range.Text = queConfig.ZhuBiaoTi;
+                range.ParagraphFormat.Alignment = WdParagraphAlignment.wdAlignParagraphCenter;
+                range.ParagraphFormat.LineUnitAfter = 0.25f;
+                range.Font.Size = 12;
+                range.Font.Bold = -1;
+                range.InsertParagraphAfter();
+            }
+            if (queConfig.FuBiaoTi != null)
+            {
+                var range = wordApp.Doc.RangeEnd();
+                range.Text = queConfig.FuBiaoTi;
+                range.ParagraphFormat.Alignment = WdParagraphAlignment.wdAlignParagraphCenter;
+                range.ParagraphFormat.LineUnitAfter = 0.25f;
+                range.Font.Size = 14;
+                range.Font.Bold = -1;
+                range.InsertParagraphAfter();
+            }
+            if (queConfig.ShiTiXinXi != null)
+            {
+                var range = wordApp.Doc.RangeEnd();
+                range.Text = queConfig.ShiTiXinXi;
+                range.ParagraphFormat.Alignment = WdParagraphAlignment.wdAlignParagraphCenter;
+                range.ParagraphFormat.LineUnitAfter = 0.25f;
+                range.Font.Size = 10;
+                range.Font.Bold = 0;
+                range.InsertParagraphAfter();
+            }
+            if (queConfig.KaoShengShuRu != null)
+            {
+                var range = wordApp.Doc.RangeEnd();
+                range.Text = queConfig.KaoShengShuRu;
+                range.ParagraphFormat.Alignment = WdParagraphAlignment.wdAlignParagraphCenter;
+                range.ParagraphFormat.LineUnitAfter = 0.25f;
+                range.Font.Size = 10;
+                range.Font.Bold = 0;
+                range.InsertParagraphAfter();
+            }
+            if (queConfig.ZhuYiShiXian != null)
+            {
+                var range = wordApp.Doc.RangeEnd();
+                range.Text = queConfig.ZhuYiShiXian;
+                range.ParagraphFormat.Alignment = WdParagraphAlignment.wdAlignParagraphLeft;
+                range.ParagraphFormat.LineUnitAfter = 0.25f;
+                range.Font.Color = WdColor.wdColorGray60;
+                range.Font.Size = 10;
+                range.Font.Bold = 0;
+                range.InsertParagraphAfter();
+            }
+            if (queConfig.IsFenJuan)
+            {
+                var range = wordApp.Doc.RangeEnd();
+                range.Text = "第Ⅰ卷";
+                range.ParagraphFormat.Alignment = WdParagraphAlignment.wdAlignParagraphCenter;
+                range.ParagraphFormat.LineUnitBefore = 0.25f;
+                range.ParagraphFormat.LineUnitAfter = 0.25f;
+                range.ParagraphFormat.Shading.BackgroundPatternColor = WdColor.wdColorGray15;
+                range.Font.Color = WdColor.wdColorBlack;
+                range.Font.Size = 14;
+                range.Font.Bold = 0;
+                range.InsertParagraphAfter();
+            }
+
+            //还原样式
+            var rangeEnd = wordApp.Doc.RangeEnd();
+            rangeEnd.ParagraphFormat.Shading.BackgroundPatternColorIndex = WdColorIndex.wdNoHighlight;
+            rangeEnd.ParagraphFormat.Alignment = WdParagraphAlignment.wdAlignParagraphLeft;
+            rangeEnd.ParagraphFormat.LineUnitBefore = docLineUnitBefore;
+            rangeEnd.ParagraphFormat.LineUnitAfter = docLineUnitAfter;
+            rangeEnd.Font.Color = docFontColor;
+            rangeEnd.Font.Size = docFontSize;
+            rangeEnd.Font.Bold = docFontBold;
+
+            //添加题
+            ques.UpdateNumber();
+            ques.QueHtmlContentToFile();
+            foreach (var tiXin in ques.TiXinList)
+            {
+                var range = wordApp.Doc.RangeEnd();
+                //分卷
+                if (queConfig.IsFenJuan && !tiXin.IsPanDuan && !tiXin.IsXuanZhe)
+                {
+                    range.InsertBreak(WdBreakType.wdPageBreak);
+                    range.Text = "第Ⅱ卷";
+                    range.ParagraphFormat.Alignment = WdParagraphAlignment.wdAlignParagraphCenter;
+                    range.ParagraphFormat.LineUnitBefore = 0.25f;
+                    range.ParagraphFormat.LineUnitAfter = 0.25f;
+                    range.ParagraphFormat.Shading.BackgroundPatternColor = WdColor.wdColorGray15;
+                    range.Font.Color = WdColor.wdColorBlack;
+                    range.Font.Size = 14;
+                    range.Font.Bold = 0;
+                    range.InsertParagraphAfter();
+                    //还原样式
+                    rangeEnd = wordApp.Doc.RangeEnd();
+                    rangeEnd.ParagraphFormat.Shading.BackgroundPatternColorIndex = WdColorIndex.wdNoHighlight;
+                    rangeEnd.ParagraphFormat.Alignment = WdParagraphAlignment.wdAlignParagraphLeft;
+                    rangeEnd.ParagraphFormat.LineUnitBefore = docLineUnitBefore;
+                    rangeEnd.ParagraphFormat.LineUnitAfter = docLineUnitAfter;
+                    rangeEnd.Font.Color = docFontColor;
+                    rangeEnd.Font.Size = docFontSize;
+                    rangeEnd.Font.Bold = docFontBold;
+                }
+                //添加题型文本
+                var fentxtxt = tiXin.GetFenStr(queConfig.IsFen);
+                range.InsertAfter(tiXin.NumberStr + tiXin.TiXinStr + fentxtxt);
+                range.InsertParagraphAfter();
+                foreach (var que in tiXin.QueList)
+                {
+                    var fenttxt = tiXin.GetFenStr(string.IsNullOrEmpty(fentxtxt));
+                    range.InsertAfter(que.NumberStr + " " + fenttxt);
+                    var upath = que.TiRange.GetUseFile();
+                    if (File.Exists(upath))
+                    {
+                        range = wordApp.Doc.RangeEnd();
+                        range.InsertFile(upath);
+                    }
+                    else
+                    {
+                        range.InsertParagraphAfter();
+                    }
+                }
+            }
+            AddZhuanDinXian(wordApp.Doc, queConfig.ZhuanDinXian);
+            #endregion
+
+            #region 生成答案
+            if (daAnConfig != null || daAnConfig.IsShowDaAn || daAnConfig.IsShowJieXi)
+            {
+                SwapWordResultType swapWordResultType;
+                //答案是否在新的文档中
+                if (!daAnConfig.IsInQue)
+                {
+                    //保存试卷
+                    swapWordResults.Add(new SwapWordCreateQueResult()
+                    {
+                        resultType = SwapWordResultType.试卷,
+                        Path = DocSaveToDirectory(wordApp.Doc, saveDirectory)
+                    });
+                    wordApp.Close();
+                    wordApp.NewDoc();
+
+                    swapWordResultType = SwapWordResultType.答题卡;
+                }
+                else
+                {
+                    var rangeBreak = wordApp.Doc.RangeEnd();
+                    rangeBreak.InsertBreak(WdBreakType.wdPageBreak);
+
+                    swapWordResultType = SwapWordResultType.试卷和答案解析;
+                }
+
+                //添加标题
+                if (daAnConfig.Title != null)
+                {
+                    //标题范围
+                    var rangeTou = wordApp.Doc.RangeEnd();
+                    //储存值
+                    docFontSize = rangeTou.Font.Size;
+                    //设置值
+                    rangeTou.Text = daAnConfig.Title;
+                    rangeTou.Font.Size = 22;
+                    rangeTou.ParagraphFormat.Alignment = WdParagraphAlignment.wdAlignParagraphCenter;
+                    //换行、还原值
+                    rangeTou.InsertParagraphAfter();
+                    rangeTou = wordApp.Doc.RangeEnd();
+                    rangeTou.Font.Size = docFontSize;
+                    rangeTou.ParagraphFormat.Alignment = WdParagraphAlignment.wdAlignParagraphLeft;
+                }
+
+                //添加答案
+                AddDaAn(wordApp.Doc, daAnConfig, ques);
+                //保存答案解析或试卷
+                swapWordResults.Add(new SwapWordCreateQueResult()
+                {
+                    resultType = swapWordResultType,
+                    Path = DocSaveToDirectory(wordApp.Doc, saveDirectory)
+                });
+                wordApp.Close();
+            }
+
+            //还原,清理文件
+            ques.QueHtmlFileToContent();
+            #endregion
+
+            #region 生成答题卡
+            if (daTiKaConfig != null && ques != null && ques.TiXinList != null)
+            {
+                if (daTiKaConfig.ZhiZhangDaXiao == PaperDaTiKa.A4竖向)
+                {
+                    wordApp.NewDoc();
+                }
+                else if (daTiKaConfig.ZhiZhangDaXiao == PaperDaTiKa.A3横向)
+                {
+                    wordApp.NewDoc(WdPaperSize.wdPaperA3, WdOrientation.wdOrientLandscape);
+                    //分栏
+                    var textColumns = wordApp.Doc.PageSetup.TextColumns;
+                    textColumns.SetCount(2);
+                }
+                else if (daTiKaConfig.ZhiZhangDaXiao == PaperDaTiKa.A3横向3)
+                {
+                    wordApp.NewDoc(WdPaperSize.wdPaperA3, WdOrientation.wdOrientLandscape);
+                    //分栏
+                    var textColumns = wordApp.Doc.PageSetup.TextColumns;
+                    textColumns.SetCount(3);
+                }
+
+                //添加定位
+                AddDtkDinWei(wordApp.Doc);
+                //添加头部信息
+                AddDtkTou(wordApp.Doc, daTiKaConfig);
+                //添加答题区域
+                AddDtkNeiRong(wordApp.Doc, daTiKaConfig, ques);
+
+                //保存答题卡
+                swapWordResults.Add(new SwapWordCreateQueResult()
+                {
+                    resultType = SwapWordResultType.答题卡,
+                    Path = DocSaveToDirectory(wordApp.Doc, saveDirectory)
+                });
+                wordApp.Close();
+            }
+            #endregion
+
+            return swapWordResults;
+        }
+
+        /// <summary>
+        /// 合并试题
+        /// </summary>
+        /// <param name="fileName">文件的绝对路径(如"D:\word.docx")</param>
+        /// <returns></returns>
+        public static void JoinQue(string fileName, SwapWordJoinQueItem swapWordJoinQueItem)
+        {
+            JoinQue(fileName, new List<SwapWordJoinQueItem>() { swapWordJoinQueItem } );
+        }
+
+        /// <summary>
+        /// 合并试题
+        /// </summary>
+        /// <param name="fileName">文件的绝对路径(如"D:\word.docx")</param>
+        /// <returns></returns>
+        public static void JoinQue(string fileName, List<SwapWordJoinQueItem> swapWordJoinQueItems)
+        {
+            WordApp wordApp = null;
+            try
+            {
+                wordApp = new WordApp();
+                wordApp.NewDoc();
+                SwapWordJoinQueItem.QueHtmlContentToFile(swapWordJoinQueItems);
+                foreach (var item in swapWordJoinQueItems)
+                {
+                    if (item.TiRange != null && !item.TiRange.IsNull)
+                    {
+                        if (!string.IsNullOrWhiteSpace(item.Data))
+                            wordApp.Doc.Content.InsertAfter($"【题编码信息】{item.Data.ToBase64()}\r\n\r\n");
+                        if (!string.IsNullOrWhiteSpace(item.TiXinStr))
+                            wordApp.Doc.Content.InsertAfter($"题型:{item.TiXinStr}\r\n\r\n");
+
+                        //添加题
+                        wordApp.Doc.Content.InsertParagraphAfter();
+                        wordApp.Doc.RangeEnd().InsertFile(item.TiRange.GetUseFile());
+                        if (item.DaAnRange != null && !item.DaAnRange.IsNull)
+                        {
+                            if (item.IsXuanZhe)
+                            {
+                                wordApp.Doc.ParagraphEndCount(1);
+                                wordApp.Doc.Content.InsertAfter($"答案:{item.DaAnRange.HtmlText}");
+                                wordApp.Doc.Content.InsertParagraphAfter();
+                            }
+                            else if (item.IsPanDuan)
+                            {
+                                if (bool.TryParse(item.DaAnRange.HtmlText, out bool jgbool))
+                                {
+                                    wordApp.Doc.ParagraphEndCount(1);
+                                    wordApp.Doc.Content.InsertAfter($"答案:" + (jgbool ? "正确" : "错误"));
+                                    wordApp.Doc.Content.InsertParagraphAfter();
+                                }
+                            }
+                            else
+                            {
+                                wordApp.Doc.Content.InsertAfter($"答案:");
+                                wordApp.Doc.RangeEnd().InsertFile(item.DaAnRange.GetUseFile());
+                            }
+                        }
+                        if (item.JieXiRange != null && !item.JieXiRange.IsNull)
+                        {
+                            wordApp.Doc.Content.InsertAfter("解析:");
+                            wordApp.Doc.RangeEnd().InsertFile(item.JieXiRange.GetUseFile());
+                        }
+                        wordApp.Doc.Content.InsertParagraphAfter();
+                    }
+                }
+                wordApp.Save(fileName);
+            }
+            catch (Exception ex)
+            {
+                throw ex;
+            }
+            finally
+            {
+                SwapWordJoinQueItem.QueHtmlFileToContent(swapWordJoinQueItems);
+                wordApp?.Close();
+            }
+        }
+
+        /// <summary>
+        /// html文本转为word文件
+        /// </summary>
+        /// <param name="fileName">文件的绝对路径(如"D:\word.docx")</param>
+        /// <param name="htmlText">html文本</param>
+        /// <param name="imgLinkLeftPath">网页中的图片的左边的路径(和图片路径拼接成绝对路径)</param>
+        /// <returns></returns>
+        public static void HtmlToWord(string fileName, string htmlText, string imgLinkLeftPath = "")
+        {
+            var htmlpath = Path.GetFullPath(Path.GetRandomFileName().Replace(".", "") + ".html");
+            File.WriteAllText(htmlpath, HttpHelp.AddLeftLinkPath(htmlText, imgLinkLeftPath), Encoding.UTF8);
+                WordApp wordApp = new WordApp();
+            try
+            {
+                wordApp.NewDoc();
+                wordApp.Doc.Content.InsertFile(htmlpath);
+                wordApp.Save(fileName);
+            }
+            catch (Exception ex)
+            {
+                throw ex;
+            }
+            finally
+            {
+                wordApp?.Close();
+                if (File.Exists(htmlpath))
+                    File.Delete(htmlpath);
+            }
+        }
+
+        /// <summary>
+        /// word文件转为html文本
+        /// </summary>
+        /// <param name="fileName">文件的绝对路径(如"D:\word.docx")</param>
+        /// <param name="linkDirectory">html关联文件文件夹(文件名称md5方式)【为空为文档名加parse,如:"D:\wordparse"】</param>
+        /// <param name="imgLinkLeftPath">html中img中src的前缀【为空:[img src='md5.png'];假如为‘imgfiles’:[img src='imgfiles\md5.png']】</param>
+        /// <returns>Html内容</returns>
+        public static string WordToHtml(string fileName, string linkDirectory = "", string imgLinkLeftPath = "")
+        {
+            var p1 = Path.GetDirectoryName(fileName);//D:\
+            var p2 = Path.GetFileNameWithoutExtension(fileName) + "parse";//wordparse
+            var p3 = Path.Combine(p1, p2);//D:\wordparse
+            if (string.IsNullOrWhiteSpace(linkDirectory))
+                linkDirectory = p3;
+
+            WordApp wordApp = null;
+            string html = string.Empty;
+            try
+            {
+                Directory.CreateDirectory(linkDirectory);
+
+                wordApp = new WordApp(fileName, true, "123");
+                wordApp.Doc.Content.SetColor(WdColor.wdColorBlack);
+                wordApp.Doc.Content.Replace(WordApp.Find_BlankRow, WordApp.Find_Paragraph);
+                wordApp.Doc.Content.Replace(WordApp.Find_Tab, "    ");//为了兼容性
+                html = QueItemRange.FillHtml(wordApp.Doc.Content, linkDirectory, imgLinkLeftPath);
+            }
+            catch (Exception ex)
+            {
+                throw ex;
+            }
+            finally
+            {
+                wordApp?.Close();
+            }
+            return html;
+        }
+
+        /// <summary>
+        /// 填充装订线(支持A3,A4)
+        /// </summary>
+        /// <param name="doc"></param>
+        /// <param name="zhuanDinXian"></param>
+        private static void AddZhuanDinXian(Document doc, ZhuanDinXian zhuanDinXian)
+        {
+            if (zhuanDinXian == null || !zhuanDinXian.IsShow)
+                return;
+
+            bool isA4 = doc.PageSetup.PaperSize == WdPaperSize.wdPaperA4;
+            float widthGutter = 15;//装订线的多余宽度
+
+            //装订线的多余宽度
+            doc.PageSetup.Gutter = widthGutter;
+            if (zhuanDinXian.IsDuplexPrinting)
+                doc.PageSetup.MirrorMargins = -1;
+
+            float widthGutterTop = 10;//装订线的上下间距
+            float widthGutterLeft = 10;//装订线的纸张边外侧间距
+            float widthGutterRight = 17;//装订线的纸张边内侧间距
+            float width = doc.PageSetup.PageWidth;//页面宽度
+            float widthLeft = doc.PageSetup.LeftMargin;//纸张内侧宽度
+            float height = doc.PageSetup.PageHeight;//页面高度
+
+            //当前页码
+            int pageNum = 1;
+            foreach (var pRange in doc.GetPageRanges())
+            {
+
+                //是否为偶数页
+                var IsOdd = !((pageNum % 2) == 1);
+                if (zhuanDinXian.IsDuplexPrinting && IsOdd)
+                {
+                    var shape = doc.Shapes.AddTextbox(
+                         MsoTextOrientation.msoTextOrientationVerticalFarEast,
+                         width - widthLeft - widthGutter + widthGutterRight,
+                         widthGutterTop,
+                         (widthGutter + widthLeft) - (widthGutterLeft + widthGutterRight),
+                         height - (widthGutterTop * 2),
+                         pRange);
+
+                    //字体居中竖向显示
+                    shape.TextFrame.Orientation = MsoTextOrientation.msoTextOrientationDownward;
+                    shape.TextFrame.TextRange.ParagraphFormat.Alignment = WdParagraphAlignment.wdAlignParagraphCenter;
+                    shape.TextFrame.TextRange.ParagraphFormat.LineUnitAfter = 0f;
+                    if (!zhuanDinXian.IsShowLine)
+                        shape.Line.Visible = MsoTriState.msoFalse;
+
+                    //文本区域
+                    var textRange = shape.TextFrame.TextRange;
+                    //添加文本
+                    if (isA4)
+                        textRange.InsertParagraphAfter();
+                    textRange.InsertParagraphAfter();
+                    textRange.InsertParagraphAfter();
+                    textRange.InsertAfter(zhuanDinXian.GetAlurSplitText());
+                }
+                else
+                {
+                    var shape = doc.Shapes.AddTextbox(
+                        MsoTextOrientation.msoTextOrientationVerticalFarEast,
+                        widthGutterLeft,
+                        widthGutterTop,
+                        (widthGutter + widthLeft) - (widthGutterLeft + widthGutterRight),
+                        height - (widthGutterTop * 2),
+                        pRange);
+
+                    //字体居中竖向显示
+                    shape.TextFrame.Orientation = MsoTextOrientation.msoTextOrientationUpward;
+                    shape.TextFrame.TextRange.ParagraphFormat.Alignment = WdParagraphAlignment.wdAlignParagraphCenter;
+                    shape.TextFrame.TextRange.ParagraphFormat.LineUnitAfter = 0f;
+                    if (!zhuanDinXian.IsShowLine)
+                        shape.Line.Visible = MsoTriState.msoFalse;
+
+                    //文本区域
+                    var textRange = shape.TextFrame.TextRange;
+                    if (isA4)
+                        textRange.InsertParagraphAfter();
+                    if (pageNum == 1 || zhuanDinXian.IsNewPageShowWriteText)
+                    {
+                        //添加文本
+                        textRange.InsertAfter(zhuanDinXian.WriteText);
+                        textRange.InsertParagraphAfter();
+                        textRange.InsertParagraphAfter();
+                        textRange.InsertAfter(zhuanDinXian.GetAlurSplitText());
+                    }
+                    else
+                    {
+                        //添加文本
+                        textRange.InsertParagraphAfter();
+                        textRange.InsertParagraphAfter();
+                        textRange.InsertAfter(zhuanDinXian.GetAlurSplitText());
+                    }
+
+                }
+                pageNum++;
+            }
+
+        }
+        /// <summary>
+        /// 添加答案
+        /// </summary>
+        /// <param name="doc"></param>
+        /// <param name="daAnConfig"></param>
+        /// <param name="ques"></param>
+        private static void AddDaAn(Document doc, SwapDaAnConfig daAnConfig, SwapTiXins ques)
+        {
+            //题型
+            foreach (var tiXin in ques.TiXinList)
+            {
+                var range = doc.RangeEnd();
+                range.InsertAfter(tiXin.NumberStr + tiXin.TiXinStr);
+                range.InsertParagraphAfter();
+                //题
+                foreach (var que in tiXin.QueList)
+                {
+                    range.InsertAfter(que.NumberStr + " ");
+                    //添加答案
+                    if (daAnConfig.IsShowDaAn)
+                    {
+                        range.InsertAfter("答案:");
+                        if (string.IsNullOrWhiteSpace(que.DaAnRange.GetUseFile()))
+                        {
+                            range.InsertAfter(daAnConfig.DaAnNullStr);
+                            range.InsertParagraphAfter();
+                        }
+                        else if (tiXin.IsXuanZhe)
+                        {
+                            range.InsertAfter(que.DaAnRange.HtmlText);
+                            range.InsertParagraphAfter();
+                        }
+                        else if (tiXin.IsPanDuan)
+                        {
+                            var t = bool.TryParse(que.DaAnRange.HtmlText, out bool t2);
+                            if (t)
+                            {
+                                var t3 = t2 ? daAnConfig.PanDuanTrueShowStr : daAnConfig.PanDuanFalseShowStr;
+                                range.InsertAfter(t3);
+                                range.InsertParagraphAfter();
+                            }
+                        }
+                        else
+                        {
+                            var upath = que.DaAnRange.GetUseFile();
+                            if (File.Exists(upath))
+                            {
+                                range = doc.RangeEnd();
+                                range.InsertFile(upath);
+                            }
+                            else
+                            {
+                                range.InsertParagraphAfter();
+                            }
+                        }
+                    }
+                    //添加解析
+                    if (daAnConfig.IsShowJieXi)
+                    {
+                        range.InsertAfter("解析:");
+
+                        var upath = que.JieXiRange.GetUseFile();
+                        if (File.Exists(upath))
+                        {
+                            range = doc.RangeEnd();
+                            range.InsertFile(upath);
+                        }
+                        else
+                        {
+                            range.InsertAfter(daAnConfig.JieXiNullStr);
+                            range.InsertParagraphAfter();
+                        }
+                    }
+                }
+            }
+        }
+        /// <summary>
+        /// 给答题卡添加定位
+        /// </summary>
+        /// <param name="Doc"></param>
+        private static void AddDtkDinWei(Document Doc)
+        {
+            var range = Doc.RangeEnd();
+            foreach (Section section in Doc.Sections)
+            {
+                #region 页眉
+                //页眉
+                range = section.Headers[WdHeaderFooterIndex.wdHeaderFooterPrimary].Range;
+                var headerTable = range.Tables.Add(range, 2, 3);
+                headerTable.Borders.InsideLineStyle = WdLineStyle.wdLineStyleNone;
+                headerTable.Borders.OutsideLineStyle = WdLineStyle.wdLineStyleNone;
+                //字体大小
+                headerTable.Range.Font.Size = 11;
+                //range.set_Style("正文");//ms office存在样式问题,使用set_Style解决
+                //样式对齐
+                foreach (Row row in headerTable.Rows)
+                {
+                    row.Cells.VerticalAlignment = WdCellVerticalAlignment.wdCellAlignVerticalBottom;
+                    foreach (Cell cell in row.Cells)
+                    {
+                        if (cell.ColumnIndex == 1)
+                            cell.Range.ParagraphFormat.Alignment = WdParagraphAlignment.wdAlignParagraphLeft;
+                        else if (cell.ColumnIndex == 2)
+                            cell.Range.ParagraphFormat.Alignment = WdParagraphAlignment.wdAlignParagraphCenter;
+                        else if (cell.ColumnIndex == 3)
+                            cell.Range.ParagraphFormat.Alignment = WdParagraphAlignment.wdAlignParagraphRight;
+                    }
+                }
+                //添加定位块
+                headerTable.Cell(1, 1).Range.Text = "       ■";
+                headerTable.Cell(2, 1).Range.Text = "▅";
+                headerTable.Cell(2, 2).Range.Text = "▅";
+                headerTable.Cell(2, 3).Range.Text = "▅     ▅";
+                //表格悬浮
+                headerTable.Rows.WrapAroundText = 1;
+                headerTable.Rows.RelativeVerticalPosition = WdRelativeVerticalPosition.wdRelativeVerticalPositionPage;
+                headerTable.Rows.VerticalPosition = Doc.PageSetup.TopMargin - 30;
+                //headerTable.Rows.HorizontalPosition = 90;
+                #endregion
+
+                #region 页脚
+                //页脚
+                range = section.Footers[WdHeaderFooterIndex.wdHeaderFooterPrimary].Range;
+                var footerTable = range.Tables.Add(range, 2, 3);
+                footerTable.Borders.InsideLineStyle = WdLineStyle.wdLineStyleNone;
+                footerTable.Borders.OutsideLineStyle = WdLineStyle.wdLineStyleNone;
+                //字体大小
+                footerTable.Range.Font.Size = 11;
+                //样式对齐
+                foreach (Row row in footerTable.Rows)
+                {
+                    row.Cells.VerticalAlignment = WdCellVerticalAlignment.wdCellAlignVerticalBottom;
+                    foreach (Cell cell in row.Cells)
+                    {
+                        if (cell.ColumnIndex == 1)
+                            cell.Range.ParagraphFormat.Alignment = WdParagraphAlignment.wdAlignParagraphLeft;
+                        else if (cell.ColumnIndex == 2)
+                            cell.Range.ParagraphFormat.Alignment = WdParagraphAlignment.wdAlignParagraphCenter;
+                        else if (cell.ColumnIndex == 3)
+                            cell.Range.ParagraphFormat.Alignment = WdParagraphAlignment.wdAlignParagraphRight;
+                    }
+                }
+                //添加定位块
+                footerTable.Cell(1, 1).Range.Text = "■";
+                footerTable.Cell(1, 2).Range.Text = "▅";
+                footerTable.Cell(1, 3).Range.Text = "▅     ▅";
+                footerTable.Cell(2, 1).Range.Text = "       ■";
+                //表格悬浮
+                footerTable.Rows.WrapAroundText = 1;
+                footerTable.Rows.RelativeVerticalPosition = WdRelativeVerticalPosition.wdRelativeVerticalPositionPage;
+                footerTable.Rows.VerticalPosition = Doc.PageSetup.PageHeight - Doc.PageSetup.BottomMargin;
+                //footerTable.Rows.HorizontalPosition = 90;
+                #endregion
+
+            }
+        }
+        /// <summary>
+        /// 给答题卡添加头部信息
+        /// </summary>
+        /// <param name="doc"></param>
+        /// <param name="daTiKaConfig"></param>
+        private static void AddDtkTou(Document doc, SwapDaTiKaConfig daTiKaConfig)
+        {
+            var range = doc.RangeEnd();
+            range.Font.Size = daTiKaConfig.WordFontSize;
+            var rangeFontSize = range.Font.Size;
+
+            //二维码区域
+            if (daTiKaConfig.IsErWeiMa)
+            {
+                int shu = 2;
+                int hen = (daTiKaConfig.ZhuBiaoTi == null || daTiKaConfig.FuBiaoTi == null) ? 1 : 2;
+                var table = doc.Tables.Add(range, hen, shu, WdDefaultTableBehavior.wdWord9TableBehavior, WdAutoFitBehavior.wdAutoFitFixed);
+                table.Borders.InsideLineStyle = WdLineStyle.wdLineStyleNone;
+                table.Borders.OutsideLineStyle = WdLineStyle.wdLineStyleNone;
+                //调整宽度
+                var tWidth = table.GetWidth();
+                table.Columns[1].Width = 0.3f * tWidth;
+                table.Columns[2].Width = 0.7f * tWidth;
+                if (hen == 1)
+                    table.Rows.Height = 50;
+                else
+                    table.Columns[1].Cells.Merge();
+                //虚线框
+                table.Columns[1].Borders.OutsideLineStyle = WdLineStyle.wdLineStyleDot;
+                table.Columns[1].Cells.VerticalAlignment = WdCellVerticalAlignment.wdCellAlignVerticalCenter;
+                table.Columns[2].Cells.VerticalAlignment = WdCellVerticalAlignment.wdCellAlignVerticalCenter;
+                //二维码粘贴区
+                var ewmRange = table.Cell(1, 1).Range;
+                ewmRange.Text = "二维码\r\n粘贴区";
+                ewmRange.ParagraphFormat.Alignment = WdParagraphAlignment.wdAlignParagraphCenter;
+                ewmRange.Font.Size = 12;
+                ewmRange.Font.Color = WdColor.wdColorGray50;
+
+                //主副标题
+                if ((daTiKaConfig.ZhuBiaoTi == null && daTiKaConfig.FuBiaoTi == null) || daTiKaConfig.ZhuBiaoTi != null)
+                {
+                    range = table.Cell(1, 2).Range;
+                    range.Text = daTiKaConfig.ZhuBiaoTi ?? "答题卡";
+                    range.ParagraphFormat.Alignment = WdParagraphAlignment.wdAlignParagraphCenter;
+                    range.Font.Size = 13;
+                    range.Font.Bold = -1;
+                }
+                if (hen == 2 && daTiKaConfig.FuBiaoTi != null)
+                {
+                    range = table.Cell(2, 2).Range;
+                    range.Text = daTiKaConfig.FuBiaoTi;
+                    range.ParagraphFormat.Alignment = WdParagraphAlignment.wdAlignParagraphCenter;
+                    range.Font.Size = 12;
+                    range.Font.Bold = 0;
+                }
+                range = doc.RangeEnd();
+                range.InsertParagraphAfter();
+            }
+            else
+            {
+                //主副标题
+                if (daTiKaConfig.ZhuBiaoTi != null)
+                {
+                    range = doc.RangeEnd();
+                    range.Text = daTiKaConfig.ZhuBiaoTi;
+                    range.ParagraphFormat.Alignment = WdParagraphAlignment.wdAlignParagraphCenter;
+                    range.Font.Size = 14;
+                    range.Font.Bold = -1;
+                    range.InsertParagraphAfter();
+                }
+                if (daTiKaConfig.FuBiaoTi != null)
+                {
+                    range = doc.RangeEnd();
+                    range.Text = daTiKaConfig.FuBiaoTi;
+                    range.ParagraphFormat.Alignment = WdParagraphAlignment.wdAlignParagraphCenter;
+                    range.Font.Size = 13;
+                    range.Font.Bold = 0;
+                    range.InsertParagraphAfter();
+                }
+            }
+
+            //还原
+            range = doc.RangeEnd();
+            range.ParagraphFormat.Alignment = WdParagraphAlignment.wdAlignParagraphLeft;
+            range.Font.Size = rangeFontSize;
+            range.Font.Bold = 0;
+
+            if (daTiKaConfig.DaTiKaType == DaTiKaStyle.机读卡)
+            {
+                //答题卡头
+                var table = doc.Tables.Add(range, 7, 3, WdDefaultTableBehavior.wdWord9TableBehavior, WdAutoFitBehavior.wdAutoFitFixed);
+                table.Rows.Height = 20;
+                table.Rows[1].Height = 25;
+                table.Rows[2].Height = 25;
+                //文本对齐方式
+                table.Columns[1].Cells.VerticalAlignment = WdCellVerticalAlignment.wdCellAlignVerticalCenter;
+                table.Columns[2].Cells.VerticalAlignment = WdCellVerticalAlignment.wdCellAlignVerticalCenter;
+                table.Columns[3].Cells.VerticalAlignment = WdCellVerticalAlignment.wdCellAlignVerticalCenter;
+                //表格宽度
+                var tWidth = table.GetWidth();
+                //调整列宽(按%)
+                if (daTiKaConfig.ZhiZhangDaXiao == PaperDaTiKa.A3横向3)
+                {
+                    table.Columns[1].Width = 0.15f * tWidth;
+                    table.Columns[2].Width = 0.26f * tWidth;
+                    table.Columns[3].Width = 0.59f * tWidth;
+                }
+                else if (daTiKaConfig.ZhiZhangDaXiao == PaperDaTiKa.A3横向)
+                {
+                    table.Columns[1].Width = 0.15f * tWidth;
+                    table.Columns[2].Width = 0.2f * tWidth;
+                    table.Columns[3].Width = 0.65f * tWidth;
+                }
+                else
+                {
+                    table.Columns[1].Width = 0.15f * tWidth;
+                    table.Columns[2].Width = 0.25f * tWidth;
+                    table.Columns[3].Width = 0.6f * tWidth;
+                }
+                //第三列合并
+                table.Columns[3].Cells.Merge();
+                //第七排合并
+                table.Cell(7, 1).Merge(table.Cell(7, 2));
+                //填充内容
+                table.Cell(1, 1).Range.Text = "姓    名:";
+                table.Cell(2, 1).Range.Text = "班    级:";
+                table.Cell(3, 1).Range.Text = "正确填涂:";
+                table.Cell(4, 1).Range.Text = "错误填涂:";
+                table.Cell(5, 1).Range.Text = "缺考标记:";
+                table.Cell(6, 1).Range.Text = "违纪标记:";
+                table.Cell(7, 1).Range.Text = "注意事项:\r\n" + daTiKaConfig.ZhuYiShiXian;
+                table.Cell(3, 2).Range.Text = daTiKaConfig.TianTuTrue;
+                table.Cell(3, 2).Range.Font.Size = 12;
+                table.Cell(4, 2).Range.Text = daTiKaConfig.TianTuFlase;
+                table.Cell(4, 2).Range.Font.Size = daTiKaConfig.TianTuFontSize;
+                table.Cell(5, 2).Range.Text = "[  ]";
+                table.Cell(5, 2).Range.Font.Size = daTiKaConfig.TianTuFontSize;
+                table.Cell(6, 2).Range.Text = "[  ]";
+                table.Cell(6, 2).Range.Font.Size = daTiKaConfig.TianTuFontSize;
+
+                //准考证号
+                var zkzhRange = table.Cell(1, 3).Range;
+                zkzhRange.Text = "准考证号";
+                zkzhRange.ParagraphFormat.Alignment = WdParagraphAlignment.wdAlignParagraphCenter;
+                zkzhRange.InsertParagraphAfter();
+                //新的范围
+                zkzhRange.SetRange(zkzhRange.End - 1, zkzhRange.End);
+                zkzhRange.Text = "1";//改变Range,预防在office中报错
+
+                //表格大小(A4)
+                int mixcount = 6;
+                if (daTiKaConfig.ZhiZhangDaXiao == PaperDaTiKa.A3横向)
+                    mixcount = 8;
+                else if (daTiKaConfig.ZhiZhangDaXiao == PaperDaTiKa.A3横向3)
+                    mixcount = 6;
+
+                var sjsl = daTiKaConfig.ZhunKaoZhenHaoShu < mixcount ? mixcount : daTiKaConfig.ZhunKaoZhenHaoShu;//实际数量
+                //表中表
+                table = doc.Tables.Add(zkzhRange, 2, sjsl, WdDefaultTableBehavior.wdWord9TableBehavior, WdAutoFitBehavior.wdAutoFitFixed);
+                table.Rows[1].Height = 20;
+                table.Rows[1].Cells.VerticalAlignment = WdCellVerticalAlignment.wdCellAlignVerticalCenter;
+                sjsl -= daTiKaConfig.ZhunKaoZhenHaoShu;
+                foreach (Cell cell in table.Rows[2].Cells)
+                {
+                    cell.Range.Font.Size = daTiKaConfig.TianTuFontSize;
+                    if (sjsl > 0)
+                    {
+                        table.Cell(1, cell.ColumnIndex).Range.Text = "/";
+                        cell.Range.Text = "[  ]\r\n[  ]\r\n[  ]\r\n[  ]\r\n[  ]\r\n[  ]\r\n[  ]\r\n[  ]\r\n[  ]\r\n[  ]";
+                        sjsl--;
+                    }
+                    else
+                        cell.Range.Text = "[ 0 ]\r\n[ 1 ]\r\n[ 2 ]\r\n[ 3 ]\r\n[ 4 ]\r\n[ 5 ]\r\n[ 6 ]\r\n[ 7 ]\r\n[ 8 ]\r\n[ 9 ]";
+                }
+            }
+            else
+            {
+                range = doc.RangeEnd();
+                range.Text = "姓名:___________ 班级:___________ 学号:___________";
+                range.ParagraphFormat.Alignment = WdParagraphAlignment.wdAlignParagraphCenter;
+                range.Font.Size = 10;
+                range.Font.Bold = 0;
+                range.InsertParagraphAfter();
+                //还原
+                range = doc.RangeEnd();
+                range.ParagraphFormat.Alignment = WdParagraphAlignment.wdAlignParagraphLeft;
+                range.Font.Size = rangeFontSize;
+                range.Font.Bold = 0;
+            }
+        }
+        /// <summary>
+        /// 添加答题区域的内容
+        /// </summary>
+        private static void AddDtkNeiRong(Document Doc, SwapDaTiKaConfig daTiKaConfig, SwapTiXins ques)
+        {
+            var rangeDtkEnd = Doc.RangeEnd();
+            foreach (var queTX in ques.TiXinList)
+            {
+                var fentxtxt = queTX.GetFenStr(daTiKaConfig.IsFen);
+                rangeDtkEnd = Doc.RangeEnd();
+                rangeDtkEnd.InsertParagraphAfter();
+                rangeDtkEnd.InsertAfter(queTX.NumberStr + queTX.TiXinStr + fentxtxt);
+                rangeDtkEnd.InsertParagraphAfter();
+                rangeDtkEnd = Doc.RangeEnd();
+
+                if (queTX.DaTiKaType == SwapTiXinDaTiKaType.不显示)
+                    continue;
+
+                //选择题,判断题
+                if (queTX.IsXuanZhe || queTX.IsPanDuan)
+                {
+                    rangeDtkEnd.Font.Size = daTiKaConfig.TianTuFontSize;
+                    if (daTiKaConfig.DaTiKaType == DaTiKaStyle.机读卡)
+                    {
+                        if (queTX.DaTiKaType == SwapTiXinDaTiKaType.竖向)
+                        {
+                            var lie = 15;
+                            if (daTiKaConfig.ZhiZhangDaXiao == PaperDaTiKa.A3横向)
+                                lie = 20;
+
+                            var hen = (int)Math.Ceiling((decimal)queTX.QueList.Count / (decimal)lie);
+
+                            var table = Doc.Tables.Add(rangeDtkEnd, hen, lie, WdDefaultTableBehavior.wdWord9TableBehavior, WdAutoFitBehavior.wdAutoFitFixed);
+                            foreach (Row item in table.Rows)
+                            {
+                                item.Cells.VerticalAlignment = WdCellVerticalAlignment.wdCellAlignVerticalCenter;
+                                foreach (Cell item2 in item.Cells)
+                                {
+                                    item2.Range.ParagraphFormat.Alignment = WdParagraphAlignment.wdAlignParagraphCenter;
+                                }
+                            }
+                            table.Borders.InsideLineStyle = WdLineStyle.wdLineStyleNone;
+                            table.AddText(queTX.GetDaTiKaJiDuKeGuanStr(true).ToArray());
+                        }
+                        else
+                        {
+                            var lie = 5;
+                            //if (daTiKaConfig.ZhiZhangDaXiao == PaperDaTiKa.A4竖向 && !queTX.IsPanDuan)
+                            //    lie = 5;
+                            if (daTiKaConfig.ZhiZhangDaXiao == PaperDaTiKa.A3横向3 && !queTX.IsPanDuan)
+                                lie = 4;
+
+                            var hen = (int)Math.Ceiling((decimal)queTX.QueList.Count / (decimal)lie);
+
+                            var table = Doc.Tables.Add(rangeDtkEnd, hen, lie, WdDefaultTableBehavior.wdWord9TableBehavior, WdAutoFitBehavior.wdAutoFitFixed);
+                            table.Borders.InsideLineStyle = WdLineStyle.wdLineStyleNone;
+                            table.AddText(queTX.GetDaTiKaJiDuKeGuanStr(false).ToArray());
+                        }
+                    }
+                    //人阅卡
+                    else
+                    {
+                        var lie = 5;
+                        var hen = (int)Math.Ceiling((decimal)queTX.QueList.Count / (decimal)lie);
+
+                        var table = Doc.Tables.Add(rangeDtkEnd, hen, lie, WdDefaultTableBehavior.wdWord9TableBehavior, WdAutoFitBehavior.wdAutoFitFixed);
+                        table.Borders.InsideLineStyle = WdLineStyle.wdLineStyleNone;
+                        table.AddText(queTX.GetDaTiKaRenYueKeGuanStr().ToArray());
+                    }
+                    rangeDtkEnd.SetRange(rangeDtkEnd.End - 1, rangeDtkEnd.End);
+                    rangeDtkEnd.Font.Size = daTiKaConfig.WordFontSize;
+                }
+                else
+                {
+                    if (queTX.DaTiKaType == SwapTiXinDaTiKaType.单排横线)
+                    {
+                        var table = Doc.Tables.Add(rangeDtkEnd, 1, 1, WdDefaultTableBehavior.wdWord9TableBehavior, WdAutoFitBehavior.wdAutoFitFixed);
+                        table.Borders.InsideLineStyle = WdLineStyle.wdLineStyleNone;
+
+                        var tableRange = table.Cell(1, 1).Range;
+                        tableRange.Underline = WdUnderline.wdUnderlineSingle;
+                        var kong1 = new string(' ', 60);
+                        foreach (var item in queTX.QueList)
+                        {
+                            var fenttxt = item.GetFenStr(string.IsNullOrEmpty(fentxtxt));
+                            tableRange.InsertAfter(item.NumberStr + fenttxt + kong1);
+                            if (item != queTX.QueList.Last())
+                            {
+                                tableRange.InsertParagraphAfter();
+                            }
+                        }
+                    }
+                    else if (queTX.DaTiKaType == SwapTiXinDaTiKaType.多排横线)
+                    {
+                        foreach (var que in queTX.QueList)
+                        {
+                            var fenttxt = que.GetFenStr(string.IsNullOrEmpty(fentxtxt));
+                            var table = Doc.Tables.Add(rangeDtkEnd, 1, 1, WdDefaultTableBehavior.wdWord9TableBehavior, WdAutoFitBehavior.wdAutoFitFixed);
+                            var tableRange = table.Cell(1, 1).Range;
+                            tableRange.Underline = WdUnderline.wdUnderlineSingle;//下划线
+                            var kong1 = new string(' ', 350);
+                            tableRange.Text = que.NumberStr + fenttxt + kong1 + ".";
+                        }
+                    }
+                    else if (queTX.DaTiKaType == SwapTiXinDaTiKaType.多排正方形网格)
+                    {
+                        foreach (var que in queTX.QueList)
+                        {
+                            var fenttxt = que.GetFenStr(string.IsNullOrEmpty(fentxtxt));
+                            rangeDtkEnd.Text = que.NumberStr + fenttxt;
+                            rangeDtkEnd.InsertParagraphAfter();
+                            rangeDtkEnd = Doc.RangeEnd();
+
+                            //A4纸
+                            var lie = 20;
+                            var hen = 10;
+                            var zhishu = "100字";
+
+                            if (daTiKaConfig.ZhiZhangDaXiao == PaperDaTiKa.A3横向3)
+                            {
+                                lie = 15;
+                                hen = 12;
+                                zhishu = "90字";
+                            }
+                            else if (daTiKaConfig.ZhiZhangDaXiao == PaperDaTiKa.A3横向)
+                            {
+                                lie = 25;
+                                hen = 8;
+                                zhishu = "100字";
+                            }
+
+                            var table = Doc.Tables.Add(rangeDtkEnd, hen, lie, WdDefaultTableBehavior.wdWord9TableBehavior, WdAutoFitBehavior.wdAutoFitFixed);
+                            table.Rows.Height = table.Columns.Width;
+                            foreach (Row item in table.Rows)
+                            {
+                                //偶数
+                                if (item.Index % 2 != 1)
+                                {
+                                    item.HeightRule = WdRowHeightRule.wdRowHeightExactly;
+                                    item.Height = 11.3f;
+                                    item.Cells.Merge();
+
+                                    if (item.Index == hen)
+                                    {
+                                        var cell1Range = item.Cells[1].Range;
+                                        cell1Range.Text = zhishu;
+                                        cell1Range.Font.Size = 6;
+                                        cell1Range.ParagraphFormat.Alignment = WdParagraphAlignment.wdAlignParagraphRight;
+                                    }
+                                }
+                            }
+                        }
+                    }
+                    else
+                    {
+                        foreach (var que in queTX.QueList)
+                        {
+                            var table = Doc.Tables.Add(rangeDtkEnd, 1, 1, WdDefaultTableBehavior.wdWord9TableBehavior, WdAutoFitBehavior.wdAutoFitFixed);
+                            table.Rows.Height = 120;
+
+                            var fenttxt = que.GetFenStr(string.IsNullOrEmpty(fentxtxt));
+                            rangeDtkEnd = table.Cell(1, 1).Range;
+                            rangeDtkEnd.Text = que.NumberStr + fenttxt;
+                            rangeDtkEnd = Doc.RangeEnd();
+                        }
+                    }
+                }
+            }
+        }
+        /// <summary>
+        /// 保存到指定文件夹
+        /// </summary>
+        /// <param name="saveDirectory">保存文件夹</param>
+        private static string DocSaveToDirectory(Document Doc, string saveDirectory)
+        {
+            Directory.CreateDirectory(saveDirectory);
+
+            var name = Path.Combine(saveDirectory, Path.GetRandomFileName().Replace(".", "") + ".docx");
+            Doc.SaveAs2(name, WdSaveFormat.wdFormatXMLDocument, AddToRecentFiles: false);
+            return name;
+        }
+
+    }
+}

+ 18 - 0
src/QuestionSwap/QuestionSwapDoc/SwapWordModel/DaTiKaStyle.cs

@@ -0,0 +1,18 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace QuestionSwapDoc.SwapWordModel
+{
+    public enum DaTiKaStyle
+    {
+        /// <summary>
+        /// 机读卡
+        /// </summary>
+        机读卡,
+        /// <summary>
+        /// 人阅卡
+        /// </summary>
+        人阅卡,
+    }
+}

+ 26 - 0
src/QuestionSwap/QuestionSwapDoc/SwapWordModel/PaperDaTiKa.cs

@@ -0,0 +1,26 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace QuestionSwapDoc.SwapWordModel
+{
+    public enum PaperDaTiKa
+    {
+        /// <summary>
+        /// A4竖向不分栏
+        /// </summary>
+        A4竖向,
+        /// <summary>
+        /// A3横向分两栏
+        /// </summary>
+        A3横向,
+        /// <summary>
+        /// A3横向分三栏
+        /// </summary>
+        A3横向3,
+        /// <summary>
+        /// 32开(未实现)
+        /// </summary>
+        K32,
+    }
+}

+ 21 - 0
src/QuestionSwap/QuestionSwapDoc/SwapWordModel/PaperQue.cs

@@ -0,0 +1,21 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace QuestionSwapDoc.SwapWordModel
+{
+    /// <summary>
+    /// 试卷纸张大小
+    /// </summary>
+    public enum PaperQue
+    {
+        /// <summary>
+        /// A4竖向不分栏
+        /// </summary>
+        A4竖向,
+        /// <summary>
+        /// A3横向分两栏
+        /// </summary>
+        A3横向,
+    }
+}

+ 48 - 0
src/QuestionSwap/QuestionSwapDoc/SwapWordModel/SwapDaAnConfig.cs

@@ -0,0 +1,48 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace QuestionSwapDoc.SwapWordModel
+{
+    /// <summary>
+    /// 答案解析配置
+    /// </summary>
+    public class SwapDaAnConfig
+    {
+        /// <summary>
+        /// 标题内容(为null不显示)
+        /// </summary>
+        public string Title { get; set; } = "《参考答案》";
+        /// <summary>
+        /// 是否答案解析和试卷在同一个文档中
+        /// </summary>
+        public bool IsInQue { get; set; } = true;
+        /// <summary>
+        /// 是否显示答案
+        /// </summary>
+        public bool IsShowDaAn { get; set; } = true;
+        /// <summary>
+        /// 是否显示解析
+        /// </summary>
+        public bool IsShowJieXi { get; set; } = true;
+
+
+        /// <summary>
+        /// 答案为空时代替的字符串
+        /// </summary>
+        public string DaAnNullStr { get; set; } = "略";
+        /// <summary>
+        /// 解析为空时代替的字符串
+        /// </summary>
+        public string JieXiNullStr { get; set; } = "略";
+        /// <summary>
+        /// 判断题正确时的答案显示字符串
+        /// </summary>
+        public string PanDuanTrueShowStr { get; set; } = "正确";
+        /// <summary>
+        /// 判断题错误时的答案显示字符串
+        /// </summary>
+        public string PanDuanFalseShowStr { get; set; } = "错误";
+
+    }
+}

+ 93 - 0
src/QuestionSwap/QuestionSwapDoc/SwapWordModel/SwapDaTiKaConfig.cs

@@ -0,0 +1,93 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace QuestionSwapDoc.SwapWordModel
+{
+    /// <summary>
+    /// 答题卡配置
+    /// </summary>
+    public class SwapDaTiKaConfig
+    {
+        /// <summary>
+        /// 正确填涂
+        /// </summary>
+        public readonly string TianTuTrue = "▄";
+        /// <summary>
+        /// 错误填涂
+        /// </summary>
+        public readonly string TianTuFlase = "[√]  [✘]  [●]  [▲]";
+        /// <summary>
+        /// 填涂区域字体大小
+        /// </summary>
+        public readonly float TianTuFontSize = 8;//建议8-9
+        /// <summary>
+        /// 文档主要字体大小
+        /// </summary>
+        public readonly float WordFontSize = 9;//建议9-10
+
+        /// <summary>
+        /// 主标题文本(为null不显示)(如:xx市xxxx学年高一上册质量检测答题卡)
+        /// </summary>
+        public string ZhuBiaoTi { get; set; } = null;
+        /// <summary>
+        /// 副标题文本(为null不显示)(如:数学必修1试卷)
+        /// </summary>
+        public string FuBiaoTi { get; set; } = null;
+        /// <summary>
+        /// 答题卡样式
+        /// </summary>
+        public DaTiKaStyle DaTiKaType { get; set; } = DaTiKaStyle.机读卡;
+        /// <summary>
+        /// 纸张大小
+        /// </summary>
+        public PaperDaTiKa ZhiZhangDaXiao { get; set; } = PaperDaTiKa.A4竖向;
+        /// <summary>
+        /// 注意事项
+        /// </summary>
+        public string ZhuYiShiXian { get; set; } = "填涂时用2B铅笔将选项涂满涂黑。修改时用橡皮擦干净。请注意题号顺序。请保持答题卡整洁,不要折叠、乱作标记。";
+        int ZhunKaoZhenHaoShu_ = 8;
+        /// <summary>
+        /// 准考证号的数量
+        /// A4竖向不分栏(1-10)A3横向分三栏(1-8)A3横向分两栏(1-14)
+        /// </summary>
+        public int ZhunKaoZhenHaoShu
+        {
+            get => ZhunKaoZhenHaoShu_;
+            set
+            {
+                if (value <= 0)
+                    ZhunKaoZhenHaoShu_ = 1;
+                else if (ZhiZhangDaXiao == PaperDaTiKa.A3横向 && value > 14)
+                    ZhunKaoZhenHaoShu_ = 14;
+                else if (ZhiZhangDaXiao == PaperDaTiKa.A4竖向 && value > 10)
+                    ZhunKaoZhenHaoShu_ = 10;
+                else if (ZhiZhangDaXiao == PaperDaTiKa.A3横向3 && value > 8)
+                    ZhunKaoZhenHaoShu_ = 8;
+                else
+                    ZhunKaoZhenHaoShu_ = value;
+            }
+        }
+        /// <summary>
+        /// 是否显示题型分值(如果分值不为空的情况下)
+        /// </summary>
+        public bool IsFen { get; set; } = true;
+        /// <summary>
+        /// 是否显示缺考标记
+        /// </summary>
+        public bool IsQueKao { get; } = true;
+        /// <summary>
+        /// 是否显示违规标记
+        /// </summary>
+        public bool IsWeiGui { get; } = true;
+        /// <summary>
+        /// 是否显示二维码区域
+        /// </summary>
+        public bool IsErWeiMa { get; set; } = true;
+        /// <summary>
+        /// 是否显示条形码区域
+        /// </summary>
+        public bool IsTiaoXinMa { get; } = false;
+
+    }
+}

+ 60 - 0
src/QuestionSwap/QuestionSwapDoc/SwapWordModel/SwapQue.cs

@@ -0,0 +1,60 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace QuestionSwapDoc.SwapWordModel
+{
+    /// <summary>
+    /// 试题
+    /// </summary>
+    public class SwapQue
+    {
+        /// <summary>
+        /// 编号文本(由方法生成的编号如:1.2.3.)
+        /// </summary>
+        public string NumberStr { get; set; } = null;
+
+
+
+        /// <summary>
+        /// 选择题选项的数量(机读卡将会使用,人阅卡不会使用)
+        /// </summary>
+        public int XuanZheTiCount { get; set; } = 0;
+        /// <summary>
+        /// 网页中的图片的左边的路径(和图片路径拼接成绝对路径)
+        /// </summary>
+        public string ImgLinkLeftPath { get; set; }
+        /// <summary>
+        /// 题的分值(为null不显示)
+        /// </summary>
+        public float? Fen { get; set; } = null;
+
+
+        /// <summary>
+        /// 题(题=题干+选项)
+        /// </summary>
+        public SwapQueContent TiRange { get; set; } = new SwapQueContent();
+        /// <summary>
+        /// 答案
+        /// </summary>
+        public SwapQueContent DaAnRange { get; set; } = new SwapQueContent();
+        /// <summary>
+        /// 解析
+        /// </summary>
+        public SwapQueContent JieXiRange { get; set; } = new SwapQueContent();
+
+        /// <summary>
+        /// 得到题上的分的文本
+        /// </summary>
+        /// <param name="isShow">是否显示此分(为false返回空字符串)</param>
+        /// <returns></returns>
+        public string GetFenStr(bool isShow = true)
+        {
+            if (!isShow || Fen == null || Fen <= 0)
+                return string.Empty;
+
+            return $"({Fen.Value}分)";
+        }
+
+    }
+}

+ 55 - 0
src/QuestionSwap/QuestionSwapDoc/SwapWordModel/SwapQueConfig.cs

@@ -0,0 +1,55 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace QuestionSwapDoc.SwapWordModel
+{
+    /// <summary>
+    /// 生成试卷需要的配置参数
+    /// </summary>
+    public class SwapQueConfig
+    {
+        /// <summary>
+        /// 主标题文本(为null不显示)(如:xx市xxxx学年高一上册质量检测)
+        /// </summary>
+        public string ZhuBiaoTi { get; set; } = null;
+        /// <summary>
+        /// 副标题文本(为null不显示)(如:数学必修1试卷)
+        /// </summary>
+        public string FuBiaoTi { get; set; } = null;
+        /// <summary>
+        /// 试题信息栏(为null不显示)(如:考试范围:xxx 考试时间:100分钟 命题人:xxx)
+        /// </summary>
+        public string ShiTiXinXi { get; set; } = null;
+        /// <summary>
+        /// 考生输入栏(为null不显示)(如:学校:___________ 姓名:___________ 班级:___________ 考号:___________)
+        /// </summary>
+        public string KaoShengShuRu { get; set; } = null;
+        /// <summary>
+        /// 注意事项栏(为null不显示)(如:注意事项:1、答题前填写好自己的姓名、班级、考号等信息 2、请将答案正确填写在答题卡上)
+        /// </summary>
+        public string ZhuYiShiXian { get; set; } = null;
+
+        /// <summary>
+        /// 是否分卷(将主观题分为一卷,客观题分为二卷)
+        /// </summary>
+        public bool IsFenJuan { get; set; } = false;
+        /// <summary>
+        /// 是否显示题型(题型例子:选择题,填空题...)
+        /// </summary>
+        public bool IsTiXin { get; set; } = true;
+        /// <summary>
+        /// 是否显示题型分值(如果分值不为空的情况下)
+        /// </summary>
+        public bool IsFen { get; set; } = true;
+
+        /// <summary>
+        /// 装订线信息
+        /// </summary>
+        public ZhuanDinXian ZhuanDinXian { get; set; } = new ZhuanDinXian();
+        /// <summary>
+        /// 纸张大小配置
+        /// </summary>
+        public PaperQue ZhiZhangDaXiao { get; set; } = PaperQue.A3横向;
+    }
+}

+ 91 - 0
src/QuestionSwap/QuestionSwapDoc/SwapWordModel/SwapQueContent.cs

@@ -0,0 +1,91 @@
+using HtmlAgilityPack;
+using QuestionSwapDoc.Help;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+
+namespace QuestionSwapDoc.SwapWordModel
+{
+    /// <summary>
+    /// 试题内容
+    /// </summary>
+    public class SwapQueContent
+    {
+        /// <summary>
+        /// 是否为空的内容
+        /// </summary>
+        public bool IsNull { get => string.IsNullOrWhiteSpace(XmlFile) && string.IsNullOrWhiteSpace(HtmlText); }
+
+        /// <summary>
+        /// xml文件绝对路径(优先使用此路径)
+        /// (注:如果‘XmlFile’和‘HtmlText’同时存在值将会优先使用‘XmlFile’放弃‘HtmlText’)
+        /// </summary>
+        public string XmlFile { get; set; }
+        /// <summary>
+        /// html文本(在网页端修改富文本后建议使用此参数)
+        /// (单选题、多选题为逗号分隔字符串,如"A,B,C")
+        /// (判断题为bool值,如"True")
+        /// (注:如果‘XmlFile’和‘HtmlText’同时存在值将会优先使用‘XmlFile’放弃‘HtmlText’)
+        /// </summary>
+        public string HtmlText { get; set; }
+
+
+
+        /// <summary>
+        /// html文本转为本地的文件并改为绝对路径
+        /// </summary>
+        public void HtmlTextToFile(string imgLinkLeftPath)
+        {
+            if (string.IsNullOrWhiteSpace(XmlFile) && !string.IsNullOrWhiteSpace(HtmlText))
+            {
+                string fileName = Path.GetRandomFileName().Replace(".", "") + ".html";
+                fileName = Path.GetFullPath(fileName);
+                File.WriteAllText(fileName, HttpHelp.AddLeftLinkPath(HtmlText, imgLinkLeftPath), Encoding.UTF8);
+                HtmlText = fileName;
+            }
+            else
+            {
+                HtmlText = string.Empty;
+            }
+        }
+        /// <summary>
+        /// html文件转为html文本内容
+        /// </summary>
+        public void HtmlFileToText()
+        {
+            if (string.IsNullOrWhiteSpace(XmlFile) && !string.IsNullOrWhiteSpace(HtmlText))
+            {
+                if (File.Exists(HtmlText))
+                {
+                    var hpath = HtmlText;
+                    HtmlText = File.ReadAllText(HtmlText);
+                    try
+                    {
+                        File.Delete(hpath);
+                    }
+                    catch { }
+                }
+            }
+            else
+            {
+                HtmlText = string.Empty;
+            }
+        }
+        /// <summary>
+        /// 得到使用的路径
+        /// </summary>
+        /// <param name="IsXuanZhe"></param>
+        /// <param name="IsPanDuan"></param>
+        /// <returns></returns>
+        public string GetUseFile()
+        {
+            if (!string.IsNullOrWhiteSpace(XmlFile))
+                return XmlFile;
+            else if (!string.IsNullOrWhiteSpace(HtmlText))
+                return HtmlText;
+            else
+                return string.Empty;
+        }
+    }
+}

+ 129 - 0
src/QuestionSwap/QuestionSwapDoc/SwapWordModel/SwapTiXin.cs

@@ -0,0 +1,129 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Text;
+
+namespace QuestionSwapDoc.SwapWordModel
+{
+    /// <summary>
+    /// 题型
+    /// </summary>
+    public class SwapTiXin
+    {
+        /// <summary>
+        /// 选项的支持的字母
+        /// </summary>
+        public readonly char[] XuanXianZhiMu = new char[] { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K' };
+        /// <summary>
+        /// 判断的支持的字符
+        /// </summary>
+        public readonly char[] PanDuanZhiFu = new char[] { '√', '×' };
+        /// <summary>
+        /// 编号文本(由方法生成的编号如:一、二、三、)
+        /// </summary>
+        public string NumberStr { get; set; } = null;
+
+
+
+        /// <summary>
+        /// 题型
+        /// </summary>
+        public string TiXinStr { get; set; }
+        /// <summary>
+        /// 是否为选择题题型(包括单选,多选)
+        /// </summary>
+        public bool IsXuanZhe { get; set; } = false;
+        /// <summary>
+        /// 是否为判断题题型
+        /// </summary>
+        public bool IsPanDuan { get; set; } = false;
+        /// <summary>
+        /// 此题型和题型下面的题在答题卡中的表现形式
+        /// </summary>
+        public SwapTiXinDaTiKaType DaTiKaType { get; set; } = SwapTiXinDaTiKaType.自动;
+        /// <summary>
+        /// 试题集合
+        /// </summary>
+        public List<SwapQue> QueList { get; set; } = new List<SwapQue>();
+
+
+
+        /// <summary>
+        /// 得到答题卡(机读卡)的客观题的填涂区域
+        /// </summary>
+        /// <param name="isErect">是否竖着排版</param>
+        /// <returns></returns>
+        public List<string> GetDaTiKaJiDuKeGuanStr(bool isErect)
+        {
+            if (!IsXuanZhe && !IsPanDuan)
+                return new List<string>();
+
+            List<string> strR = new List<string>(QueList.Count);
+            string jg = isErect ? "\r\n" : " ";
+
+            foreach (var item in QueList)
+            {
+                var num = item.NumberStr.Substring(0, item.NumberStr.Length - 1);
+                if (!isErect && num.Length == 1)
+                    num = " " + num;
+
+                var nums = new List<string>() { num };
+                var xztc = item.XuanZheTiCount <= 0 ? 4 : item.XuanZheTiCount;
+                var xx = IsXuanZhe ? XuanXianZhiMu.Take(xztc) : PanDuanZhiFu;
+                foreach (var item2 in xx)
+                {
+                    if (IsPanDuan)
+                        nums.Add($"[{item2}]");
+                    else
+                        nums.Add($"[ {item2} ]");
+                }
+                strR.Add(string.Join(jg, nums));
+            }
+            return strR;
+        }
+
+        /// <summary>
+        /// 得到答题卡(人阅卡)的客观题的填写区域
+        /// </summary>
+        /// <returns></returns>
+        public List<string> GetDaTiKaRenYueKeGuanStr()
+        {
+            if (!IsXuanZhe && !IsPanDuan)
+                return new List<string>();
+
+            List<string> strR = new List<string>(QueList.Count);
+            foreach (var item in QueList)
+            {
+                var num = item.NumberStr.Substring(0, item.NumberStr.Length - 1);
+                if (num.Length == 1)
+                    num = " " + num;
+
+                strR.Add($"{num}(     )");
+            }
+            return strR;
+        }
+
+        /// <summary>
+        /// 得到题型上的分的文本(仅限于下面的所有题分值一样的情况)
+        /// </summary>
+        /// <param name="isShow">是否显示此分(为false返回空字符串)</param>
+        /// <returns></returns>
+        public string GetFenStr(bool isShow = true)
+        {
+            if (!isShow || QueList == null || !QueList.Any() || QueList.Any(o => !o.Fen.HasValue || o.Fen <= 0))
+                return string.Empty;
+
+            var fens = QueList.Select(o => o.Fen).Distinct();
+            if (fens.Count() > 1)
+                return string.Empty;
+            else
+            {
+                if (QueList.Count == 1)
+                    return $"(共{fens.FirstOrDefault().Value}分)";
+                else
+                    return $"(每题{fens.FirstOrDefault().Value}分,共{QueList.Sum(o => o.Fen).Value}分)";
+            }
+        }
+    }
+}

+ 42 - 0
src/QuestionSwap/QuestionSwapDoc/SwapWordModel/SwapTiXinDaTiKaType.cs

@@ -0,0 +1,42 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace QuestionSwapDoc.SwapWordModel
+{
+    public enum SwapTiXinDaTiKaType
+    {
+        /// <summary>
+        /// 自动模式(选择判断为横向,其他题型为长方形空白框)
+        /// </summary>
+        自动 = 0,
+        /// <summary>
+        /// 选择判断为横向(只在机读卡中适用)
+        /// </summary>
+        横向 = 10,
+        /// <summary>
+        /// 选择判断为竖向(只在机读卡中适用)
+        /// </summary>
+        竖向,
+        /// <summary>
+        /// 非选择判断为长方形空白区域
+        /// </summary>
+        长方形空白框 = 20,
+        /// <summary>
+        /// 非选择判断为单排横线模式(合适填空题)
+        /// </summary>
+        单排横线,
+        /// <summary>
+        /// 非选择判断为多排横线模式(合适英文作文题)
+        /// </summary>
+        多排横线,
+        /// <summary>
+        /// 非选择判断为多排正方形网格模式(合适语文作文题)
+        /// </summary>
+        多排正方形网格,
+        /// <summary>
+        /// 不为题型下面的题添加答题区域
+        /// </summary>
+        不显示 = -1,
+    }
+}

+ 108 - 0
src/QuestionSwap/QuestionSwapDoc/SwapWordModel/SwapTiXins.cs

@@ -0,0 +1,108 @@
+using OfficeAppHelp.Help;
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Text;
+
+namespace QuestionSwapDoc.SwapWordModel
+{
+    /// <summary>
+    /// 题型集合
+    /// </summary>
+    public class SwapTiXins
+    {
+        /// <summary>
+        /// 题型集合
+        /// </summary>
+        public List<SwapTiXin> TiXinList { get; set; } = new List<SwapTiXin>();
+
+
+        /// <summary>
+        /// 更新编号
+        /// </summary>
+        public void UpdateNumber()
+        {
+            if (TiXinList == null)
+                return;
+
+            //题型,题
+            int txn = 1, tn = 1;
+            foreach (var item in TiXinList)
+            {
+                item.NumberStr = txn.ToCNString() + "、";
+
+                if (item.QueList != null)
+                {
+                    foreach (var item2 in item.QueList)
+                    {
+                        item2.NumberStr = tn.ToString() + ".";
+                        tn++;
+                    }
+                }
+                txn++;
+            }
+        }
+
+        /// <summary>
+        /// html转为文件绝对路径
+        /// </summary>
+        public void QueHtmlContentToFile()
+        {
+            foreach (var item in TiXinList)
+            {
+                foreach (var que in item.QueList)
+                {
+                    //题
+                    if (que.TiRange != null)
+                    {
+                        que.TiRange.HtmlTextToFile(que.ImgLinkLeftPath);
+                    }
+                    //答案
+                    if (que.DaAnRange != null)
+                    {
+                        //非选择判断
+                        if (!item.IsXuanZhe && !item.IsPanDuan)
+                            que.DaAnRange.HtmlTextToFile(que.ImgLinkLeftPath);
+                    }
+                    //解析
+                    if (que.JieXiRange != null)
+                    {
+                        que.JieXiRange.HtmlTextToFile(que.ImgLinkLeftPath);
+                    }
+                }
+            }
+        }
+
+        /// <summary>
+        /// html文件转为html内容文本
+        /// </summary>
+        public void QueHtmlFileToContent()
+        {
+            foreach (var item in TiXinList)
+            {
+                foreach (var que in item.QueList)
+                {
+                    //题
+                    if (que.TiRange != null)
+                    {
+                        que.TiRange.HtmlFileToText();
+                    }
+                    //答案
+                    if (que.DaAnRange != null)
+                    {
+                        //非选择判断
+                        if (!item.IsXuanZhe && !item.IsPanDuan)
+                            que.DaAnRange.HtmlFileToText();
+                    }
+                    //解析
+                    if (que.JieXiRange != null)
+                    {
+                        que.JieXiRange.HtmlFileToText();
+                    }
+                }
+            }
+        }
+
+    }
+}

+ 21 - 0
src/QuestionSwap/QuestionSwapDoc/SwapWordModel/SwapWordCreateQueResult.cs

@@ -0,0 +1,21 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace QuestionSwapDoc.SwapWordModel
+{
+    /// <summary>
+    /// 创建试卷、答案、解析、答题卡的结果
+    /// </summary>
+    public class SwapWordCreateQueResult
+    {
+        /// <summary>
+        /// 文档包含类型
+        /// </summary>
+        public SwapWordResultType resultType { get; set; }
+        /// <summary>
+        /// 文档的绝对路径
+        /// </summary>
+        public string Path { get; set; }
+    }
+}

+ 88 - 0
src/QuestionSwap/QuestionSwapDoc/SwapWordModel/SwapWordJoinQueItem.cs

@@ -0,0 +1,88 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace QuestionSwapDoc.SwapWordModel
+{
+    /// <summary>
+    /// 合并试题的项
+    /// </summary>
+    public class SwapWordJoinQueItem
+    {
+        /// <summary>
+        /// 有关于这个题的数据
+        /// </summary>
+        public string Data { get; set; }
+        /// <summary>
+        /// 题型
+        /// </summary>
+        public string TiXinStr { get; set; }
+        /// <summary>
+        /// 网页中的图片的左边的路径(和图片路径拼接成绝对路径)
+        /// </summary>
+        public string ImgLinkLeftPath { get; set; }
+        /// <summary>
+        /// 是否为选择题题型(包括单选,多选)
+        /// </summary>
+        public bool IsXuanZhe { get; set; } = false;
+        /// <summary>
+        /// 是否为判断题题型
+        /// </summary>
+        public bool IsPanDuan { get; set; } = false;
+
+        /// <summary>
+        /// 题(题=题干+选项)
+        /// </summary>
+        public SwapQueContent TiRange { get; set; } = new SwapQueContent();
+        /// <summary>
+        /// 答案
+        /// </summary>
+        public SwapQueContent DaAnRange { get; set; } = new SwapQueContent();
+        /// <summary>
+        /// 解析
+        /// </summary>
+        public SwapQueContent JieXiRange { get; set; } = new SwapQueContent();
+
+        /// <summary>
+        /// html转为文件绝对路径
+        /// </summary>
+        public static void QueHtmlContentToFile(List<SwapWordJoinQueItem> ques)
+        {
+            foreach (var que in ques)
+            {
+                //题
+                if (que.TiRange != null)
+                    que.TiRange.HtmlTextToFile(que.ImgLinkLeftPath);
+                //答案
+                if (que.DaAnRange != null)
+                    //非选择判断
+                    if (!que.IsXuanZhe && !que.IsPanDuan)
+                        que.DaAnRange.HtmlTextToFile(que.ImgLinkLeftPath);
+                //解析
+                if (que.JieXiRange != null)
+                    que.JieXiRange.HtmlTextToFile(que.ImgLinkLeftPath);
+            }
+        }
+
+        /// <summary>
+        /// html文件转为html内容文本
+        /// </summary>
+        public static void QueHtmlFileToContent(List<SwapWordJoinQueItem> ques)
+        {
+            foreach (var que in ques)
+            {
+                //题
+                if (que.TiRange != null)
+                    que.TiRange.HtmlFileToText();
+                //答案
+                if (que.DaAnRange != null)
+                    //非选择判断
+                    if (!que.IsXuanZhe && !que.IsPanDuan)
+                        que.DaAnRange.HtmlFileToText();
+                //解析
+                if (que.JieXiRange != null)
+                    que.JieXiRange.HtmlFileToText();
+            }
+        }
+    }
+}

+ 14 - 0
src/QuestionSwap/QuestionSwapDoc/SwapWordModel/SwapWordResultType.cs

@@ -0,0 +1,14 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace QuestionSwapDoc.SwapWordModel
+{
+    public enum SwapWordResultType
+    {
+        试卷,
+        试卷和答案解析,
+        答案解析,
+        答题卡
+    }
+}

+ 70 - 0
src/QuestionSwap/QuestionSwapDoc/SwapWordModel/ZhuanDinXian.cs

@@ -0,0 +1,70 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace QuestionSwapDoc.SwapWordModel
+{
+    /// <summary>
+    /// 装订线
+    /// </summary>
+    public class ZhuanDinXian
+    {
+        /// <summary>
+        /// 是否显示装订线
+        /// </summary>
+        public bool IsShow { get; set; } = false;
+        /// <summary>
+        /// 是否显示边框线
+        /// </summary>
+        public bool IsShowLine { get; set; } = true;
+        /// <summary>
+        /// 是否采用双面打印的方式来生成装订线
+        /// (单面打印在同一个方向,双面打印奇偶页将会在不同方向)
+        /// </summary>
+        public bool IsDuplexPrinting { get; set; } = true;
+        /// <summary>
+        /// 是否每一份新的纸张页显示书写内容
+        /// </summary>
+        public bool IsNewPageShowWriteText { get; set; } = false;
+        /// <summary>
+        /// 装订线书写内容
+        /// </summary>
+        public string WriteText { get; set; } = "学校:______________ 班级:______________ 姓名:______________ 学号:______________";
+        /// <summary>
+        /// 装订线分割线
+        /// </summary>
+        public char Split { get; set; } = '/';
+        /// <summary>
+        /// 装订线线内容(最好不要超过10个字符串)
+        /// </summary>
+        public string AlurText { get; set; } = "装订线内请不要答题";
+
+        /// <summary>
+        /// 得到装订线的分割内容
+        /// </summary>
+        /// <param name="strLength">装订线的分割内容长度</param>
+        /// <returns></returns>
+        public string GetAlurSplitText(int strLength = 150)
+        {
+            string alurSplitText = new string(Split, strLength);
+            if (string.IsNullOrEmpty(AlurText))
+                return alurSplitText;
+
+            //左边的开始位置
+            int leftInsert = (int)((double)strLength * 0.2);
+            //每个字符的间距
+            int charLength = (strLength - leftInsert * 2) / (AlurText.Length + 1);
+
+            leftInsert += charLength;
+            //当前个数
+            foreach (var item in AlurText)
+            {
+                alurSplitText = alurSplitText.Insert(leftInsert, item.ToString());
+                leftInsert += charLength + 1;
+            }
+
+            return alurSplitText;
+        }
+
+    }
+}

+ 28 - 0
src/QuestionSwap/QuestionSwapDoc/使用方法.txt

@@ -0,0 +1,28 @@
+环境:
+1.请在电脑上先安装wps
+2.电脑上如果安装有“wps”和“ms office”会优先调用“ms office”。(可以将wps打开然后最小化就默认调用的wps)
+3.一台计算机上一次性只能执行一个方法
+4.如果部署到IIS中需要有管理员组权限(应用程序池-选中应用-设置应用程序池默认设置-进程模型-标识-自定义账户-设置-填入管理员组账户或选择有管理员权限的组-确认)
+
+注意:
+1.所有路径都是绝对路径,请不要使用相对路径
+
+
+//解析试题
+var data = SwapQuestion.GetQue(@"D:\word.docx");
+
+//生成试卷、答案、解析、答题卡
+var data = SwapWord.CreateQue(@"D:\cs", queConfig, daAnConfig, daTiKaConfig, ques);
+//合并试题
+SwapWord.JoinQue(@"D:\cs\123.docx", swapWordJoinQueItems);
+//Html转为Word
+SwapWord.HtmlToWord(@"D:\cs\123.docx", htmlText, @"D:\iisWeb\222");
+//Word转Html
+var htmlText = SwapWord.WordToHtml(@"D:\cs\123.docx", @"D:\cs\123", @"123");
+
+
+
+生成模板:(引用项目OfficeOpenXmlHelp,此项目下的方法不需要走队列)
+WordOpenXml wordOpenXml = new WordOpenXml(@"D:\试题标记说明举例.docx", true);
+wordOpenXml.Replace("AAAAAAAAAA", "在教材人教版科目语文中可以使用的题型有:单选题,多选题,作文题");
+wordOpenXml.Save();

+ 10 - 0
src/YGNT.Exam.Application/AppConsts.cs

@@ -0,0 +1,10 @@
+namespace YGNT.Exam
+{
+    public class AppConsts
+    {
+        /// <summary>
+        /// Default pass phrase for SimpleStringCipher decrypt/encrypt operations
+        /// </summary>
+        public const string DefaultPassPhrase = "gsKxGZ012HLL3MI5";
+    }
+}

+ 64 - 0
src/YGNT.Exam.Application/Authorization/AbpLoginResultTypeHelper.cs

@@ -0,0 +1,64 @@
+using System;
+using Abp;
+using Abp.Authorization;
+using Abp.Dependency;
+using Abp.UI;
+
+namespace YGNT.Exam.Authorization
+{
+    public class AbpLoginResultTypeHelper : AbpServiceBase, ITransientDependency
+    {
+        public AbpLoginResultTypeHelper()
+        {
+            LocalizationSourceName = ExamConsts.LocalizationSourceName;
+        }
+
+        public Exception CreateExceptionForFailedLoginAttempt(AbpLoginResultType result, string usernameOrEmailAddress, string tenancyName)
+        {
+            switch (result)
+            {
+                case AbpLoginResultType.Success:
+                    return new Exception("Don't call this method with a success result!");
+                case AbpLoginResultType.InvalidUserNameOrEmailAddress:
+                case AbpLoginResultType.InvalidPassword:
+                    return new UserFriendlyException(L("LoginFailed"), L("InvalidUserNameOrPassword"));
+                case AbpLoginResultType.InvalidTenancyName:
+                    return new UserFriendlyException(L("LoginFailed"), L("ThereIsNoTenantDefinedWithName{0}", tenancyName));
+                case AbpLoginResultType.TenantIsNotActive:
+                    return new UserFriendlyException(L("LoginFailed"), L("TenantIsNotActive", tenancyName));
+                case AbpLoginResultType.UserIsNotActive:
+                    return new UserFriendlyException(L("LoginFailed"), L("UserIsNotActiveAndCanNotLogin", usernameOrEmailAddress));
+                case AbpLoginResultType.UserEmailIsNotConfirmed:
+                    return new UserFriendlyException(L("LoginFailed"), L("UserEmailIsNotConfirmedAndCanNotLogin"));
+                case AbpLoginResultType.LockedOut:
+                    return new UserFriendlyException(L("LoginFailed"), L("UserLockedOutMessage"));
+                default: // Can not fall to default actually. But other result types can be added in the future and we may forget to handle it
+                    Logger.Warn("Unhandled login fail reason: " + result);
+                    return new UserFriendlyException(L("LoginFailed"));
+            }
+        }
+
+        public string CreateLocalizedMessageForFailedLoginAttempt(AbpLoginResultType result, string usernameOrEmailAddress, string tenancyName)
+        {
+            switch (result)
+            {
+                case AbpLoginResultType.Success:
+                    throw new Exception("Don't call this method with a success result!");
+                case AbpLoginResultType.InvalidUserNameOrEmailAddress:
+                case AbpLoginResultType.InvalidPassword:
+                    return L("InvalidUserNameOrPassword");
+                case AbpLoginResultType.InvalidTenancyName:
+                    return L("ThereIsNoTenantDefinedWithName{0}", tenancyName);
+                case AbpLoginResultType.TenantIsNotActive:
+                    return L("TenantIsNotActive", tenancyName);
+                case AbpLoginResultType.UserIsNotActive:
+                    return L("UserIsNotActiveAndCanNotLogin", usernameOrEmailAddress);
+                case AbpLoginResultType.UserEmailIsNotConfirmed:
+                    return L("UserEmailIsNotConfirmedAndCanNotLogin");
+                default: // Can not fall to default actually. But other result types can be added in the future and we may forget to handle it
+                    Logger.Warn("Unhandled login fail reason: " + result);
+                    return L("LoginFailed");
+            }
+        }
+    }
+}

+ 65 - 0
src/YGNT.Exam.Application/Authorization/Accounts/AccountAppService.cs

@@ -0,0 +1,65 @@
+using System.Threading.Tasks;
+using Abp.Configuration;
+using Abp.Zero.Configuration;
+using YGNT.Exam.Authorization.Accounts.Dto;
+using YGNT.Exam.Authorization.Users;
+
+namespace YGNT.Exam.Authorization.Accounts
+{
+    /// <summary>
+    /// ÕË»§
+    /// </summary>
+    public class AccountAppService : ExamAppServiceBase, IAccountAppService
+    {
+        // from: http://regexlib.com/REDetails.aspx?regexp_id=1923
+        public const string PasswordRegex = "^[a-zA-Z0-9]{6,18}$";
+
+        private readonly UserRegistrationManager _userRegistrationManager;
+
+        public AccountAppService(
+            UserRegistrationManager userRegistrationManager)
+        {
+            _userRegistrationManager = userRegistrationManager;
+        }
+
+        public async Task<IsTenantAvailableOutput> IsTenantAvailable(IsTenantAvailableInput input)
+        {
+            var tenant = await TenantManager.FindByTenancyNameAsync(input.TenancyName);
+            if (tenant == null)
+            {
+                return new IsTenantAvailableOutput(TenantAvailabilityState.NotFound);
+            }
+
+            if (!tenant.IsActive)
+            {
+                return new IsTenantAvailableOutput(TenantAvailabilityState.InActive);
+            }
+
+            return new IsTenantAvailableOutput(TenantAvailabilityState.Available, tenant.Id);
+        }
+
+        /// <summary>
+        /// ×¢²áÕË»§
+        /// </summary>
+        /// <param name="input"></param>
+        /// <returns></returns>
+        public async Task<RegisterOutput> Register(RegisterInput input)
+        {
+            var user = await _userRegistrationManager.RegisterAsync(
+                input.Name,
+                input.Surname,
+                input.EmailAddress,
+                input.UserName,
+                input.Password,
+                true // Assumed email address is always confirmed. Change this if you want to implement email confirmation.
+            );
+
+            var isEmailConfirmationRequiredForLogin = await SettingManager.GetSettingValueAsync<bool>(AbpZeroSettingNames.UserManagement.IsEmailConfirmationRequiredForLogin);
+
+            return new RegisterOutput
+            {
+                CanLogin = user.IsActive && (user.IsEmailConfirmed || !isEmailConfirmationRequiredForLogin)
+            };
+        }
+    }
+}

+ 12 - 0
src/YGNT.Exam.Application/Authorization/Accounts/Dto/IsTenantAvailableInput.cs

@@ -0,0 +1,12 @@
+using System.ComponentModel.DataAnnotations;
+using Abp.MultiTenancy;
+
+namespace YGNT.Exam.Authorization.Accounts.Dto
+{
+    public class IsTenantAvailableInput
+    {
+        [Required]
+        [StringLength(AbpTenantBase.MaxTenancyNameLength)]
+        public string TenancyName { get; set; }
+    }
+}

+ 19 - 0
src/YGNT.Exam.Application/Authorization/Accounts/Dto/IsTenantAvailableOutput.cs

@@ -0,0 +1,19 @@
+namespace YGNT.Exam.Authorization.Accounts.Dto
+{
+    public class IsTenantAvailableOutput
+    {
+        public TenantAvailabilityState State { get; set; }
+
+        public int? TenantId { get; set; }
+
+        public IsTenantAvailableOutput()
+        {
+        }
+
+        public IsTenantAvailableOutput(TenantAvailabilityState state, int? tenantId = null)
+        {
+            State = state;
+            TenantId = tenantId;
+        }
+    }
+}

+ 51 - 0
src/YGNT.Exam.Application/Authorization/Accounts/Dto/RegisterInput.cs

@@ -0,0 +1,51 @@
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using Abp.Auditing;
+using Abp.Authorization.Users;
+using Abp.Extensions;
+using YGNT.Exam.Validation;
+
+namespace YGNT.Exam.Authorization.Accounts.Dto
+{
+    public class RegisterInput : IValidatableObject
+    {
+        [Required]
+        [StringLength(AbpUserBase.MaxNameLength)]
+        public string Name { get; set; }
+
+        [Required]
+        [StringLength(AbpUserBase.MaxSurnameLength)]
+        public string Surname { get; set; }
+
+        [Required]
+        [StringLength(AbpUserBase.MaxUserNameLength)]
+        public string UserName { get; set; }
+
+        [Required]
+        [EmailAddress]
+        [StringLength(AbpUserBase.MaxEmailAddressLength)]
+        public string EmailAddress { get; set; }
+
+        [Required]
+        [StringLength(AbpUserBase.MaxPlainPasswordLength)]
+        [DisableAuditing]
+        public string Password { get; set; }
+
+        /// <summary>
+        /// 验证码
+        /// </summary>
+        [DisableAuditing]
+        public string CaptchaResponse { get; set; }
+
+        public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
+        {
+            if (!UserName.IsNullOrEmpty())
+            {
+                if (!UserName.Equals(EmailAddress) && ValidationHelper.IsEmail(UserName))
+                {
+                    yield return new ValidationResult("Username cannot be an email address unless it's the same as your email address!");
+                }
+            }
+        }
+    }
+}

+ 7 - 0
src/YGNT.Exam.Application/Authorization/Accounts/Dto/RegisterOutput.cs

@@ -0,0 +1,7 @@
+namespace YGNT.Exam.Authorization.Accounts.Dto
+{
+    public class RegisterOutput
+    {
+        public bool CanLogin { get; set; }
+    }
+}

+ 9 - 0
src/YGNT.Exam.Application/Authorization/Accounts/Dto/TenantAvailabilityState.cs

@@ -0,0 +1,9 @@
+namespace YGNT.Exam.Authorization.Accounts.Dto
+{
+    public enum TenantAvailabilityState
+    {
+        Available = 1,
+        InActive,
+        NotFound
+    }
+}

+ 13 - 0
src/YGNT.Exam.Application/Authorization/Accounts/IAccountAppService.cs

@@ -0,0 +1,13 @@
+using System.Threading.Tasks;
+using Abp.Application.Services;
+using YGNT.Exam.Authorization.Accounts.Dto;
+
+namespace YGNT.Exam.Authorization.Accounts
+{
+    public interface IAccountAppService : IApplicationService
+    {
+        Task<IsTenantAvailableOutput> IsTenantAvailable(IsTenantAvailableInput input);
+
+        Task<RegisterOutput> Register(RegisterInput input);
+    }
+}

+ 16 - 0
src/YGNT.Exam.Application/Configuration/ConfigurationAppService.cs

@@ -0,0 +1,16 @@
+using System.Threading.Tasks;
+using Abp.Authorization;
+using Abp.Runtime.Session;
+using YGNT.Exam.Configuration.Dto;
+
+namespace YGNT.Exam.Configuration
+{
+    [AbpAuthorize]
+    public class ConfigurationAppService : ExamAppServiceBase, IConfigurationAppService
+    {
+        public async Task ChangeUiTheme(ChangeUiThemeInput input)
+        {
+            await SettingManager.ChangeSettingForUserAsync(AbpSession.ToUserIdentifier(), AppSettingNames.UiTheme, input.Theme);
+        }
+    }
+}

+ 11 - 0
src/YGNT.Exam.Application/Configuration/Dto/ChangeUiThemeInput.cs

@@ -0,0 +1,11 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace YGNT.Exam.Configuration.Dto
+{
+    public class ChangeUiThemeInput
+    {
+        [Required]
+        [StringLength(32)]
+        public string Theme { get; set; }
+    }
+}

+ 10 - 0
src/YGNT.Exam.Application/Configuration/IConfigurationAppService.cs

@@ -0,0 +1,10 @@
+using System.Threading.Tasks;
+using YGNT.Exam.Configuration.Dto;
+
+namespace YGNT.Exam.Configuration
+{
+    public interface IConfigurationAppService
+    {
+        Task ChangeUiTheme(ChangeUiThemeInput input);
+    }
+}

+ 14 - 0
src/YGNT.Exam.Application/Configuration/Ui/UiThemeInfo.cs

@@ -0,0 +1,14 @@
+namespace YGNT.Exam.Configuration.Ui
+{
+    public class UiThemeInfo
+    {
+        public string Name { get; }
+        public string CssClass { get; }
+
+        public UiThemeInfo(string name, string cssClass)
+        {
+            Name = name;
+            CssClass = cssClass;
+        }
+    }
+}

+ 36 - 0
src/YGNT.Exam.Application/Configuration/Ui/UiThemes.cs

@@ -0,0 +1,36 @@
+using System.Collections.Generic;
+
+namespace YGNT.Exam.Configuration.Ui
+{
+    public static class UiThemes
+    {
+        public static List<UiThemeInfo> All { get; }
+
+        static UiThemes()
+        {
+            All = new List<UiThemeInfo>
+            {
+                new UiThemeInfo("Red", "red"),
+                new UiThemeInfo("Pink", "pink"),
+                new UiThemeInfo("Purple", "purple"),
+                new UiThemeInfo("Deep Purple", "deep-purple"),
+                new UiThemeInfo("Indigo", "indigo"),
+                new UiThemeInfo("Blue", "blue"),
+                new UiThemeInfo("Light Blue", "light-blue"),
+                new UiThemeInfo("Cyan", "cyan"),
+                new UiThemeInfo("Teal", "teal"),
+                new UiThemeInfo("Green", "green"),
+                new UiThemeInfo("Light Green", "light-green"),
+                new UiThemeInfo("Lime", "lime"),
+                new UiThemeInfo("Yellow", "yellow"),
+                new UiThemeInfo("Amber", "amber"),
+                new UiThemeInfo("Orange", "orange"),
+                new UiThemeInfo("Deep Orange", "deep-orange"),
+                new UiThemeInfo("Brown", "brown"),
+                new UiThemeInfo("Grey", "grey"),
+                new UiThemeInfo("Blue Grey", "blue-grey"),
+                new UiThemeInfo("Black", "black")
+            };
+        }
+    }
+}

+ 47 - 0
src/YGNT.Exam.Application/ExamAppServiceBase.cs

@@ -0,0 +1,47 @@
+using System;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Identity;
+using Abp.Application.Services;
+using Abp.IdentityFramework;
+using Abp.Runtime.Session;
+using YGNT.Exam.Authorization.Users;
+using YGNT.Exam.MultiTenancy;
+
+namespace YGNT.Exam
+{
+    /// <summary>
+    /// Derive your application services from this class.
+    /// </summary>
+    public abstract class ExamAppServiceBase : ApplicationService
+    {
+        public TenantManager TenantManager { get; set; }
+
+        public UserManager UserManager { get; set; }
+
+        protected ExamAppServiceBase()
+        {
+            LocalizationSourceName = ExamConsts.LocalizationSourceName;
+        }
+
+        protected virtual async Task<User> GetCurrentUserAsync()
+        {
+            var user = await UserManager.FindByIdAsync(AbpSession.GetUserId().ToString());
+            if (user == null)
+            {
+                throw new Exception("There is no current user!");
+            }
+
+            return user;
+        }
+
+        protected virtual Task<Tenant> GetCurrentTenantAsync()
+        {
+            return TenantManager.GetByIdAsync(AbpSession.GetTenantId());
+        }
+
+        protected virtual void CheckErrors(IdentityResult identityResult)
+        {
+            identityResult.CheckErrors(LocalizationManager);
+        }
+    }
+}

+ 31 - 0
src/YGNT.Exam.Application/ExamApplicationModule.cs

@@ -0,0 +1,31 @@
+using Abp.AutoMapper;
+using Abp.Modules;
+using Abp.Reflection.Extensions;
+using YGNT.Exam.Authorization;
+using YGNT.QuestionLibrary;
+
+namespace YGNT.Exam
+{
+    [DependsOn(
+        typeof(ExamCoreModule),
+        typeof(AbpAutoMapperModule))]
+    public class ExamApplicationModule : AbpModule
+    {
+        public override void PreInitialize()
+        {
+            Configuration.Authorization.Providers.Add<ExamAuthorizationProvider>();
+        }
+
+        public override void Initialize()
+        {
+            var thisAssembly = typeof(ExamApplicationModule).GetAssembly();
+
+            IocManager.RegisterAssemblyByConvention(thisAssembly);
+
+            Configuration.Modules.AbpAutoMapper().Configurators.Add(
+                // Scan the assembly for classes which inherit from AutoMapper.Profile
+                cfg => cfg.AddMaps(thisAssembly)
+            );
+        }
+    }
+}

+ 59 - 0
src/YGNT.Exam.Application/Extension/ResultHelper.cs

@@ -0,0 +1,59 @@
+using System;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Threading.Tasks;
+using Abp.Application.Services.Dto;
+using Abp.Domain.Entities;
+using Abp.Linq.Extensions;
+using Microsoft.EntityFrameworkCore;
+
+namespace YGNT.Exam.Extension
+{
+    public static class ResultHelper
+    {
+        /// <summary>
+        /// 获取查询列表
+        /// </summary>
+        /// <typeparam name="TResult"></typeparam>
+        /// <typeparam name="TEntity"></typeparam>
+        /// <param name="queryable"></param>
+        /// <param name="search"></param>
+        /// <param name="select"></param>
+        /// <returns></returns>
+        public static async Task<PagedResultDto<TResult>> GetResultAsync<TResult, TEntity>(this IQueryable<TEntity> queryable, PagedResultRequestDto search, Expression<Func<TEntity, TResult>> select) where TEntity : IEntity
+        {
+            var result = new PagedResultDto<TResult>()
+            {
+                TotalCount = await queryable.CountAsync(),
+                Items = await queryable.PageBy(search.SkipCount, search.MaxResultCount)
+                    .Select(select)
+                   .ToListAsync()
+            };
+            return result;
+        }
+
+        /// <summary>
+        /// 获取查询列表
+        /// </summary>
+        /// <typeparam name="TResult"></typeparam>
+        /// <typeparam name="TEntity"></typeparam>
+        /// <typeparam name="TPrimaryKey"></typeparam>
+        /// <param name="queryable"></param>
+        /// <param name="search"></param>
+        /// <param name="select"></param>
+        /// <param name="primaryKey"></param>
+        /// <returns></returns>
+        public static async Task<PagedResultDto<TResult>> GetResultAsync<TResult, TEntity, TPrimaryKey>(this IQueryable<TEntity> queryable, PagedResultRequestDto search, Expression<Func<TEntity, TResult>> select, TPrimaryKey primaryKey)
+            where TEntity : IEntity<TPrimaryKey>
+        {
+            var result = new PagedResultDto<TResult>()
+            {
+                TotalCount = await queryable.CountAsync(),
+                Items = await queryable.PageBy(search.SkipCount, search.MaxResultCount)
+                    .Select(select)
+                    .ToListAsync()
+            };
+            return result;
+        }
+    }
+}

+ 23 - 0
src/YGNT.Exam.Application/Messages/Dto/UserMessageDto.cs

@@ -0,0 +1,23 @@
+using System;
+using Abp.Application.Services.Dto;
+
+namespace YGNT.Exam.Messages.Dto
+{
+    public class UserMessageDto : EntityDto<long>
+    {
+        /// <summary>
+        /// 信息
+        /// </summary>
+        public string Message { get; set; }
+
+        /// <summary>
+        /// 创建日期
+        /// </summary>
+        public DateTime CreationTime { get; set; }
+
+        /// <summary>
+        /// 消息类型
+        /// </summary>
+        public MessageType? MessageType { get; set; }
+    }
+}

+ 7 - 0
src/YGNT.Exam.Application/Messages/Dto/UserMessageInfoDto.cs

@@ -0,0 +1,7 @@
+namespace YGNT.Exam.Messages.Dto
+{
+    public class UserMessageInfoDto : UserMessageDto
+    {
+
+    }
+}

+ 22 - 0
src/YGNT.Exam.Application/Messages/Dto/UserMessageSearchInputDto.cs

@@ -0,0 +1,22 @@
+using YGNT.Infrastructure.Model;
+
+namespace YGNT.Exam.Messages.Dto
+{
+    public class UserMessageSearchInputDto : SearchBaseInputDto
+    {
+        /// <summary>
+        /// 是否已读 不传查询全部
+        /// </summary>
+        public bool? IsRead { get; set; }
+
+        /// <summary>
+        /// 消息类型
+        /// </summary>
+        public MessageType? MessageType { get; set; }
+
+        /// <summary>
+        /// 是否预览,预览模式将不会更新已读
+        /// </summary>
+        public bool IsPreview { get; set; } = true;
+    }
+}

+ 48 - 0
src/YGNT.Exam.Application/Messages/IUserMessageAppService.cs

@@ -0,0 +1,48 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Abp.Application.Services;
+using Abp.Application.Services.Dto;
+using YGNT.Exam.Messages.Dto;
+
+namespace YGNT.Exam.Messages
+{
+    /// <summary>
+    /// 用户消息
+    /// </summary>
+    public interface IUserMessageAppService : IApplicationService
+    {
+        /// <summary>
+        /// 获取用户未读消息数量
+        /// </summary>
+        /// <returns></returns>
+        Task<long> GetNotReadCountAsync();
+
+        /// <summary>
+        /// 获取用户消息
+        /// </summary>
+        /// <returns></returns>
+        Task<PagedResultDto<UserMessageDto>> GetUserMessageAsync(UserMessageSearchInputDto search);
+
+        /// <summary>
+        /// 删除消息
+        /// </summary>
+        /// <param name="id"></param>
+        /// <returns></returns>
+        Task DeleteAsync(long id);
+
+        /// <summary>
+        /// 删除消息
+        /// </summary>
+        /// <param name="idList"></param>
+        /// <returns></returns>
+        Task DeleteListAsync(List<long> idList);
+
+        /// <summary>
+        /// 读取消息
+        /// </summary>
+        /// <param name="id"></param>
+        /// <returns></returns>
+        Task<UserMessageInfoDto> ReadAsync(long id);
+
+    }
+}

+ 102 - 0
src/YGNT.Exam.Application/Messages/UserMessageAppService.cs

@@ -0,0 +1,102 @@
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
+using System.Threading.Tasks;
+using Abp.Application.Services.Dto;
+using Abp.Authorization;
+using Abp.Domain.Repositories;
+using Abp.Extensions;
+using Abp.Linq.Extensions;
+using Abp.Runtime.Session;
+using Mapster;
+using Microsoft.AspNetCore.Mvc;
+using YGNT.Exam.Messages.Dto;
+using YGNT.Infrastructure.Extension;
+using Z.EntityFramework.Plus;
+
+namespace YGNT.Exam.Messages
+{
+    [AbpAuthorize()]
+    public class UserMessageAppService : IUserMessageAppService
+    {
+        private readonly IRepository<UserMessage, long> _userMessageRepository;
+        public IAbpSession AbpSession { get; set; }
+
+        public UserMessageAppService(IRepository<UserMessage, long> userMessageRepository)
+        {
+            _userMessageRepository = userMessageRepository;
+            AbpSession = NullAbpSession.Instance;
+        }
+
+        /// <summary>
+        /// 获取用户未读消息数量
+        /// </summary>
+        /// <returns></returns>
+        public async Task<long> GetNotReadCountAsync()
+        {
+            var userId = AbpSession.GetUserId();
+            return await _userMessageRepository.CountAsync(x => x.UserId == userId
+                                                                && !x.IsRead);
+        }
+
+        /// <summary>
+        /// 获取用户消息
+        /// </summary>
+        /// <param name="search"></param>
+        /// <returns></returns>
+        public async Task<PagedResultDto<UserMessageDto>> GetUserMessageAsync(UserMessageSearchInputDto search)
+        {
+            var userId = AbpSession.GetUserId();
+            var queryable = _userMessageRepository.GetAll()
+                .WhereIf(!search.KeyWord.IsNullOrEmpty(), x => x.Message.Contains(search.KeyWord))
+                .WhereIf(search.MessageType.HasValue, x => x.MessageType == search.MessageType)
+                .WhereIf(search.IsRead.HasValue, x => x.IsRead == search.IsRead)
+                .Where(x => x.UserId == userId)
+                .OrderBy(x => x.IsRead)
+                .ThenByDescending(x => x.CreationTime);
+
+            var result = await queryable.GetResultAsync(search, x => x.Adapt<UserMessageDto>(), default(long));
+            if (!search.IsPreview)
+                await _userMessageRepository.GetAll()
+                    .Where(x => result.Items.Select(f => f.Id).Contains(x.Id))
+                    .UpdateAsync(x => new UserMessage() { IsRead = true });
+            return result;
+        }
+
+        /// <summary>
+        /// 删除消息
+        /// </summary>
+        /// <param name="id"></param>
+        /// <returns></returns>
+        public async Task DeleteAsync([Required] long id)
+        {
+            await _userMessageRepository.DeleteAsync(id);
+        }
+
+        /// <summary>
+        /// 删除消息
+        /// </summary>
+        /// <param name="idList"></param>
+        /// <returns></returns>
+        public async Task DeleteListAsync([Required] List<long> idList)
+        {
+            foreach (var id in idList)
+            {
+                await DeleteAsync(id);
+            }
+        }
+
+        /// <summary>
+        /// 读取消息
+        /// </summary>
+        /// <param name="id"></param>
+        /// <returns></returns>
+        [HttpGet]
+        public async Task<UserMessageInfoDto> ReadAsync([Required] long id)
+        {
+            var message = await _userMessageRepository.GetAsync(id);
+            message.IsRead = true;
+            return message.Adapt<UserMessageInfoDto>();
+        }
+    }
+}

+ 29 - 0
src/YGNT.Exam.Application/MultiTenancy/Dto/CreateTenantDto.cs

@@ -0,0 +1,29 @@
+using System.ComponentModel.DataAnnotations;
+using Abp.Authorization.Users;
+using Abp.AutoMapper;
+using Abp.MultiTenancy;
+
+namespace YGNT.Exam.MultiTenancy.Dto
+{
+    [AutoMapTo(typeof(Tenant))]
+    public class CreateTenantDto
+    {
+        [Required]
+        [StringLength(AbpTenantBase.MaxTenancyNameLength)]
+        [RegularExpression(AbpTenantBase.TenancyNameRegex)]
+        public string TenancyName { get; set; }
+
+        [Required]
+        [StringLength(AbpTenantBase.MaxNameLength)]
+        public string Name { get; set; }
+
+        [Required]
+        [StringLength(AbpUserBase.MaxEmailAddressLength)]
+        public string AdminEmailAddress { get; set; }
+
+        [StringLength(AbpTenantBase.MaxConnectionStringLength)]
+        public string ConnectionString { get; set; }
+
+        public bool IsActive {get; set;}
+    }
+}

+ 11 - 0
src/YGNT.Exam.Application/MultiTenancy/Dto/PagedTenantResultRequestDto.cs

@@ -0,0 +1,11 @@
+using Abp.Application.Services.Dto;
+
+namespace YGNT.Exam.MultiTenancy.Dto
+{
+    public class PagedTenantResultRequestDto : PagedResultRequestDto
+    {
+        public string Keyword { get; set; }
+        public bool? IsActive { get; set; }
+    }
+}
+

+ 22 - 0
src/YGNT.Exam.Application/MultiTenancy/Dto/TenantDto.cs

@@ -0,0 +1,22 @@
+using System.ComponentModel.DataAnnotations;
+using Abp.Application.Services.Dto;
+using Abp.AutoMapper;
+using Abp.MultiTenancy;
+
+namespace YGNT.Exam.MultiTenancy.Dto
+{
+    [AutoMapFrom(typeof(Tenant))]
+    public class TenantDto : EntityDto
+    {
+        [Required]
+        [StringLength(AbpTenantBase.MaxTenancyNameLength)]
+        [RegularExpression(AbpTenantBase.TenancyNameRegex)]
+        public string TenancyName { get; set; }
+
+        [Required]
+        [StringLength(AbpTenantBase.MaxNameLength)]
+        public string Name { get; set; }        
+        
+        public bool IsActive {get; set;}
+    }
+}

+ 10 - 0
src/YGNT.Exam.Application/MultiTenancy/ITenantAppService.cs

@@ -0,0 +1,10 @@
+using Abp.Application.Services;
+using YGNT.Exam.MultiTenancy.Dto;
+
+namespace YGNT.Exam.MultiTenancy
+{
+    public interface ITenantAppService : IAsyncCrudAppService<TenantDto, int, PagedTenantResultRequestDto, CreateTenantDto, TenantDto>
+    {
+    }
+}
+

+ 123 - 0
src/YGNT.Exam.Application/MultiTenancy/TenantAppService.cs

@@ -0,0 +1,123 @@
+using System.Linq;
+using System.Threading.Tasks;
+using Abp.Application.Services;
+using Abp.Application.Services.Dto;
+using Abp.Authorization;
+using Abp.Domain.Repositories;
+using Abp.Extensions;
+using Abp.IdentityFramework;
+using Abp.Linq.Extensions;
+using Abp.MultiTenancy;
+using Abp.Runtime.Security;
+using YGNT.Exam.Authorization;
+using YGNT.Exam.Authorization.Roles;
+using YGNT.Exam.Authorization.Users;
+using YGNT.Exam.Editions;
+using YGNT.Exam.MultiTenancy.Dto;
+using Microsoft.AspNetCore.Identity;
+
+namespace YGNT.Exam.MultiTenancy
+{
+    [AbpAuthorize(PermissionNames.Pages_Tenants)]
+    public class TenantAppService : AsyncCrudAppService<Tenant, TenantDto, int, PagedTenantResultRequestDto, CreateTenantDto, TenantDto>, ITenantAppService
+    {
+        private readonly TenantManager _tenantManager;
+        private readonly EditionManager _editionManager;
+        private readonly UserManager _userManager;
+        private readonly RoleManager _roleManager;
+        private readonly IAbpZeroDbMigrator _abpZeroDbMigrator;
+
+        public TenantAppService(
+            IRepository<Tenant, int> repository,
+            TenantManager tenantManager,
+            EditionManager editionManager,
+            UserManager userManager,
+            RoleManager roleManager,
+            IAbpZeroDbMigrator abpZeroDbMigrator)
+            : base(repository)
+        {
+            _tenantManager = tenantManager;
+            _editionManager = editionManager;
+            _userManager = userManager;
+            _roleManager = roleManager;
+            _abpZeroDbMigrator = abpZeroDbMigrator;
+        }
+
+        public override async Task<TenantDto> CreateAsync(CreateTenantDto input)
+        {
+            CheckCreatePermission();
+
+            // Create tenant
+            var tenant = ObjectMapper.Map<Tenant>(input);
+            tenant.ConnectionString = input.ConnectionString.IsNullOrEmpty()
+                ? null
+                : SimpleStringCipher.Instance.Encrypt(input.ConnectionString);
+
+            var defaultEdition = await _editionManager.FindByNameAsync(EditionManager.DefaultEditionName);
+            if (defaultEdition != null)
+            {
+                tenant.EditionId = defaultEdition.Id;
+            }
+
+            await _tenantManager.CreateAsync(tenant);
+            await CurrentUnitOfWork.SaveChangesAsync(); // To get new tenant's id.
+
+            // Create tenant database
+            _abpZeroDbMigrator.CreateOrMigrateForTenant(tenant);
+
+            // We are working entities of new tenant, so changing tenant filter
+            using (CurrentUnitOfWork.SetTenantId(tenant.Id))
+            {
+                // Create static roles for new tenant
+                CheckErrors(await _roleManager.CreateStaticRoles(tenant.Id));
+
+                await CurrentUnitOfWork.SaveChangesAsync(); // To get static role ids
+
+                // Grant all permissions to admin role
+                var adminRole = _roleManager.Roles.Single(r => r.Name == StaticRoleNames.Tenants.Admin);
+                await _roleManager.GrantAllPermissionsAsync(adminRole);
+
+                // Create admin user for the tenant
+                var adminUser = User.CreateTenantAdminUser(tenant.Id, input.AdminEmailAddress);
+                await _userManager.InitializeOptionsAsync(tenant.Id);
+                CheckErrors(await _userManager.CreateAsync(adminUser, User.DefaultPassword));
+                await CurrentUnitOfWork.SaveChangesAsync(); // To get admin user's id
+
+                // Assign admin user to role!
+                CheckErrors(await _userManager.AddToRoleAsync(adminUser, adminRole.Name));
+                await CurrentUnitOfWork.SaveChangesAsync();
+            }
+
+            return MapToEntityDto(tenant);
+        }
+
+        protected override IQueryable<Tenant> CreateFilteredQuery(PagedTenantResultRequestDto input)
+        {
+            return Repository.GetAll()
+                .WhereIf(!input.Keyword.IsNullOrWhiteSpace(), x => x.TenancyName.Contains(input.Keyword) || x.Name.Contains(input.Keyword))
+                .WhereIf(input.IsActive.HasValue, x => x.IsActive == input.IsActive);
+        }
+
+        protected override void MapToEntity(TenantDto updateInput, Tenant entity)
+        {
+            // Manually mapped since TenantDto contains non-editable properties too.
+            entity.Name = updateInput.Name;
+            entity.TenancyName = updateInput.TenancyName;
+            entity.IsActive = updateInput.IsActive;
+        }
+
+        public override async Task DeleteAsync(EntityDto<int> input)
+        {
+            CheckDeletePermission();
+
+            var tenant = await _tenantManager.GetByIdAsync(input.Id);
+            await _tenantManager.DeleteAsync(tenant);
+        }
+
+        private void CheckErrors(IdentityResult identityResult)
+        {
+            identityResult.CheckErrors(LocalizationManager);
+        }
+    }
+}
+

+ 311 - 0
src/YGNT.Exam.Application/Net/MimeTypes/MimeTypeNames.cs

@@ -0,0 +1,311 @@
+using System;
+
+namespace YGNT.Exam.Net.MimeTypes
+{
+    /* Copied from:
+     * http://stackoverflow.com/questions/10362140/asp-mvc-are-there-any-constants-for-the-default-content-types */
+
+    /// <summary>
+    /// Common mime types. 
+    /// </summary>
+    public static class MimeTypeNames
+    {
+        ///<summary>Used to denote the encoding necessary for files containing JavaScript source code. The alternative MIME type for this file type is text/javascript.</summary>
+        public const string ApplicationXJavascript = "application/x-javascript";
+
+        ///<summary>24bit Linear PCM audio at 8-48kHz, 1-N channels; Defined in RFC 3190</summary>
+        public const string AudioL24 = "audio/L24";
+
+        ///<summary>Adobe Flash files for example with the extension .swf</summary>
+        public const string ApplicationXShockwaveFlash = "application/x-shockwave-flash";
+
+        ///<summary>Arbitrary binary data.[5] Generally speaking this type identifies files that are not associated with a specific application. Contrary to past assumptions by software packages such as Apache this is not a type that should be applied to unknown files. In such a case, a server or application should not indicate a content type, as it may be incorrect, but rather, should omit the type in order to allow the recipient to guess the type.[6]</summary>
+        public const string ApplicationOctetStream = "application/octet-stream";
+
+        ///<summary>Atom feeds</summary>
+        public const string ApplicationAtomXml = "application/atom+xml";
+
+        ///<summary>Cascading Style Sheets; Defined in RFC 2318</summary>
+        public const string TextCss = "text/css";
+
+        ///<summary>commands; subtype resident in Gecko browsers like Firefox 3.5</summary>
+        public const string TextCmd = "text/cmd";
+
+        ///<summary>Comma-separated values; Defined in RFC 4180</summary>
+        public const string TextCsv = "text/csv";
+
+        ///<summary>deb (file format), a software package format used by the Debian project</summary>
+        public const string ApplicationXDeb = "application/x-deb";
+
+        ///<summary>Defined in RFC 1847</summary>
+        public const string MultipartEncrypted = "multipart/encrypted";
+
+        ///<summary>Defined in RFC 1847</summary>
+        public const string MultipartSigned = "multipart/signed";
+
+        ///<summary>Defined in RFC 2616</summary>
+        public const string MessageHttp = "message/http";
+
+        ///<summary>Defined in RFC 4735</summary>
+        public const string ModelExample = "model/example";
+
+        ///<summary>device-independent document in DVI format</summary>
+        public const string ApplicationXDvi = "application/x-dvi";
+
+        ///<summary>DTD files; Defined by RFC 3023</summary>
+        public const string ApplicationXmlDtd = "application/xml-dtd";
+
+        ///<summary>ECMAScript/JavaScript; Defined in RFC 4329 (equivalent to application/ecmascript but with looser processing rules) It is not accepted in IE 8 or earlier - text/javascript is accepted but it is defined as obsolete in RFC 4329. The "type" attribute of the <script> tag in HTML5 is optional and in practice omitting the media type of JavaScript programs is the most interoperable solution since all browsers have always assumed the correct default even before HTML5.</summary>
+        public const string ApplicationJavascript = "application/javascript";
+
+        ///<summary>ECMAScript/JavaScript; Defined in RFC 4329 (equivalent to application/javascript but with stricter processing rules)</summary>
+        public const string ApplicationEcmascript = "application/ecmascript";
+
+        ///<summary>EDI EDIFACT data; Defined in RFC 1767</summary>
+        public const string ApplicationEdifact = "application/EDIFACT";
+
+        ///<summary>EDI X12 data; Defined in RFC 1767</summary>
+        public const string ApplicationEdiX12 = "application/EDI-X12";
+
+        ///<summary>Email; Defined in RFC 2045 and RFC 2046</summary>
+        public const string MessagePartial = "message/partial";
+
+        ///<summary>Email; EML files, MIME files, MHT files, MHTML files; Defined in RFC 2045 and RFC 2046</summary>
+        public const string MessageRfc822 = "message/rfc822";
+
+        ///<summary>Extensible Markup Language; Defined in RFC 3023</summary>
+        public const string TextXml = "text/xml";
+
+        ///<summary>Flash video (FLV files)</summary>
+        public const string VideoXFlv = "video/x-flv";
+
+        ///<summary>GIF image; Defined in RFC 2045 and RFC 2046</summary>
+        public const string ImageGif = "image/gif";
+
+        ///<summary>GoogleWebToolkit data</summary>
+        public const string TextXGwtRpc = "text/x-gwt-rpc";
+
+        ///<summary>Gzip</summary>
+        public const string ApplicationXGzip = "application/x-gzip";
+
+        ///<summary>HTML; Defined in RFC 2854</summary>
+        public const string TextHtml = "text/html";
+
+        ///<summary>ICO image; Registered[9]</summary>
+        public const string ImageVndMicrosoftIcon = "image/vnd.microsoft.icon";
+
+        ///<summary>IGS files, IGES files; Defined in RFC 2077</summary>
+        public const string ModelIges = "model/iges";
+
+        ///<summary>IMDN Instant Message Disposition Notification; Defined in RFC 5438</summary>
+        public const string MessageImdnXml = "message/imdn+xml";
+
+        ///<summary>JavaScript Object Notation JSON; Defined in RFC 4627</summary>
+        public const string ApplicationJson = "application/json";
+
+        ///<summary>JavaScript Object Notation (JSON) Patch; Defined in RFC 6902</summary>
+        public const string ApplicationJsonPatch = "application/json-patch+json";
+
+        ///<summary>JavaScript - Defined in and obsoleted by RFC 4329 in order to discourage its usage in favor of application/javascript. However,text/javascript is allowed in HTML 4 and 5 and, unlike application/javascript, has cross-browser support. The "type" attribute of the <script> tag in HTML5 is optional and there is no need to use it at all since all browsers have always assumed the correct default (even in HTML 4 where it was required by the specification).</summary>
+        [Obsolete]
+        public const string TextJavascript = "text/javascript";
+
+        ///<summary>JPEG JFIF image; Associated with Internet Explorer; Listed in ms775147(v=vs.85) - Progressive JPEG, initiated before global browser support for progressive JPEGs (Microsoft and Firefox).</summary>
+        public const string ImagePjpeg = "image/pjpeg";
+
+        ///<summary>JPEG JFIF image; Defined in RFC 2045 and RFC 2046</summary>
+        public const string ImageJpeg = "image/jpeg";
+
+        ///<summary>jQuery template data</summary>
+        public const string TextXJqueryTmpl = "text/x-jquery-tmpl";
+
+        ///<summary>KML files (e.g. for Google Earth)</summary>
+        public const string ApplicationVndGoogleEarthKmlXml = "application/vnd.google-earth.kml+xml";
+
+        ///<summary>LaTeX files</summary>
+        public const string ApplicationXLatex = "application/x-latex";
+
+        ///<summary>Matroska open media format</summary>
+        public const string VideoXMatroska = "video/x-matroska";
+
+        ///<summary>Microsoft Excel 2007 files</summary>
+        public const string ApplicationVndOpenxmlformatsOfficedocumentSpreadsheetmlSheet = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
+
+        ///<summary>Microsoft Excel files</summary>
+        public const string ApplicationVndMsExcel = "application/vnd.ms-excel";
+
+        ///<summary>Microsoft Powerpoint 2007 files</summary>
+        public const string ApplicationVndOpenxmlformatsOfficedocumentPresentationmlPresentation = "application/vnd.openxmlformats-officedocument.presentationml.presentation";
+
+        ///<summary>Microsoft Powerpoint files</summary>
+        public const string ApplicationVndMsPowerpoint = "application/vnd.ms-powerpoint";
+
+        ///<summary>Microsoft Word 2007 files</summary>
+        public const string ApplicationVndOpenxmlformatsOfficedocumentWordprocessingmlDocument = "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
+
+        ///<summary>Microsoft Word files[15]</summary>
+        public const string ApplicationMsword = "application/msword";
+
+        ///<summary>MIME Email; Defined in RFC 2045 and RFC 2046</summary>
+        public const string MultipartAlternative = "multipart/alternative";
+
+        ///<summary>MIME Email; Defined in RFC 2045 and RFC 2046</summary>
+        public const string MultipartMixed = "multipart/mixed";
+
+        ///<summary>MIME Email; Defined in RFC 2387 and used by MHTML (HTML mail)</summary>
+        public const string MultipartRelated = "multipart/related";
+
+        ///<summary>MIME Webform; Defined in RFC 2388</summary>
+        public const string MultipartFormData = "multipart/form-data";
+
+        ///<summary>Mozilla XUL files</summary>
+        public const string ApplicationVndMozillaXulXml = "application/vnd.mozilla.xul+xml";
+
+        ///<summary>MP3 or other MPEG audio; Defined in RFC 3003</summary>
+        public const string AudioMpeg = "audio/mpeg";
+
+        ///<summary>MP4 audio</summary>
+        public const string AudioMp4 = "audio/mp4";
+
+        ///<summary>MP4 video; Defined in RFC 4337</summary>
+        public const string VideoMp4 = "video/mp4";
+
+        ///<summary>MPEG-1 video with multiplexed audio; Defined in RFC 2045 and RFC 2046</summary>
+        public const string VideoMpeg = "video/mpeg";
+
+        ///<summary>MSH files, MESH files; Defined in RFC 2077, SILO files</summary>
+        public const string ModelMesh = "model/mesh";
+
+        ///<summary>mulaw audio at 8 kHz, 1 channel; Defined in RFC 2046</summary>
+        public const string AudioBasic = "audio/basic";
+
+        ///<summary>Ogg Theora or other video (with audio); Defined in RFC 5334</summary>
+        public const string VideoOgg = "video/ogg";
+
+        ///<summary>Ogg Vorbis, Speex, Flac and other audio; Defined in RFC 5334</summary>
+        public const string AudioOgg = "audio/ogg";
+
+        ///<summary>Ogg, a multimedia bitstream container format; Defined in RFC 5334</summary>
+        public const string ApplicationOgg = "application/ogg";
+
+        ///<summary>OP</summary>
+        public const string ApplicationXopXml = "application/xop+xml";
+
+        ///<summary>OpenDocument Graphics; Registered[14]</summary>
+        public const string ApplicationVndOasisOpendocumentGraphics = "application/vnd.oasis.opendocument.graphics";
+
+        ///<summary>OpenDocument Presentation; Registered[13]</summary>
+        public const string ApplicationVndOasisOpendocumentPresentation = "application/vnd.oasis.opendocument.presentation";
+
+        ///<summary>OpenDocument Spreadsheet; Registered[12]</summary>
+        public const string ApplicationVndOasisOpendocumentSpreadsheet = "application/vnd.oasis.opendocument.spreadsheet";
+
+        ///<summary>OpenDocument Text; Registered[11]</summary>
+        public const string ApplicationVndOasisOpendocumentText = "application/vnd.oasis.opendocument.text";
+
+        ///<summary>p12 files</summary>
+        public const string ApplicationXPkcs12 = "application/x-pkcs12";
+
+        ///<summary>p7b and spc files</summary>
+        public const string ApplicationXPkcs7Certificates = "application/x-pkcs7-certificates";
+
+        ///<summary>p7c files</summary>
+        public const string ApplicationXPkcs7Mime = "application/x-pkcs7-mime";
+
+        ///<summary>p7r files</summary>
+        public const string ApplicationXPkcs7Certreqresp = "application/x-pkcs7-certreqresp";
+
+        ///<summary>p7s files</summary>
+        public const string ApplicationXPkcs7Signature = "application/x-pkcs7-signature";
+
+        ///<summary>Portable Document Format, PDF has been in use for document exchange on the Internet since 1993; Defined in RFC 3778</summary>
+        public const string ApplicationPdf = "application/pdf";
+
+        ///<summary>Portable Network Graphics; Registered,[8] Defined in RFC 2083</summary>
+        public const string ImagePng = "image/png";
+
+        ///<summary>PostScript; Defined in RFC 2046</summary>
+        public const string ApplicationPostscript = "application/postscript";
+
+        ///<summary>QuickTime video; Registered[10]</summary>
+        public const string VideoQuicktime = "video/quicktime";
+
+        ///<summary>RAR archive files</summary>
+        public const string ApplicationXRarCompressed = "application/x-rar-compressed";
+
+        ///<summary>RealAudio; Documented in RealPlayer Customer Support Answer 2559</summary>
+        public const string AudioVndRnRealaudio = "audio/vnd.rn-realaudio";
+
+        ///<summary>Resource Description Framework; Defined by RFC 3870</summary>
+        public const string ApplicationRdfXml = "application/rdf+xml";
+
+        ///<summary>RSS feeds</summary>
+        public const string ApplicationRssXml = "application/rss+xml";
+
+        ///<summary>SOAP; Defined by RFC 3902</summary>
+        public const string ApplicationSoapXml = "application/soap+xml";
+
+        ///<summary>StuffIt archive files</summary>
+        public const string ApplicationXStuffit = "application/x-stuffit";
+
+        ///<summary>SVG vector image; Defined in SVG Tiny 1.2 Specification Appendix M</summary>
+        public const string ImageSvgXml = "image/svg+xml";
+
+        ///<summary>Tag Image File Format (only for Baseline TIFF); Defined in RFC 3302</summary>
+        public const string ImageTiff = "image/tiff";
+
+        ///<summary>Tarball files</summary>
+        public const string ApplicationXTar = "application/x-tar";
+
+        ///<summary>Textual data; Defined in RFC 2046 and RFC 3676</summary>
+        public const string TextPlain = "text/plain";
+
+        ///<summary>TrueType Font No registered MIME type, but this is the most commonly used</summary>
+        public const string ApplicationXFontTtf = "application/x-font-ttf";
+
+        ///<summary>vCard (contact information); Defined in RFC 6350</summary>
+        public const string TextVcard = "text/vcard";
+
+        ///<summary>Vorbis encoded audio; Defined in RFC 5215</summary>
+        public const string AudioVorbis = "audio/vorbis";
+
+        ///<summary>WAV audio; Defined in RFC 2361</summary>
+        public const string AudioVndWave = "audio/vnd.wave";
+
+        ///<summary>Web Open Font Format; (candidate recommendation; use application/x-font-woff until standard is official)</summary>
+        public const string ApplicationFontWoff = "application/font-woff";
+
+        ///<summary>WebM Matroska-based open media format</summary>
+        public const string VideoWebm = "video/webm";
+
+        ///<summary>WebM open media format</summary>
+        public const string AudioWebm = "audio/webm";
+
+        ///<summary>Windows Media Audio Redirector; Documented in Microsoft help page</summary>
+        public const string AudioXMsWax = "audio/x-ms-wax";
+
+        ///<summary>Windows Media Audio; Documented in Microsoft KB 288102</summary>
+        public const string AudioXMsWma = "audio/x-ms-wma";
+
+        ///<summary>Windows Media Video; Documented in Microsoft KB 288102</summary>
+        public const string VideoXMsWmv = "video/x-ms-wmv";
+
+        ///<summary>WRL files, VRML files; Defined in RFC 2077</summary>
+        public const string ModelVrml = "model/vrml";
+
+        ///<summary>X3D ISO standard for representing 3D computer graphics, X3D XML files</summary>
+        public const string ModelX3DXml = "model/x3d+xml";
+
+        ///<summary>X3D ISO standard for representing 3D computer graphics, X3DB binary files</summary>
+        public const string ModelX3DBinary = "model/x3d+binary";
+
+        ///<summary>X3D ISO standard for representing 3D computer graphics, X3DV VRML files</summary>
+        public const string ModelX3DVrml = "model/x3d+vrml";
+
+        ///<summary>XHTML; Defined by RFC 3236</summary>
+        public const string ApplicationXhtmlXml = "application/xhtml+xml";
+
+        ///<summary>ZIP archive files; Registered[7]</summary>
+        public const string ApplicationZip = "application/zip";
+    }
+}

+ 18 - 0
src/YGNT.Exam.Application/Properties/AssemblyInfo.cs

@@ -0,0 +1,18 @@
+using System.Reflection;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("YGNT.Exam.Application")]
+[assembly: AssemblyTrademark("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components.  If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("3870c648-4aea-4b85-ba3f-f2f63b96136a")]

+ 40 - 0
src/YGNT.Exam.Application/Roles/Dto/CreateRoleDto.cs

@@ -0,0 +1,40 @@
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using Abp.Authorization.Roles;
+using YGNT.Exam.Authorization.Roles;
+
+namespace YGNT.Exam.Roles.Dto
+{
+    public class CreateRoleDto
+    {
+        /// <summary>
+        /// 角色名
+        /// </summary>
+        [Required]
+        [StringLength(AbpRoleBase.MaxNameLength)]
+        public string Name { get; set; }
+
+        /// <summary>
+        /// 显示名称
+        /// </summary>
+        [Required]
+        [StringLength(AbpRoleBase.MaxDisplayNameLength)]
+        public string DisplayName { get; set; }
+
+        /// <summary>
+        /// 标准化名称
+        /// </summary>
+        public string NormalizedName { get; set; }
+
+        /// <summary>
+        /// 描述
+        /// </summary>
+        [StringLength(Role.MaxDescriptionLength)]
+        public string Description { get; set; }
+
+        /// <summary>
+        /// 拥有的权限
+        /// </summary>
+        public List<string> GrantedPermissions { get; set; }
+    }
+}

+ 11 - 0
src/YGNT.Exam.Application/Roles/Dto/FlatPermissionDto.cs

@@ -0,0 +1,11 @@
+namespace YGNT.Exam.Roles.Dto
+{
+    public class FlatPermissionDto
+    {
+        public string Name { get; set; }
+        
+        public string DisplayName { get; set; }
+        
+        public string Description { get; set; }
+    }
+}

+ 13 - 0
src/YGNT.Exam.Application/Roles/Dto/GetRoleForEditOutput.cs

@@ -0,0 +1,13 @@
+using System.Collections.Generic;
+
+namespace YGNT.Exam.Roles.Dto
+{
+    public class GetRoleForEditOutput
+    {
+        public RoleEditDto Role { get; set; }
+
+        public List<FlatPermissionDto> Permissions { get; set; }
+
+        public List<string> GrantedPermissionNames { get; set; }
+    }
+}

+ 10 - 0
src/YGNT.Exam.Application/Roles/Dto/GetRolesInput.cs

@@ -0,0 +1,10 @@
+namespace YGNT.Exam.Roles.Dto
+{
+    public class GetRolesInput
+    {
+        /// <summary>
+        /// 权限
+        /// </summary>
+        public string Permission { get; set; }
+    }
+}

+ 10 - 0
src/YGNT.Exam.Application/Roles/Dto/PagedRoleResultRequestDto.cs

@@ -0,0 +1,10 @@
+using Abp.Application.Services.Dto;
+
+namespace YGNT.Exam.Roles.Dto
+{
+    public class PagedRoleResultRequestDto : PagedResultRequestDto
+    {
+        public string Keyword { get; set; }
+    }
+}
+

+ 25 - 0
src/YGNT.Exam.Application/Roles/Dto/PermissionDto.cs

@@ -0,0 +1,25 @@
+using Abp.Application.Services.Dto;
+using Abp.AutoMapper;
+using Abp.Authorization;
+
+namespace YGNT.Exam.Roles.Dto
+{
+    [AutoMapFrom(typeof(Permission))]
+    public class PermissionDto : EntityDto<long>
+    {
+        /// <summary>
+        /// ȨÏÞÃû³Æ
+        /// </summary>
+        public string Name { get; set; }
+
+        /// <summary>
+        /// ÏÔʾÃû³Æ
+        /// </summary>
+        public string DisplayName { get; set; }
+
+        /// <summary>
+        /// ÃèÊö
+        /// </summary>
+        public string Description { get; set; }
+    }
+}

+ 42 - 0
src/YGNT.Exam.Application/Roles/Dto/RoleDto.cs

@@ -0,0 +1,42 @@
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using Abp.Application.Services.Dto;
+using Abp.Authorization.Roles;
+using Abp.AutoMapper;
+using YGNT.Exam.Authorization.Roles;
+
+namespace YGNT.Exam.Roles.Dto
+{
+    public class RoleDto : EntityDto<int>
+    {
+        /// <summary>
+        /// 角色名
+        /// </summary>
+        [Required]
+        [StringLength(AbpRoleBase.MaxNameLength)]
+        public string Name { get; set; }
+
+        /// <summary>
+        /// 显示名称
+        /// </summary>
+        [Required]
+        [StringLength(AbpRoleBase.MaxDisplayNameLength)]
+        public string DisplayName { get; set; }
+
+        /// <summary>
+        /// 标准化名称
+        /// </summary>
+        public string NormalizedName { get; set; }
+
+        /// <summary>
+        /// 描述
+        /// </summary>
+        [StringLength(Role.MaxDescriptionLength)]
+        public string Description { get; set; }
+
+        /// <summary>
+        /// 拥有的权限
+        /// </summary>
+        public List<string> GrantedPermissions { get; set; }
+    }
+}

+ 35 - 0
src/YGNT.Exam.Application/Roles/Dto/RoleEditDto.cs

@@ -0,0 +1,35 @@
+using System.ComponentModel.DataAnnotations;
+using Abp.Application.Services.Dto;
+using Abp.Authorization.Roles;
+using YGNT.Exam.Authorization.Roles;
+
+namespace YGNT.Exam.Roles.Dto
+{
+    public class RoleEditDto : EntityDto<int>
+    {
+        /// <summary>
+        /// 角色名
+        /// </summary>
+        [Required]
+        [StringLength(AbpRoleBase.MaxNameLength)]
+        public string Name { get; set; }
+
+        /// <summary>
+        /// 显示名称
+        /// </summary>
+        [Required]
+        [StringLength(AbpRoleBase.MaxDisplayNameLength)]
+        public string DisplayName { get; set; }
+
+        /// <summary>
+        /// 描述
+        /// </summary>
+        [StringLength(Role.MaxDescriptionLength)]
+        public string Description { get; set; }
+
+        /// <summary>
+        /// 是否为系统固定角色
+        /// </summary>
+        public bool IsStatic { get; set; }
+    }
+}

+ 34 - 0
src/YGNT.Exam.Application/Roles/Dto/RoleListDto.cs

@@ -0,0 +1,34 @@
+using System;
+using Abp.Application.Services.Dto;
+using Abp.Domain.Entities.Auditing;
+
+namespace YGNT.Exam.Roles.Dto
+{
+    public class RoleListDto : EntityDto, IHasCreationTime
+    {
+        /// <summary>
+        /// 角色名
+        /// </summary>
+        public string Name { get; set; }
+
+        /// <summary>
+        /// 显示名称
+        /// </summary>
+        public string DisplayName { get; set; }
+
+        /// <summary>
+        /// 是否为系统固定角色
+        /// </summary>
+        public bool IsStatic { get; set; }
+
+        /// <summary>
+        /// 是否默认角色
+        /// </summary>
+        public bool IsDefault { get; set; }
+
+        /// <summary>
+        /// 创建时间
+        /// </summary>
+        public DateTime CreationTime { get; set; }
+    }
+}

+ 29 - 0
src/YGNT.Exam.Application/Roles/Dto/RoleMapProfile.cs

@@ -0,0 +1,29 @@
+using System.Linq;
+using AutoMapper;
+using Abp.Authorization;
+using Abp.Authorization.Roles;
+using YGNT.Exam.Authorization.Roles;
+
+namespace YGNT.Exam.Roles.Dto
+{
+    public class RoleMapProfile : Profile
+    {
+        public RoleMapProfile()
+        {
+            // Role and permission
+            CreateMap<Permission, string>().ConvertUsing(r => r.Name);
+            CreateMap<RolePermissionSetting, string>().ConvertUsing(r => r.Name);
+
+            CreateMap<CreateRoleDto, Role>();
+
+            CreateMap<RoleDto, Role>();
+
+            CreateMap<Role, RoleDto>().ForMember(x => x.GrantedPermissions,
+                opt => opt.MapFrom(x => x.Permissions.Where(p => p.IsGranted)));
+
+            CreateMap<Role, RoleListDto>();
+            CreateMap<Role, RoleEditDto>();
+            CreateMap<Permission, FlatPermissionDto>();
+        }
+    }
+}

+ 16 - 0
src/YGNT.Exam.Application/Roles/IRoleAppService.cs

@@ -0,0 +1,16 @@
+using System.Threading.Tasks;
+using Abp.Application.Services;
+using Abp.Application.Services.Dto;
+using YGNT.Exam.Roles.Dto;
+
+namespace YGNT.Exam.Roles
+{
+    public interface IRoleAppService : IAsyncCrudAppService<RoleDto, int, PagedRoleResultRequestDto, CreateRoleDto, RoleDto>
+    {
+        Task<ListResultDto<PermissionDto>> GetAllPermissions();
+
+        Task<GetRoleForEditOutput> GetRoleForEdit(EntityDto input);
+
+        Task<ListResultDto<RoleListDto>> GetRolesAsync(GetRolesInput input);
+    }
+}

+ 161 - 0
src/YGNT.Exam.Application/Roles/RoleAppService.cs

@@ -0,0 +1,161 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Abp.Application.Services;
+using Abp.Application.Services.Dto;
+using Abp.Authorization;
+using Abp.Domain.Repositories;
+using Abp.Extensions;
+using Abp.IdentityFramework;
+using Abp.Linq.Extensions;
+using Abp.UI;
+using YGNT.Exam.Authorization;
+using YGNT.Exam.Authorization.Roles;
+using YGNT.Exam.Authorization.Users;
+using YGNT.Exam.Roles.Dto;
+using Microsoft.AspNetCore.Identity;
+using Microsoft.EntityFrameworkCore;
+
+namespace YGNT.Exam.Roles
+{
+    /// <summary>
+    /// 角色
+    /// </summary>
+    [AbpAuthorize(PermissionNames.Pages_Roles)]
+    public class RoleAppService : AsyncCrudAppService<Role, RoleDto, int, PagedRoleResultRequestDto, CreateRoleDto, RoleDto>, IRoleAppService
+    {
+        private readonly RoleManager _roleManager;
+        private readonly UserManager _userManager;
+
+        public RoleAppService(IRepository<Role> repository, RoleManager roleManager, UserManager userManager)
+            : base(repository)
+        {
+            _roleManager = roleManager;
+            _userManager = userManager;
+        }
+
+        public override async Task<RoleDto> CreateAsync(CreateRoleDto input)
+        {
+            CheckCreatePermission();
+
+            var role = ObjectMapper.Map<Role>(input);
+            role.SetNormalizedName();
+
+            CheckErrors(await _roleManager.CreateAsync(role));
+
+            var grantedPermissions = PermissionManager
+                .GetAllPermissions()
+                .Where(p => input.GrantedPermissions.Contains(p.Name))
+                .ToList();
+
+            await _roleManager.SetGrantedPermissionsAsync(role, grantedPermissions);
+
+            return MapToEntityDto(role);
+        }
+
+        public async Task<ListResultDto<RoleListDto>> GetRolesAsync(GetRolesInput input)
+        {
+            var roles = await _roleManager
+                .Roles
+                .WhereIf(
+                    !input.Permission.IsNullOrWhiteSpace(),
+                    r => r.Permissions.Any(rp => rp.Name == input.Permission && rp.IsGranted)
+                )
+                .ToListAsync();
+
+            return new ListResultDto<RoleListDto>(ObjectMapper.Map<List<RoleListDto>>(roles));
+        }
+
+        public override async Task<RoleDto> UpdateAsync(RoleDto input)
+        {
+            CheckUpdatePermission();
+
+            var role = await _roleManager.GetRoleByIdAsync(input.Id);
+
+            if (role.IsStatic)
+                throw new UserFriendlyException("禁止修改固定角色权限");
+
+            ObjectMapper.Map(input, role);
+
+            CheckErrors(await _roleManager.UpdateAsync(role));
+
+            var grantedPermissions = PermissionManager
+                .GetAllPermissions()
+                .Where(p => input.GrantedPermissions.Contains(p.Name))
+                .ToList();
+
+            await _roleManager.SetGrantedPermissionsAsync(role, grantedPermissions);
+
+            return MapToEntityDto(role);
+        }
+
+        public override async Task DeleteAsync(EntityDto<int> input)
+        {
+            CheckDeletePermission();
+
+            var role = await _roleManager.FindByIdAsync(input.Id.ToString());
+            if (role.IsStatic)
+                throw new UserFriendlyException("禁止删除固定角色");
+
+            var users = await _userManager.GetUsersInRoleAsync(role.NormalizedName);
+
+            foreach (var user in users)
+            {
+                CheckErrors(await _userManager.RemoveFromRoleAsync(user, role.NormalizedName));
+            }
+
+            CheckErrors(await _roleManager.DeleteAsync(role));
+        }
+
+        /// <summary>
+        /// 获取所有权限
+        /// </summary>
+        public Task<ListResultDto<PermissionDto>> GetAllPermissions()
+        {
+            var permissions = PermissionManager.GetAllPermissions();
+
+            return Task.FromResult(new ListResultDto<PermissionDto>(
+                ObjectMapper.Map<List<PermissionDto>>(permissions).OrderBy(p => p.DisplayName).ToList()
+            ));
+        }
+
+        protected override IQueryable<Role> CreateFilteredQuery(PagedRoleResultRequestDto input)
+        {
+            return Repository.GetAllIncluding(x => x.Permissions)
+                .WhereIf(!input.Keyword.IsNullOrWhiteSpace(), x => x.Name.Contains(input.Keyword)
+                || x.DisplayName.Contains(input.Keyword)
+                || x.Description.Contains(input.Keyword));
+        }
+
+        protected override async Task<Role> GetEntityByIdAsync(int id)
+        {
+            return await Repository.GetAllIncluding(x => x.Permissions).FirstOrDefaultAsync(x => x.Id == id);
+        }
+
+        protected override IQueryable<Role> ApplySorting(IQueryable<Role> query, PagedRoleResultRequestDto input)
+        {
+            return query.OrderBy(r => r.DisplayName);
+        }
+
+        protected virtual void CheckErrors(IdentityResult identityResult)
+        {
+            identityResult.CheckErrors(LocalizationManager);
+        }
+
+        public async Task<GetRoleForEditOutput> GetRoleForEdit(EntityDto input)
+        {
+            var permissions = PermissionManager.GetAllPermissions();
+            var role = await _roleManager.GetRoleByIdAsync(input.Id);
+            var grantedPermissions = (await _roleManager.GetGrantedPermissionsAsync(role)).ToArray();
+            var roleEditDto = ObjectMapper.Map<RoleEditDto>(role);
+
+            return new GetRoleForEditOutput
+            {
+                Role = roleEditDto,
+                Permissions = ObjectMapper.Map<List<FlatPermissionDto>>(permissions).OrderBy(p => p.DisplayName).ToList(),
+                GrantedPermissionNames = grantedPermissions.Select(p => p.Name).ToList()
+            };
+        }
+    }
+}
+

+ 14 - 0
src/YGNT.Exam.Application/Sessions/Dto/ApplicationInfoDto.cs

@@ -0,0 +1,14 @@
+using System;
+using System.Collections.Generic;
+
+namespace YGNT.Exam.Sessions.Dto
+{
+    public class ApplicationInfoDto
+    {
+        public string Version { get; set; }
+
+        public DateTime ReleaseDate { get; set; }
+
+        public Dictionary<string, bool> Features { get; set; }
+    }
+}

+ 11 - 0
src/YGNT.Exam.Application/Sessions/Dto/GetCurrentLoginInformationsOutput.cs

@@ -0,0 +1,11 @@
+namespace YGNT.Exam.Sessions.Dto
+{
+    public class GetCurrentLoginInformationsOutput
+    {
+        public ApplicationInfoDto Application { get; set; }
+
+        public UserLoginInfoDto User { get; set; }
+
+        public TenantLoginInfoDto Tenant { get; set; }
+    }
+}

+ 14 - 0
src/YGNT.Exam.Application/Sessions/Dto/TenantLoginInfoDto.cs

@@ -0,0 +1,14 @@
+using Abp.Application.Services.Dto;
+using Abp.AutoMapper;
+using YGNT.Exam.MultiTenancy;
+
+namespace YGNT.Exam.Sessions.Dto
+{
+    [AutoMapFrom(typeof(Tenant))]
+    public class TenantLoginInfoDto : EntityDto
+    {
+        public string TenancyName { get; set; }
+
+        public string Name { get; set; }
+    }
+}

+ 18 - 0
src/YGNT.Exam.Application/Sessions/Dto/UserLoginInfoDto.cs

@@ -0,0 +1,18 @@
+using Abp.Application.Services.Dto;
+using Abp.AutoMapper;
+using YGNT.Exam.Authorization.Users;
+
+namespace YGNT.Exam.Sessions.Dto
+{
+    [AutoMapFrom(typeof(User))]
+    public class UserLoginInfoDto : EntityDto<long>
+    {
+        public string Name { get; set; }
+
+        public string Surname { get; set; }
+
+        public string UserName { get; set; }
+
+        public string EmailAddress { get; set; }
+    }
+}

+ 11 - 0
src/YGNT.Exam.Application/Sessions/ISessionAppService.cs

@@ -0,0 +1,11 @@
+using System.Threading.Tasks;
+using Abp.Application.Services;
+using YGNT.Exam.Sessions.Dto;
+
+namespace YGNT.Exam.Sessions
+{
+    public interface ISessionAppService : IApplicationService
+    {
+        Task<GetCurrentLoginInformationsOutput> GetCurrentLoginInformations();
+    }
+}

+ 39 - 0
src/YGNT.Exam.Application/Sessions/SessionAppService.cs

@@ -0,0 +1,39 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Abp.Auditing;
+using YGNT.Exam.Sessions.Dto;
+
+namespace YGNT.Exam.Sessions
+{
+    public class SessionAppService : ExamAppServiceBase, ISessionAppService
+    {
+        /// <summary>
+        /// 获取当前登录信息
+        /// </summary>
+        [DisableAuditing]
+        public async Task<GetCurrentLoginInformationsOutput> GetCurrentLoginInformations()
+        {
+            var output = new GetCurrentLoginInformationsOutput
+            {
+                Application = new ApplicationInfoDto
+                {
+                    Version = AppVersionHelper.Version,
+                    ReleaseDate = AppVersionHelper.ReleaseDate,
+                    Features = new Dictionary<string, bool>()
+                }
+            };
+
+            if (AbpSession.TenantId.HasValue)
+            {
+                output.Tenant = ObjectMapper.Map<TenantLoginInfoDto>(await GetCurrentTenantAsync());
+            }
+
+            if (AbpSession.UserId.HasValue)
+            {
+                output.User = ObjectMapper.Map<UserLoginInfoDto>(await GetCurrentUserAsync());
+            }
+
+            return output;
+        }
+    }
+}

+ 19 - 0
src/YGNT.Exam.Application/Users/Dto/ChangePasswordDto.cs

@@ -0,0 +1,19 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace YGNT.Exam.Users.Dto
+{
+    public class ChangePasswordDto
+    {
+        /// <summary>
+        /// 当前密码
+        /// </summary>
+        [Required]
+        public string CurrentPassword { get; set; }
+
+        /// <summary>
+        /// 新密码
+        /// </summary>
+        [Required]
+        public string NewPassword { get; set; }
+    }
+}

+ 10 - 0
src/YGNT.Exam.Application/Users/Dto/ChangeUserLanguageDto.cs

@@ -0,0 +1,10 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace YGNT.Exam.Users.Dto
+{
+    public class ChangeUserLanguageDto
+    {
+        [Required]
+        public string LanguageName { get; set; }
+    }
+}

+ 71 - 0
src/YGNT.Exam.Application/Users/Dto/CreateUserDto.cs

@@ -0,0 +1,71 @@
+using System.ComponentModel.DataAnnotations;
+using Abp.Auditing;
+using Abp.Authorization.Users;
+using Abp.AutoMapper;
+using Abp.Runtime.Validation;
+using YGNT.Exam.Authorization.Users;
+using YGNT.Infrastructure.Consts;
+
+namespace YGNT.Exam.Users.Dto
+{
+    [AutoMapTo(typeof(User))]
+    public class CreateUserDto : IShouldNormalize
+    {
+        /// <summary>
+        /// 用户名
+        /// </summary>
+        [Required]
+        [StringLength(AbpUserBase.MaxUserNameLength)]
+        public string UserName { get; set; }
+
+        /// <summary>
+        /// 名
+        /// </summary>
+        [Required]
+        [StringLength(AbpUserBase.MaxNameLength)]
+        public string Name { get; set; }
+
+        /// <summary>
+        /// 姓
+        /// </summary>
+        [Required]
+        [StringLength(AbpUserBase.MaxSurnameLength)]
+        public string Surname { get; set; }
+
+        /// <summary>
+        /// 手机号
+        /// </summary>
+        [Required]
+        [StringLength(AbpUserBase.MaxPhoneNumberLength)]
+        public string PhoneNumber { get; set; }
+
+        /// <summary>
+        /// 邮箱地址
+        /// </summary>
+        [EmailAddress]
+        [StringLength(AbpUserBase.MaxEmailAddressLength)]
+        public string EmailAddress { get; set; }
+
+        /// <summary>
+        /// 是否启用
+        /// </summary>
+        public bool IsActive { get; set; }
+
+        public string[] RoleNames { get; set; }
+
+        [Required]
+        [RegularExpression(RegexConsts.PasswordRegex,
+            ErrorMessage = "密码需要为6-18位")]
+        [StringLength(AbpUserBase.MaxPlainPasswordLength)]
+        [DisableAuditing]
+        public string Password { get; set; }
+
+        public void Normalize()
+        {
+            if (RoleNames == null)
+            {
+                RoleNames = new string[0];
+            }
+        }
+    }
+}

Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff