Browse Source

Merge branch 'master' of http://49.234.186.218:9000/root/ieplus

mingfu 1 day ago
parent
commit
0f90a76063
25 changed files with 1074 additions and 110 deletions
  1. 10 0
      back-ui/src/api/dz/papers.js
  2. 1 1
      back-ui/src/views/dz/cards/index.vue
  3. 3 2
      back-ui/src/views/dz/orders/index.vue
  4. 88 0
      back-ui/src/views/dz/papers/components/BatchYearSelect.vue
  5. 19 6
      back-ui/src/views/dz/papers/components/list-exact-hand.vue
  6. 16 6
      back-ui/src/views/dz/papers/components/list-exact-intelligent.vue
  7. 19 6
      back-ui/src/views/dz/papers/components/list-full-hand.vue
  8. 19 6
      back-ui/src/views/dz/papers/components/list-full-intelligent.vue
  9. 16 7
      back-ui/src/views/dz/papers/components/paper-exact-hand.vue
  10. 2 5
      back-ui/src/views/dz/papers/components/paper-exact-intelligent.vue
  11. 17 6
      back-ui/src/views/dz/papers/components/paper-full-hand.vue
  12. 17 7
      back-ui/src/views/dz/papers/components/paper-full-intelligent.vue
  13. 156 0
      back-ui/src/views/dz/papers/hooks/useBatchYear.js
  14. 29 1
      back-ui/src/views/dz/papers/hooks/usePaperBatchCondition.js
  15. 68 0
      back-ui/src/views/dz/papers/hooks/usePaperDownload.js
  16. 2 2
      back-ui/src/views/dz/papers/hooks/usePaperExactCondition.js
  17. 2 2
      back-ui/src/views/dz/papers/hooks/usePaperFullCondition.js
  18. 19 18
      back-ui/src/views/learn/questions/index.vue
  19. 10 1
      ie-admin/src/main/java/com/ruoyi/web/controller/learn/LearnPaperController.java
  20. 39 1
      ie-admin/src/main/java/com/ruoyi/web/service/CommService.java
  21. 7 2
      ie-common/pom.xml
  22. 0 1
      ie-common/src/main/java/com/ruoyi/common/utils/StringUtils.java
  23. 51 30
      ie-system/src/main/java/com/ruoyi/learn/service/impl/LearnQuestionsServiceImpl.java
  24. 180 0
      ie-system/src/main/java/com/ruoyi/system/utils/downloadpaper/DownloadPaperUtils.java
  25. 284 0
      ie-system/src/main/java/com/ruoyi/system/utils/downloadpaper/HttpUtils.java

+ 10 - 0
back-ui/src/api/dz/papers.js

@@ -694,4 +694,14 @@ export function getClassesBuildStatsDetail(params) {
         method: 'get',
         params
     })
+}
+
+/// 下载试卷
+export function downloadPaper(params) {
+    return request({
+        url: '/learn/paper/download',
+        method: 'get',
+        params,
+        responseType: 'blob'
+    })
 }

+ 1 - 1
back-ui/src/views/dz/cards/index.vue

@@ -175,7 +175,7 @@
         <svg-icon icon-class="chart" class="mr-1" style="font-size: 12px" />
         结算
       </CustomButton>
-      <CustomButton color="#009688" :disabled="batchDisabled" @click="handleRenew">
+      <CustomButton color="#009688" v-hasPermi="['dz:cards:renew']" :disabled="batchDisabled" @click="handleRenew">
         <svg-icon icon-class="time" class="mr-1" style="font-size: 14px" />
         续期
       </CustomButton>

+ 3 - 2
back-ui/src/views/dz/orders/index.vue

@@ -124,7 +124,6 @@
       <el-table-column type="selection" width="55" align="center" />
       <el-table-column label="ID" align="center" prop="id" />
 <!--      <el-table-column label="订单号" align="center" prop="code" />-->
-<!--      <el-table-column label="传微信单号" align="center" prop="outTradeNo" />-->
 <!--      <el-table-column label="二维码标识" align="center" prop="qrcodeId" />-->
 <!--      <el-table-column label="${comment}" align="center" prop="type" />-->
       <el-table-column label="卡号" align="center" prop="cardNo">
@@ -134,7 +133,7 @@
       </el-table-column>
       <el-table-column label="年份" align="center" prop="year" />
       <el-table-column label="手机号码" align="center" prop="phonenumber" />
-<!--      <el-table-column label="微信订单号" align="center" prop="transactionId" />-->
+      
       <el-table-column label="用户ID" align="center" prop="customerCode" />
 <!--      <el-table-column label="失效期" align="center" prop="outTime" width="180">-->
 <!--        <template #default="scope">-->
@@ -173,6 +172,8 @@
           <dict-tag :options="order_status" :value="scope.row.status" />
         </template>
       </el-table-column>
+      <el-table-column label="订单号" align="center" prop="outTradeNo" />
+      <el-table-column label="微信单号" align="center" prop="transactionId" />
 <!--      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">-->
 <!--        <template #default="scope">-->
 <!--          <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['dz:orders:edit']">修改</el-button>-->

+ 88 - 0
back-ui/src/views/dz/papers/components/BatchYearSelect.vue

@@ -0,0 +1,88 @@
+<template>
+    <el-form-item label="年份批次">
+        <div style="display: flex; gap: 8px; align-items: center;">
+            <!-- <el-select v-model="selectedYear" clearable placeholder="请选择年份" style="width: 80px">
+                <el-option v-for="year in yearList" :key="year" :label="year" :value="year"/>
+            </el-select> -->
+            <el-select v-model="localBatchId" clearable placeholder="请选择批次" style="width: 157px">
+                <el-option v-for="b in filteredBatchList" :key="b.batchId" :label="formatBatchName(b)" :value="b.batchId"/>
+            </el-select>
+            <el-button type="primary" @click="handleAddBatch">新增</el-button>
+        </div>
+    </el-form-item>
+    
+    <!-- 新增批次弹窗 -->
+    <el-dialog v-model="batchDialogVisible" title="新增批次" width="500px" append-to-body>
+        <el-form ref="batchFormRef" :model="batchForm" :rules="batchRules" label-width="80px">
+            <el-form-item label="年份" prop="year">
+                <el-input-number v-model="batchForm.year" :min="2000" :max="2100" placeholder="请输入年份" style="width: 100%"/>
+            </el-form-item>
+            <el-form-item label="批次" prop="name">
+                <el-input v-model="batchForm.name" placeholder="请输入批次名称" maxlength="100"/>
+            </el-form-item>
+        </el-form>
+        <template #footer>
+            <div class="dialog-footer">
+                <el-button @click="batchDialogVisible = false">取 消</el-button>
+                <el-button type="primary" @click="handleSaveBatch" :loading="batchSaving">保 存</el-button>
+            </div>
+        </template>
+    </el-dialog>
+</template>
+
+<script setup>
+import { ref, watch } from 'vue'
+import { useBatchYear } from '@/views/dz/papers/hooks/useBatchYear.js'
+
+const props = defineProps({
+    batchId: {
+        required: false,
+        default: null
+    },
+    batchList: {
+        required: false,
+        default: null
+    }
+})
+
+const emit = defineEmits(['update:batchId'])
+
+// 创建本地 ref,用于双向绑定
+// 注意:在 Vue 3 中,当父组件传递 ref 时,Vue 会自动解包,所以 props.batchId 是值,不是 ref
+const localBatchId = ref(props.batchId)
+const localBatchList = ref(Array.isArray(props.batchList) ? props.batchList : [])
+
+// 监听 props 变化,同步到本地 ref
+watch(() => props.batchId, (val) => {
+    if (localBatchId.value !== val) {
+        localBatchId.value = val
+    }
+}, { immediate: true })
+
+watch(() => props.batchList, (val) => {
+    const listValue = Array.isArray(val) ? val : []
+    localBatchList.value = listValue
+}, { immediate: true, deep: true })
+
+const {
+    selectedYear,
+    yearList,
+    filteredBatchList,
+    batchDialogVisible,
+    batchFormRef,
+    batchSaving,
+    batchForm,
+    batchRules,
+    handleAddBatch,
+    handleSaveBatch,
+    formatBatchName
+} = useBatchYear(localBatchId, localBatchList)
+
+// 监听 localBatchId 的变化,同步回父组件
+watch(localBatchId, (val) => {
+    emit('update:batchId', val)
+})
+</script>
+
+<style scoped>
+</style>

+ 19 - 6
back-ui/src/views/dz/papers/components/list-exact-hand.vue

@@ -2,7 +2,7 @@
     <el-form ref="queryRef" :model="queryParams" :rules="rules" label-width="68px" inline>
         <el-form-item label="批次" prop="batchId">
             <el-select v-model="batchId" clearable @change="handleQuery" style="width: 172px">
-                <el-option v-for="b in batchList" :label="b.name" :value="b.batchId"/>
+                <el-option v-for="b in batchList" :label="formatBatchName(b)" :value="b.batchId"/>
             </el-select>
         </el-form-item>
         <el-form-item label="考生类型" prop="examType">
@@ -35,7 +35,11 @@
 <!--            <el-button icon="Refresh" @click="resetQuery">重置</el-button>-->
         </el-form-item>
     </el-form>
-    <Table :data="list" :columns="columns" :actions="actions" @get-list="getList" @action="handleAction" />
+    <Table :data="list" :columns="columns" :actions="actions" @get-list="getList" @action="handleAction">
+        <template #batchName="{row}">
+            <span>{{ getBatchDisplayName(row) }}</span>
+        </template>
+    </Table>
     <el-drawer v-model="drawer" title="班级详情" size="1000px">
         <class-detail v-if="drawer" :data="drawerObj" exact-mode />
     </el-drawer>
@@ -45,18 +49,20 @@
 import consts from "@/utils/consts.js";
 import {useProvidePaperList} from "@/views/dz/papers/hooks/usePaperList.js";
 import {useProvidePaperExactCondition} from "@/views/dz/papers/hooks/usePaperExactCondition.js";
+import {usePaperDownload} from "@/views/dz/papers/hooks/usePaperDownload.js";
 import Table from "@/components/Table/index.vue";
 import ClassDetail from "@/views/dz/papers/components/plugs/class-detail.vue";
 
 const columns = [
     {label: '组卷类型', prop: 'buildTypeName'},
     {label: '班级', prop: 'className'},
-    {label: '批次', prop: 'batchName'},
+    {label: '批次', prop: 'batchName', type: 'slot', slotName: 'batchName'},
     {label: '班级人数', prop: 'total'},
     ...consts.config.exactColumns.slice(0, 3)
 ]
 const actions = [
-    {label: '查看详情', key: 'detail', permission: ['']}
+    {label: '详情', key: 'detail', permission: ['learn:test:list']},
+    // {label: '下载', key: 'download', permission: ['learn:paper:download']}
 ]
 
 const drawer = ref(false)
@@ -73,7 +79,9 @@ const {
     majorGroups,
     majorPlanId,
     majors,
-    conditionArgs
+    conditionArgs,
+    formatBatchName,
+    getBatchDisplayName
 } = useProvidePaperExactCondition(type)
 const classId = ref('')
 const queryParams = computed(() => ({
@@ -81,11 +89,16 @@ const queryParams = computed(() => ({
     classId: toValue(classId)
 }))
 const {rules, handleQuery, resetQuery, list, total, getList, classList} = useProvidePaperList(queryParams)
+const {handleDownload} = usePaperDownload(queryParams)
 
-const handleAction = function (action, row) {
+const handleAction = async function (action, row) {
     switch (action.key) {
         case "detail":
             drawer.value = true
+            drawerObj.value = row
+            break;
+        case "download":
+            await handleDownload(row)
             break;
         default:
             throw new Error(`Action key '${action.key}' not support.`)

+ 16 - 6
back-ui/src/views/dz/papers/components/list-exact-intelligent.vue

@@ -2,7 +2,7 @@
     <el-form ref="queryRef" :model="queryParams" :rules="rules" label-width="68px" inline>
         <el-form-item label="批次" prop="batchId">
             <el-select v-model="batchId" clearable @change="handleQuery" style="width: 172px">
-                <el-option v-for="b in batchList" :label="b.name" :value="b.batchId"/>
+                <el-option v-for="b in batchList" :label="formatBatchName(b)" :value="b.batchId"/>
             </el-select>
         </el-form-item>
         <el-form-item label="班级" prop="classId">
@@ -15,7 +15,11 @@
 <!--            <el-button icon="Refresh" @click="resetQuery">重置</el-button>-->
         </el-form-item>
     </el-form>
-    <Table :data="list" :columns="columns" :actions="actions" @get-list="getList" @action="handleAction" />
+    <Table :data="list" :columns="columns" :actions="actions" @get-list="getList" @action="handleAction">
+        <template #batchName="{row}">
+            <span>{{ getBatchDisplayName(row) }}</span>
+        </template>
+    </Table>
     <el-drawer v-model="drawer" title="班级详情" size="1000px">
         <class-detail v-if="drawer" :data="drawerObj" exact-mode />
     </el-drawer>
@@ -26,34 +30,40 @@
 import consts from "@/utils/consts.js";
 import {useProvidePaperBatchCondition} from "@/views/dz/papers/hooks/usePaperBatchCondition.js";
 import {useProvidePaperList} from "@/views/dz/papers/hooks/usePaperList.js";
+import {usePaperDownload} from "@/views/dz/papers/hooks/usePaperDownload.js";
 import Table from "@/components/Table/index.vue"
 import ClassDetail from "@/views/dz/papers/components/plugs/class-detail.vue";
 
 const columns = [
     {label: '组卷类型', prop: 'buildTypeName'},
     {label: '班级', prop: 'className'},
-    {label: '批次', prop: 'batchName'},
+    {label: '批次', prop: 'batchName', type: 'slot', slotName: 'batchName'},
     {label: '班级人数', prop: 'total'},
     ...consts.config.exactColumns
 ]
 const actions = [
-    {label: '查看详情', key: 'detail', permission: ['learn:test:list']}
+    {label: '详情', key: 'detail', permission: ['learn:test:list']},
+    // {label: '下载', key: 'download', permission: ['learn:paper:download']}
 ]
 
 const drawer = ref(false)
 const drawerObj = ref(null)
 const type = consts.enums.buildType.ExactIntelligent
-const {batchList, batchId} = useProvidePaperBatchCondition(type)
+const {batchList, batchId, formatBatchName, getBatchDisplayName} = useProvidePaperBatchCondition(type)
 const classId = ref('')
 const queryParams = computed(() => ({buildType: type, batchId: toValue(batchId), classId: toValue(classId)}))
 const {rules, handleQuery, resetQuery, list, total, getList, classList} = useProvidePaperList(queryParams)
+const {handleDownload} = usePaperDownload(queryParams)
 
-const handleAction = function (action, row) {
+const handleAction = async function (action, row) {
     switch (action.key) {
         case "detail":
             drawer.value = true
             drawerObj.value = row
             break;
+        case "download":
+            await handleDownload(row)
+            break;
         default:
             throw new Error(`Action key '${action.key}' not support.`)
             break

+ 19 - 6
back-ui/src/views/dz/papers/components/list-full-hand.vue

@@ -2,7 +2,7 @@
     <el-form ref="queryRef" :model="queryParams" :rules="rules" label-width="68px" inline>
         <el-form-item label="批次" prop="batchId">
             <el-select v-model="batchId" clearable @change="handleQuery" style="width: 172px">
-                <el-option v-for="b in batchList" :label="b.name" :value="b.batchId"/>
+                <el-option v-for="b in batchList" :label="formatBatchName(b)" :value="b.batchId"/>
             </el-select>
         </el-form-item>
         <el-form-item label="考生类型" prop="examType">
@@ -27,7 +27,11 @@
 <!--            <el-button icon="Refresh" @click="resetQuery">重置</el-button>-->
         </el-form-item>
     </el-form>
-    <Table :data="list" :columns="columns" :actions="actions" @get-list="getList" @action="handleAction"/>
+    <Table :data="list" :columns="columns" :actions="actions" @get-list="getList" @action="handleAction">
+        <template #batchName="{row}">
+            <span>{{ getBatchDisplayName(row) }}</span>
+        </template>
+    </Table>
     <el-drawer v-model="drawer" title="班级详情" size="1000px">
         <class-detail v-if="drawer" :data="drawerObj"/>
     </el-drawer>
@@ -37,18 +41,20 @@
 import consts from "@/utils/consts.js";
 import {useProvidePaperFullCondition} from "@/views/dz/papers/hooks/usePaperFullCondition.js";
 import {useProvidePaperList} from "@/views/dz/papers/hooks/usePaperList.js";
+import {usePaperDownload} from "@/views/dz/papers/hooks/usePaperDownload.js";
 import Table from "@/components/Table/index.vue";
 import ClassDetail from "@/views/dz/papers/components/plugs/class-detail.vue";
 
 const columns = [
     {label: '组卷类型', prop: 'buildTypeName'},
     {label: '班级', prop: 'className'},
-    {label: '批次', prop: 'batchName'},
+    {label: '批次', prop: 'batchName', type: 'slot', slotName: 'batchName'},
     {label: '班级人数', prop: 'total'},
     ...consts.config.fullColumns
 ]
 const actions = [
-    {label: '查看详情', key: 'detail', permission: ['']}
+    {label: '详情', key: 'detail', permission: ['learn:test:list']},
+    // {label: '下载', key: 'download', permission: ['learn:paper:download']}
 ]
 
 const drawer = ref(false)
@@ -61,7 +67,9 @@ const {
     examType,
     groupedSubjects,
     subjectId,
-    conditionArgs
+    conditionArgs,
+    formatBatchName,
+    getBatchDisplayName
 } = useProvidePaperFullCondition(type)
 const classId = ref('')
 const queryParams = computed(() => ({
@@ -69,11 +77,16 @@ const queryParams = computed(() => ({
     classId: toValue(classId)
 }))
 const {rules, handleQuery, resetQuery, list, total, getList, classList} = useProvidePaperList(queryParams)
+const {handleDownload} = usePaperDownload(queryParams)
 
-const handleAction = function (action, row) {
+const handleAction = async function (action, row) {
     switch (action.key) {
         case "detail":
             drawer.value = true
+            drawerObj.value = row
+            break;
+        case "download":
+            await handleDownload(row)
             break;
         default:
             throw new Error(`Action key '${action.key}' not support.`)

+ 19 - 6
back-ui/src/views/dz/papers/components/list-full-intelligent.vue

@@ -2,7 +2,7 @@
     <el-form ref="queryRef" :model="queryParams" :rules="rules" label-width="68px" inline>
         <el-form-item label="批次" prop="batchId">
             <el-select v-model="batchId" clearable @change="handleQuery" style="width: 172px">
-                <el-option v-for="b in batchList" :label="b.name" :value="b.batchId"/>
+                <el-option v-for="b in batchList" :label="formatBatchName(b)" :value="b.batchId"/>
             </el-select>
         </el-form-item>
         <el-form-item label="考生类型" prop="examType">
@@ -27,7 +27,11 @@
 <!--            <el-button icon="Refresh" @click="resetQuery">重置</el-button>-->
         </el-form-item>
     </el-form>
-    <Table :data="list" :columns="columns" :actions="actions" @get-list="getList" @action="handleAction"/>
+    <Table :data="list" :columns="columns" :actions="actions" @get-list="getList" @action="handleAction">
+        <template #batchName="{row}">
+            <span>{{ getBatchDisplayName(row) }}</span>
+        </template>
+    </Table>
     <el-drawer v-model="drawer" title="班级详情" size="1000px">
         <class-detail v-if="drawer" :data="drawerObj"/>
     </el-drawer>
@@ -37,18 +41,20 @@
 import consts from "@/utils/consts.js";
 import {useProvidePaperList} from "@/views/dz/papers/hooks/usePaperList.js";
 import {useProvidePaperFullCondition} from "@/views/dz/papers/hooks/usePaperFullCondition.js";
+import {usePaperDownload} from "@/views/dz/papers/hooks/usePaperDownload.js";
 import Table from '@/components/Table/index.vue';
 import ClassDetail from "@/views/dz/papers/components/plugs/class-detail.vue";
 
 const columns = [
     {label: '组卷类型', prop: 'buildTypeName'},
     {label: '班级', prop: 'className'},
-    {label: '批次', prop: 'batchName'},
+    {label: '批次', prop: 'batchName', type: 'slot', slotName: 'batchName'},
     {label: '班级人数', prop: 'total'},
     ...consts.config.fullColumns
 ]
 const actions = [
-    {label: '查看详情', key: 'detail', permission: ['']}
+    {label: '详情', key: 'detail', permission: ['learn:test:list']},
+    // {label: '下载', key: 'download', permission: ['learn:paper:download']}
 ]
 
 const drawer = ref(false)
@@ -61,7 +67,9 @@ const {
     examType,
     groupedSubjects,
     subjectId,
-    conditionArgs
+    conditionArgs,
+    formatBatchName,
+    getBatchDisplayName
 } = useProvidePaperFullCondition(type)
 const classId = ref('')
 const queryParams = computed(() => ({
@@ -69,11 +77,16 @@ const queryParams = computed(() => ({
     classId: toValue(classId)
 }))
 const {rules, handleQuery, resetQuery, list, total, getList, classList} = useProvidePaperList(queryParams)
+const {handleDownload} = usePaperDownload(queryParams)
 
-const handleAction = function (action, row) {
+const handleAction = async function (action, row) {
     switch (action.key) {
         case "detail":
             drawer.value = true
+            drawerObj.value = row
+            break;
+        case "download":
+            await handleDownload(row)
             break;
         default:
             throw new Error(`Action key '${action.key}' not support.`)

+ 16 - 7
back-ui/src/views/dz/papers/components/paper-exact-hand.vue

@@ -3,11 +3,7 @@
         <el-row :gutter="20">
             <el-col :span="8">
                 <el-form label-width="68px">
-                    <el-form-item label="批次">
-                        <el-select v-model="batchId" clearable style="width: 227px">
-                            <el-option v-for="b in batchList" :label="b.name" :value="b.batchId"/>
-                        </el-select>
-                    </el-form-item>
+                    <BatchYearSelect v-model:batch-id="batchId" :batch-list="batchList" />
                     <el-form-item label="考生类型">
                         <el-select v-model="examType" clearable style="width: 227px">
                             <el-option v-for="e in examTypes" :label="e.dictLabel" :value="e.dictValue"/>
@@ -51,7 +47,7 @@
 </template>
 
 <script setup name="PaperExactHand">
-
+import { ref, computed } from 'vue'
 import consts from "@/utils/consts.js";
 import {useProvidePaperExactCondition} from "@/views/dz/papers/hooks/usePaperExactCondition.js";
 import {useProvidePaperClassStatisticCondition} from "@/views/dz/papers/hooks/usePaperClassStatisticCondition.js";
@@ -65,6 +61,7 @@ import router from "@/router/index.js";
 import BuiltPaperList from "@/views/dz/papers/components/plugs/built-paper-list.vue";
 import StudentListDialog from "@/views/dz/papers/components/plugs/student-list-dialog.vue";
 import {useInjectGlobalLoading} from "@/views/hooks/useGlobalLoading.js";
+import BatchYearSelect from "@/views/dz/papers/components/BatchYearSelect.vue";
 
 const type = consts.enums.buildType.ExactHand
 const {
@@ -73,7 +70,7 @@ const {
     universityId, universities,
     majorGroup, majorGroups,
     majors, majorPlanId,
-    conditionArgs, onConditionReady
+    conditionArgs, onConditionReady, onBatchReady
 } = useProvidePaperExactCondition(type)
 const {selectedClasses, classList, loadClassStatistic} = useProvidePaperClassStatisticCondition()
 const {knowledgeNode, knowledgeCheckNodes, loadKnowledge} = useProvidePaperKnowledgeCondition()
@@ -169,6 +166,18 @@ onConditionReady(async (payload) => {
     await built.value.loadBuiltPaper(payload)
 })
 
+// 监听批次变化,调用接口
+onBatchReady(async (payload) => {
+    // 如果已经有完整的条件参数,更新批次ID;否则只使用批次
+    if (statArg) {
+        statArg = { ...statArg, batchId: payload.batchId }
+    } else {
+        statArg = payload
+    }
+    await _loadClassStatistic()
+    await built.value.loadBuiltPaper(statArg)
+})
+
 watch(conditionArgs, () => built.value.reset())
 
 // 刷新数据

+ 2 - 5
back-ui/src/views/dz/papers/components/paper-exact-intelligent.vue

@@ -2,11 +2,7 @@
     <el-row :gutter="20">
         <el-col :span="8">
             <el-form label-width="68px">
-                <el-form-item label="批次">
-                    <el-select v-model="batchId" clearable style="width: 227px">
-                        <el-option v-for="b in batchList" :label="b.name" :value="b.batchId"/>
-                    </el-select>
-                </el-form-item>
+                <BatchYearSelect v-model:batch-id="batchId" :batch-list="batchList" />
             </el-form>
         </el-col>
         <el-col :span="16">
@@ -34,6 +30,7 @@ import {buildPaperExactIntelligent} from "@/api/dz/papers.js";
 import {useInjectGlobalLoading} from "@/views/hooks/useGlobalLoading.js";
 import BuiltPaperList from "@/views/dz/papers/components/plugs/built-paper-list.vue";
 import StudentListDialog from "@/views/dz/papers/components/plugs/student-list-dialog.vue";
+import BatchYearSelect from "@/views/dz/papers/components/BatchYearSelect.vue";
 
 const type = consts.enums.buildType.ExactIntelligent
 const {batchId, batchList, onBatchReady} = useProvidePaperBatchCondition(type, true)

+ 17 - 6
back-ui/src/views/dz/papers/components/paper-full-hand.vue

@@ -3,11 +3,7 @@
         <el-row :gutter="20">
             <el-col :span="8">
                 <el-form label-width="68px">
-                    <el-form-item label="批次">
-                        <el-select v-model="batchId" clearable style="width: 227px">
-                            <el-option v-for="b in batchList" :label="b.name" :value="b.batchId"/>
-                        </el-select>
-                    </el-form-item>
+                    <BatchYearSelect v-model:batch-id="batchId" :batch-list="batchList" />
                     <el-form-item label="考生类型">
                         <el-select v-model="examType" clearable style="width: 227px">
                             <el-option v-for="e in examTypes" :label="e.dictLabel" :value="e.dictValue"/>
@@ -43,6 +39,7 @@
 </template>
 
 <script setup name="PaperFullHand">
+import { ref, computed } from 'vue'
 import {ElMessage} from "element-plus";
 import router from "@/router/index.js";
 import consts from "@/utils/consts.js";
@@ -56,6 +53,7 @@ import QuestionHand from "@/views/dz/papers/components/plugs/question-hand.vue";
 import BuiltPaperList from "@/views/dz/papers/components/plugs/built-paper-list.vue";
 import StudentListDialog from "@/views/dz/papers/components/plugs/student-list-dialog.vue";
 import {useInjectGlobalLoading} from "@/views/hooks/useGlobalLoading.js";
+import BatchYearSelect from "@/views/dz/papers/components/BatchYearSelect.vue";
 
 const type = consts.enums.buildType.FullHand
 const {
@@ -66,7 +64,8 @@ const {
     subjectId,
     groupedSubjects,
     conditionArgs,
-    onConditionReady
+    onConditionReady,
+    onBatchReady
 } = useProvidePaperFullCondition(type)
 const {selectedClasses, classList, loadClassStatistic} = useProvidePaperClassStatisticCondition()
 const {knowledgeNode, knowledgeCheckNodes, loadKnowledge} = useProvidePaperKnowledgeCondition()
@@ -155,6 +154,18 @@ onConditionReady(async (payload) => {
     await built.value.loadBuiltPaper(payload)
 })
 
+// 监听批次变化,调用接口
+onBatchReady(async (payload) => {
+    // 如果已经有完整的条件参数,更新批次ID;否则只使用批次
+    if (statArg) {
+        statArg = { ...statArg, batchId: payload.batchId }
+    } else {
+        statArg = payload
+    }
+    await _loadClassStatistic()
+    await built.value.loadBuiltPaper(statArg)
+})
+
 watch(conditionArgs, () => built.value.reset())
 
 // 刷新数据

+ 17 - 7
back-ui/src/views/dz/papers/components/paper-full-intelligent.vue

@@ -2,11 +2,7 @@
     <el-row :gutter="20">
         <el-col :span="8">
             <el-form label-width="68px">
-                <el-form-item label="批次">
-                    <el-select v-model="batchId" clearable style="width: 227px">
-                        <el-option v-for="b in batchList" :label="b.name" :value="b.batchId"/>
-                    </el-select>
-                </el-form-item>
+                <BatchYearSelect v-model:batch-id="batchId" :batch-list="batchList" />
                 <el-form-item label="考生类型">
                     <el-select v-model="examType" clearable style="width: 227px">
                         <el-option v-for="e in examTypes" :label="e.dictLabel" :value="e.dictValue"/>
@@ -41,7 +37,7 @@
 </template>
 
 <script setup name="PaperFullIntelligent">
-
+import { ref, computed } from 'vue'
 import consts from "@/utils/consts.js";
 import {useProvidePaperFullCondition} from "@/views/dz/papers/hooks/usePaperFullCondition.js";
 import ClassStatisticTable from "@/views/dz/papers/components/plugs/class-statistic-table.vue";
@@ -53,6 +49,7 @@ import {ElMessage} from "element-plus";
 import {buildPaperFullIntelligent} from "@/api/dz/papers.js";
 import BuiltPaperList from "@/views/dz/papers/components/plugs/built-paper-list.vue";
 import StudentListDialog from "@/views/dz/papers/components/plugs/student-list-dialog.vue";
+import BatchYearSelect from "@/views/dz/papers/components/BatchYearSelect.vue";
 
 const type = consts.enums.buildType.FullIntelligent
 const {
@@ -63,7 +60,8 @@ const {
     subjectId,
     groupedSubjects,
     conditionArgs,
-    onConditionReady
+    onConditionReady,
+    onBatchReady
 } = useProvidePaperFullCondition(type)
 const {selectedClasses, classList, loadClassStatistic} = useProvidePaperClassStatisticCondition()
 const {knowledgeNode, knowledgeCheckNodes, loadKnowledge} = useProvidePaperKnowledgeCondition()
@@ -143,6 +141,18 @@ onConditionReady(async (payload) => {
     await built.value.loadBuiltPaper(payload)
 })
 
+// 监听批次变化,调用接口
+onBatchReady(async (payload) => {
+    // 如果已经有完整的条件参数,更新批次ID;否则只使用批次
+    if (statArg) {
+        statArg = { ...statArg, batchId: payload.batchId }
+    } else {
+        statArg = payload
+    }
+    await _loadClassStatistic()
+    await built.value.loadBuiltPaper(statArg)
+})
+
 watch(conditionArgs, () => built.value.reset())
 
 // 刷新数据

+ 156 - 0
back-ui/src/views/dz/papers/hooks/useBatchYear.js

@@ -0,0 +1,156 @@
+import { ref, computed, watch, nextTick } from 'vue'
+import { ElMessage } from 'element-plus'
+import { addTest } from '@/api/learn/test.js'
+import { getPaperBatches } from '@/api/dz/papers.js'
+
+/**
+ * 批次和年份相关的公共逻辑
+ * @param {Ref} batchId - 批次ID的ref
+ * @param {Ref} batchList - 批次列表的ref
+ * @returns {Object} 返回年份和批次相关的状态和方法
+ */
+export function useBatchYear(batchId, batchList) {
+    // 年份相关
+    const selectedYear = ref(null)
+    
+    // 从batchList中提取唯一的年份列表
+    const yearList = computed(() => {
+        const years = new Set()
+        if (batchList.value && Array.isArray(batchList.value)) {
+            batchList.value.forEach(b => {
+                if (b && b.year) {
+                    years.add(b.year)
+                }
+            })
+        }
+        return Array.from(years).sort((a, b) => b - a) // 降序排列
+    })
+
+    // 根据选中的年份过滤批次列表
+    const filteredBatchList = computed(() => {
+        if (!batchList.value || !Array.isArray(batchList.value)) {
+            return []
+        }
+        if (!selectedYear.value) {
+            return batchList.value
+        }
+        return batchList.value.filter(b => b && b.year === selectedYear.value)
+    })
+
+    // 监听年份列表变化,默认选择第一个年份
+    watch(yearList, (years) => {
+        if (years.length > 0 && !selectedYear.value) {
+            selectedYear.value = years[0]
+        }
+    }, { immediate: true })
+
+    // 监听年份变化,清空批次选择,然后选择第一个批次
+    watch(selectedYear, async () => {
+        batchId.value = null
+        // 等待 filteredBatchList 更新后,选择第一个批次
+        await nextTick()
+        if (filteredBatchList.value.length > 0) {
+            batchId.value = filteredBatchList.value[0].batchId
+        }
+    })
+
+    // 监听过滤后的批次列表变化,默认选择第一个批次
+    watch(filteredBatchList, (batches) => {
+        if (batches.length > 0 && !batchId.value) {
+            batchId.value = batches[0].batchId
+        }
+    }, { immediate: true })
+
+    // 新增批次相关
+    const batchDialogVisible = ref(false)
+    const batchFormRef = ref(null)
+    const batchSaving = ref(false)
+    const batchForm = ref({
+        year: null,
+        name: ''
+    })
+    const batchRules = {
+        year: [
+            { required: true, message: '请输入年份', trigger: 'blur' }
+        ],
+        name: [
+            { required: true, message: '请输入批次名称', trigger: 'blur' }
+        ]
+    }
+
+    // 打开新增批次弹窗
+    const handleAddBatch = () => {
+        batchForm.value = {
+            year: selectedYear.value || new Date().getFullYear(),
+            name: ''
+        }
+        batchDialogVisible.value = true
+    }
+
+    // 保存批次
+    const handleSaveBatch = async () => {
+        if (!batchFormRef.value) return
+        
+        await batchFormRef.value.validate(async (valid) => {
+            if (valid) {
+                try {
+                    batchSaving.value = true
+                    await addTest({
+                        year: batchForm.value.year,
+                        name: batchForm.value.name
+                    })
+                    ElMessage.success('保存成功')
+                    batchDialogVisible.value = false
+                    
+                    // 刷新批次列表
+                    const res = await getPaperBatches()
+                    batchList.value = res.data
+                    
+                    // 如果保存的年份是当前选中的年份,自动选中新创建的批次
+                    if (batchForm.value.year === selectedYear.value) {
+                        const newBatch = res.data.find(b => b.year === batchForm.value.year && b.name === batchForm.value.name)
+                        if (newBatch) {
+                            batchId.value = newBatch.batchId
+                        }
+                    } else {
+                        // 如果保存的年份不是当前选中的年份,自动选中该年份
+                        selectedYear.value = batchForm.value.year
+                    }
+                } catch (error) {
+                    console.error('保存批次失败:', error)
+                    ElMessage.error('保存失败')
+                } finally {
+                    batchSaving.value = false
+                }
+            }
+        })
+    }
+
+    /**
+     * 格式化批次名称(用于下拉框显示)
+     * @param {Object} batch - 批次对象
+     * @returns {String} 格式化后的批次名称
+     */
+    const formatBatchName = (batch) => {
+        if (!batch || !batch.name) return '-'
+        if (batch.year) {
+            return `${batch.name}(${batch.year})`
+        }
+        return batch.name
+    }
+
+    return {
+        selectedYear,
+        yearList,
+        filteredBatchList,
+        batchDialogVisible,
+        batchFormRef,
+        batchSaving,
+        batchForm,
+        batchRules,
+        handleAddBatch,
+        handleSaveBatch,
+        formatBatchName
+    }
+}
+

+ 29 - 1
back-ui/src/views/dz/papers/hooks/usePaperBatchCondition.js

@@ -34,7 +34,35 @@ export const useProvidePaperBatchCondition = function (type, autoSelectFirst = f
         }
     })
 
-    const payload = {buildType, batchId, batchList, onBatchReady: batchEvent.on}
+    /**
+     * 格式化批次名称(用于下拉框显示)
+     * @param {Object} batch - 批次对象
+     * @returns {String} 格式化后的批次名称
+     */
+    const formatBatchName = (batch) => {
+        if (!batch || !batch.name) return '-'
+        if (batch.year) {
+            return `${batch.name}(${batch.year})`
+        }
+        return batch.name
+    }
+
+    /**
+     * 获取批次显示名称(batchName(year))
+     * @param {Object} row - 表格行数据
+     * @returns {String} 批次显示名称
+     */
+    const getBatchDisplayName = (row) => {
+        if (!row || !row.batchName) return '-'
+        // 从 batchList 中查找对应的批次,获取年份
+        const batch = batchList.value.find(b => b.batchId === row.batchId || b.name === row.batchName)
+        if (batch && batch.year) {
+            return `${row.batchName}(${batch.year})`
+        }
+        return row.batchName
+    }
+
+    const payload = {buildType, batchId, batchList, formatBatchName, getBatchDisplayName, onBatchReady: batchEvent.on}
     provideLocal(key, payload)
     return payload
 }

+ 68 - 0
back-ui/src/views/dz/papers/hooks/usePaperDownload.js

@@ -0,0 +1,68 @@
+import { toValue } from "vue";
+import { downloadPaper } from "@/api/dz/papers.js";
+import { ElMessage } from "element-plus";
+import { saveAs } from "file-saver";
+import { blobValidate } from "@/utils/ruoyi";
+
+/**
+ * 试卷下载通用 hook
+ * @param {ComputedRef|Ref} queryParams - 查询参数
+ * @returns {Function} handleDownload - 下载处理函数
+ */
+export function usePaperDownload(queryParams) {
+    /**
+     * 处理下载
+     * @param {Object} row - 表格行数据
+     */
+    const handleDownload = async (row) => {
+        try {
+            // 优先使用 row 中的字段,如果没有则使用 queryParams 中的值
+            const currentParams = toValue(queryParams)
+            const params = {
+                batchId: row.batchId || currentParams.batchId,
+                examType: row.examType || currentParams.examType,
+                subjectId: row.subjectId || currentParams.subjectId,
+                classId: row.classId || row.id || currentParams.classId,
+                // 定向组卷可能需要的额外参数
+                universityId: row.universityId || currentParams.universityId,
+                majorGroup: row.majorGroup || currentParams.majorGroup,
+                majorPlanId: row.majorPlanId || currentParams.majorPlanId
+            }
+            
+            const response = await downloadPaper(params)
+            
+            // 检查响应是否为 blob
+            if (blobValidate(response)) {
+                const blob = new Blob([response])
+                // 生成文件名,可以根据实际情况调整
+                const filename = `${row.className || '试卷'}_${row.batchName || ''}_${Date.now()}.xlsx`
+                saveAs(blob, filename)
+                ElMessage.success('下载成功')
+            } else {
+                // 如果不是 blob,尝试解析错误信息
+                if (response instanceof Blob) {
+                    const text = await response.text()
+                    try {
+                        const errorObj = JSON.parse(text)
+                        ElMessage.error(errorObj.msg || errorObj.message || '下载失败')
+                    } catch (e) {
+                        ElMessage.error('下载失败')
+                    }
+                } else if (response && response.msg) {
+                    ElMessage.error(response.msg || '下载失败')
+                } else {
+                    ElMessage.error('下载失败')
+                }
+            }
+        } catch (error) {
+            console.error('下载失败:', error)
+            const errorMsg = error?.response?.data?.msg || error?.message || '下载失败,请稍后重试'
+            ElMessage.error(errorMsg)
+        }
+    }
+
+    return {
+        handleDownload
+    }
+}
+

+ 2 - 2
back-ui/src/views/dz/papers/hooks/usePaperExactCondition.js

@@ -4,7 +4,7 @@ import {createEventHook, injectLocal, provideLocal} from "@vueuse/core";
 
 const key = Symbol('PaperExactCondition')
 export const useProvidePaperExactCondition = function (type) {
-    const {buildType, batchId, batchList} = useProvidePaperBatchCondition(type)
+    const {buildType, batchId, batchList, formatBatchName, getBatchDisplayName, onBatchReady} = useProvidePaperBatchCondition(type)
 
     const examType = ref('')
     const examTypes = ref([])
@@ -111,7 +111,7 @@ export const useProvidePaperExactCondition = function (type) {
         await triggerExactEvent(args)
     })
 
-    const payload = {buildType, batchId, batchList,
+    const payload = {buildType, batchId, batchList, formatBatchName, getBatchDisplayName, onBatchReady,
         examType, examTypes,
         universityId, universities,
         majorGroup, majorGroups,

+ 2 - 2
back-ui/src/views/dz/papers/hooks/usePaperFullCondition.js

@@ -4,7 +4,7 @@ import {getPaperExamTypes, getPaperSubjects} from "@/api/dz/papers.js";
 
 const key = Symbol('PaperFullCondition')
 export const useProvidePaperFullCondition = function (type) {
-    const {buildType, batchId, batchList} = useProvidePaperBatchCondition(type)
+    const {buildType, batchId, batchList, formatBatchName, getBatchDisplayName, onBatchReady} = useProvidePaperBatchCondition(type)
 
     const examType = ref('')
     const examTypes = ref([])
@@ -61,7 +61,7 @@ export const useProvidePaperFullCondition = function (type) {
         await readyEvent.trigger(payload)
     })
 
-    const payload = {buildType, batchId, batchList,
+    const payload = {buildType, batchId, batchList, formatBatchName, getBatchDisplayName, onBatchReady,
         examType, examTypes,
         subjectId, subjectList, groupedSubjects,
         conditionArgs, conditionData,

+ 19 - 18
back-ui/src/views/learn/questions/index.vue

@@ -285,10 +285,10 @@
                         </el-col>
                     </el-row>
                 </div>
-                
+
                 <!-- 横线分隔(仅在修改时显示) -->
                 <el-divider v-if="form.id != null"></el-divider>
-                
+
                 <!-- 可修改内容区域,两列布局 -->
                 <el-row :gutter="20">
                     <el-col :span="12">
@@ -377,7 +377,7 @@
                         <el-option v-for="q in question_type" :label="q.label" :value="q.value"/>
                     </el-select>
                 </el-form-item> -->
-                
+
                 <el-row :gutter="20">
                     <el-col :span="12">
                         <el-form-item label="知识点" prop="knowledgeId">
@@ -401,7 +401,8 @@
                     </el-col>
                     <el-col :span="12">
                         <el-form-item label="试题解析" prop="parse">
-                            <el-input v-model="form.parse" type="textarea" :autosize="{ minRows: 3 }" placeholder="请输入内容"/>
+                            <Editor v-if="!isTextMode" v-model="form.parse" :min-height="120" />
+                            <el-input v-else v-model="form.parse" type="textarea" :autosize="{ minRows: 3 }" placeholder="请输入内容"/>
                         </el-form-item>
                     </el-col>
                 </el-row>
@@ -453,7 +454,7 @@
                 </div>
             </template>
         </el-dialog>
-        
+
         <!-- 选项详情弹窗 -->
         <el-dialog v-model="showOptionsDialog" title="选项详情" width="600px" append-to-body>
             <div class="options-detail">
@@ -495,7 +496,7 @@
                 </div>
             </template>
         </el-dialog>
-        
+
         <el-dialog v-model="showTypeDialog" title="修改题型" show-close width="500px" append-to-body>
             <div class="text-content">题号:{{typeForm.ids}}</div>
             <div class="mt-2 mb-2">共<el-text type="primary">{{typeForm.ids.length}}</el-text>项</div>
@@ -868,17 +869,17 @@ function handleShowOptions(row) {
 /** 获取图片代理URL(如果需要后端代理,可以在这里实现) */
 function getImageProxyUrl(imageUrl) {
     if (!imageUrl) return ''
-    
+
     // 如果是相对路径或本地路径,直接返回
     if (!imageUrl.startsWith('http://') && !imageUrl.startsWith('https://') && !imageUrl.startsWith('//')) {
         return imageUrl
     }
-    
+
     // 如果需要通过后端代理访问第三方图片,可以取消下面的注释
     // 注意:需要后端提供图片代理接口
     // const encodedUrl = encodeURIComponent(imageUrl)
     // return `${import.meta.env.VITE_APP_BASE_API}/common/image/proxy?url=${encodedUrl}`
-    
+
     // 暂时直接返回原URL,通过添加跨域属性来处理
     return imageUrl
 }
@@ -886,13 +887,13 @@ function getImageProxyUrl(imageUrl) {
 /** 格式化内容,将图片URL转换为img标签 */
 function formatContentWithImages(content) {
     if (!content) return ''
-    
+
     // 如果内容已经是HTML格式(包含img标签),需要处理其中的图片URL
     if (content.includes('<img') || content.includes('<IMG')) {
         // 提取所有img标签中的src属性
         const imgSrcRegex = /<img[^>]+src=["']([^"']+)["'][^>]*>/gi
         let formattedContent = content
-        
+
         formattedContent = formattedContent.replace(imgSrcRegex, (match, src) => {
             // 如果是第三方图片URL,转换为代理URL
             let imageUrl = src
@@ -909,18 +910,18 @@ function formatContentWithImages(content) {
             }
             return match
         })
-        
+
         return formattedContent
     }
-    
+
     // 匹配图片URL的正则表达式
     // 支持:https://、http://、//开头的完整URL,以及相对路径
     const imageUrlRegex = /(https?:\/\/[^\s<>"']+\.(jpg|jpeg|png|gif|bmp|webp|svg)(\?[^\s<>"']*)?)|(\/\/[^\s<>"']+\.(jpg|jpeg|png|gif|bmp|webp|svg)(\?[^\s<>"']*)?)|(\/[^\s<>"']+\.(jpg|jpeg|png|gif|bmp|webp|svg)(\?[^\s<>"']*)?)/gi
-    
+
     // 将纯文本的图片URL转换为img标签
     let formattedContent = content
     const matches = content.match(imageUrlRegex)
-    
+
     if (matches) {
         matches.forEach(url => {
             // 确保URL是完整的
@@ -929,19 +930,19 @@ function formatContentWithImages(content) {
             if (imageUrl.startsWith('//')) {
                 imageUrl = 'https:' + imageUrl
             }
-            
+
             // 对于第三方图片,尝试使用代理
             const proxyUrl = getImageProxyUrl(imageUrl)
             // 如果代理不可用,使用原URL并添加跨域属性
             const finalUrl = proxyUrl !== imageUrl ? proxyUrl : imageUrl
             const crossOriginAttr = proxyUrl === imageUrl ? 'crossorigin="anonymous" referrerpolicy="no-referrer"' : ''
-            
+
             // 将URL替换为img标签
             const imgTag = `<img src="${finalUrl}" ${crossOriginAttr} alt="图片" style="max-width: 100%; height: auto; display: block; margin: 10px 0;" onerror="this.onerror=null; this.src='${imageUrl}'; this.style.border='1px solid #ddd'; this.alt='图片加载失败,点击查看原图'; this.style.cursor='pointer'; this.title='点击查看原图'; this.onclick='window.open(this.src)';" />`
             formattedContent = formattedContent.replace(url, imgTag)
         })
     }
-    
+
     return formattedContent
 }
 

+ 10 - 1
ie-admin/src/main/java/com/ruoyi/web/controller/learn/LearnPaperController.java

@@ -5,6 +5,7 @@ import javax.annotation.security.PermitAll;
 import javax.servlet.http.HttpServletResponse;
 
 import com.ruoyi.common.annotation.Anonymous;
+import com.ruoyi.web.service.CommService;
 import com.ruoyi.web.service.PaperService;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
@@ -23,7 +24,7 @@ import com.ruoyi.common.core.page.TableDataInfo;
 
 /**
  * 试卷Controller
- * 
+ *
  * @author ruoyi
  * @date 2025-09-18
  */
@@ -37,6 +38,8 @@ public class LearnPaperController extends BaseController
 
     @Autowired
     private PaperService paperService;
+    @Autowired
+    private CommService commService;
 
     /**
      * 查询试卷列表
@@ -116,4 +119,10 @@ public class LearnPaperController extends BaseController
         paperService.buildAllPapers(seq);
         return success();
     }
+
+    @ApiOperation("下载试卷")
+    @RequestMapping(value = "download", method = {RequestMethod.GET, RequestMethod.POST})
+    public void download(HttpServletResponse response, Long paperId) {
+        commService.download(response, paperId);
+    }
 }

+ 39 - 1
ie-admin/src/main/java/com/ruoyi/web/service/CommService.java

@@ -16,10 +16,18 @@ import com.ruoyi.enums.CardTimeStatus;
 import com.ruoyi.enums.UserTypeEnum;
 import com.ruoyi.learn.domain.LearnKnowledgeCourse;
 import com.ruoyi.learn.domain.LearnKnowledgeTree;
+import com.ruoyi.learn.domain.LearnPaper;
+import com.ruoyi.learn.domain.LearnQuestions;
+import com.ruoyi.learn.service.ILearnPaperService;
+import com.ruoyi.learn.service.ILearnQuestionsService;
 import com.ruoyi.system.service.ISysConfigService;
+import com.ruoyi.system.utils.downloadpaper.DownloadPaperUtils;
+import org.apache.commons.io.IOUtils;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import org.springframework.util.CollectionUtils;
 
+import javax.servlet.http.HttpServletResponse;
 import java.time.LocalDate;
 import java.time.ZoneId;
 import java.util.Date;
@@ -33,6 +41,11 @@ public class CommService {
     private final ISysConfigService configService;
     private final IDzCardsService cardsService;
 
+    @Autowired
+    ILearnPaperService paperService;
+    @Autowired
+    ILearnQuestionsService learnQuestionsService;
+
     public CommService(ISysConfigService configService, IDzCardsService cardsService) {
         this.configService = configService;
         this.cardsService = cardsService;
@@ -152,7 +165,6 @@ public class CommService {
         }
         return treeNodeList;
     }
-
     public String getLocation(){
         return getLocation(null);
     }
@@ -165,4 +177,30 @@ public class CommService {
         }
         return location;
     }
+
+    /**
+     * 下载试卷
+     * @param response
+     * @param paperId
+     */
+    public void download(HttpServletResponse response, Long paperId){
+        try {
+            LearnPaper papers = paperService.selectLearnPaperById(paperId);
+            List<LearnQuestions> questions = learnQuestionsService.selectQuestionByPaperId(paperId);
+            byte[] data = DownloadPaperUtils.download(papers.getPaperName(), questions);
+
+            response.setHeader("Pragma", "No-cache");
+            response.setHeader("Cache-Control", "No-cache");
+            response.setHeader("Expires", "0");
+            response.setHeader("Content-Disposition",  String.format("attachment; filename=\"%s\"", new String((papers.getPaperName() + ".docx").getBytes("utf-8"), "ISO-8859-1")));
+            response.setHeader("Content-Type", "application/octet-stream");
+            IOUtils.write(data, response.getOutputStream());
+        } catch (Exception e) {
+            throw new RuntimeException(e.getMessage(), e);
+        }
+
+
+    }
+
+
 }

+ 7 - 2
ie-common/pom.xml

@@ -76,7 +76,7 @@
             <groupId>com.fasterxml.jackson.core</groupId>
             <artifactId>jackson-databind</artifactId>
         </dependency>
-        
+
         <!-- 阿里JSON解析器 -->
         <dependency>
             <groupId>com.alibaba.fastjson2</groupId>
@@ -206,6 +206,11 @@
             <artifactId>hutool-db</artifactId>
             <version>5.7.21</version>
         </dependency>
+
+        <dependency>
+            <groupId>org.apache.httpcomponents</groupId>
+            <artifactId>httpmime</artifactId>
+        </dependency>
     </dependencies>
 
-</project>
+</project>

+ 0 - 1
ie-common/src/main/java/com/ruoyi/common/utils/StringUtils.java

@@ -24,7 +24,6 @@ public class StringUtils extends org.apache.commons.lang3.StringUtils
 
     /** 星号 */
     private static final char ASTERISK = '*';
-    public static final String tikuimages= "tikuimages";
 
     /**
      * 获取参数不为空值

+ 51 - 30
ie-system/src/main/java/com/ruoyi/learn/service/impl/LearnQuestionsServiceImpl.java

@@ -28,6 +28,42 @@ public class LearnQuestionsServiceImpl implements ILearnQuestionsService
     @Autowired
     private ISysConfigService configService;
 
+    /**
+     * 获取图片路径列表(从配置中获取)
+     * @return 图片路径列表
+     */
+    private List<String> getImagePaths() {
+        String configValue = configService.selectConfigByKey("question.img.prefix");
+        if (StringUtils.isBlank(configValue)) {
+            // 如果配置为空,返回默认值
+            return Arrays.asList("tikuimages", "mathJye");
+        }
+        // 将配置值按逗号分割并转换为List
+        return Arrays.asList(configValue.split(","));
+    }
+
+    /**
+     * 替换图片路径中的 tikuimages 和 mathJye
+     * @param content 原始内容
+     * @param value 替换值
+     * @return 替换后的内容
+     */
+    private String replaceImagePath(String content, String value) {
+        if (StringUtils.isBlank(content)) {
+            return content;
+        }
+        String result = content;
+        List<String> imagePaths = getImagePaths();
+        for (String imagePath : imagePaths) {
+            imagePath = imagePath.trim(); // 去除空格
+            if (StringUtils.isNotBlank(imagePath)) {
+                result = result.replaceAll("src=\"/" + imagePath, "src=\"" + value + imagePath);
+                result = result.replaceAll("src=\'/" + imagePath, "src=\'" + value + imagePath);
+            }
+        }
+        return result;
+    }
+
     /**
      * 查询试题
      *
@@ -176,40 +212,31 @@ public class LearnQuestionsServiceImpl implements ILearnQuestionsService
         if(CollectionUtils.isNotEmpty(res)) {
             for(LearnQuestions questions: res) {
                 if (StringUtils.isNotEmpty(questions.getTitle())) {
-                    questions.setTitle(questions.getTitle().replaceAll("src=\"/"+StringUtils.tikuimages, "src=\""+value+StringUtils.tikuimages));
-                    questions.setTitle(questions.getTitle().replaceAll("src=\'/"+StringUtils.tikuimages, "src=\'"+value+StringUtils.tikuimages));
+                    questions.setTitle(replaceImagePath(questions.getTitle(), value));
                 }
                 if (StringUtils.isNotBlank(questions.getOptionA())) {
-                    questions.setOptionA(questions.getOptionA().replaceAll("src=\"/"+StringUtils.tikuimages, "src=\"" + value+StringUtils.tikuimages));
-                    questions.setOptionA(questions.getOptionA().replaceAll("src=\'/"+StringUtils.tikuimages, "src=\'" + value+StringUtils.tikuimages));
+                    questions.setOptionA(replaceImagePath(questions.getOptionA(), value));
                 }
                 if (StringUtils.isNotBlank(questions.getOptionB())) {
-                    questions.setOptionB(questions.getOptionB().replaceAll("src=\"/"+StringUtils.tikuimages, "src=\"" + value+StringUtils.tikuimages));
-                    questions.setOptionB(questions.getOptionB().replaceAll("src=\'/"+StringUtils.tikuimages, "src=\'" + value+StringUtils.tikuimages));
+                    questions.setOptionB(replaceImagePath(questions.getOptionB(), value));
                 }
                 if (StringUtils.isNotBlank(questions.getOptionC())) {
-                    questions.setOptionC(questions.getOptionC().replaceAll("src=\"/"+StringUtils.tikuimages, "src=\"" + value+StringUtils.tikuimages));
-                    questions.setOptionC(questions.getOptionC().replaceAll("src=\'/"+StringUtils.tikuimages, "src=\'" + value+StringUtils.tikuimages));
+                    questions.setOptionC(replaceImagePath(questions.getOptionC(), value));
                 }
                 if (StringUtils.isNotBlank(questions.getOptionD())) {
-                    questions.setOptionD(questions.getOptionD().replaceAll("src=\"/"+StringUtils.tikuimages, "src=\"" + value+StringUtils.tikuimages));
-                    questions.setOptionD(questions.getOptionD().replaceAll("src=\'/"+StringUtils.tikuimages, "src=\'" + value+StringUtils.tikuimages));
+                    questions.setOptionD(replaceImagePath(questions.getOptionD(), value));
                 }
                 if (StringUtils.isNotBlank(questions.getOptionE())) {
-                    questions.setOptionE(questions.getOptionE().replaceAll("src=\"/"+StringUtils.tikuimages, "src=\"" + value+StringUtils.tikuimages));
-                    questions.setOptionE(questions.getOptionE().replaceAll("src=\'/"+StringUtils.tikuimages, "src=\'" + value+StringUtils.tikuimages));
+                    questions.setOptionE(replaceImagePath(questions.getOptionE(), value));
                 }
                 if(StringUtils.isNotBlank(questions.getAnswer1())) {
-                    questions.setAnswer1(questions.getAnswer1().replaceAll("src=\"/"+StringUtils.tikuimages, "src=\""+value+StringUtils.tikuimages));
-                    questions.setAnswer1(questions.getAnswer1().replaceAll("src=\'/"+StringUtils.tikuimages, "src=\'"+value+StringUtils.tikuimages));
+                    questions.setAnswer1(replaceImagePath(questions.getAnswer1(), value));
                 }
                 if(StringUtils.isNotBlank(questions.getAnswer2())) {
-                    questions.setAnswer2(questions.getAnswer2().replaceAll("src=\"/"+StringUtils.tikuimages, "src=\""+value+StringUtils.tikuimages));
-                    questions.setAnswer2(questions.getAnswer2().replaceAll("src=\'/"+StringUtils.tikuimages, "src=\'"+value+StringUtils.tikuimages));
+                    questions.setAnswer2(replaceImagePath(questions.getAnswer2(), value));
                 }
                 if(StringUtils.isNotBlank(questions.getParse())) {
-                    questions.setParse(questions.getParse().replaceAll("src=\"/"+StringUtils.tikuimages, "src=\""+value+StringUtils.tikuimages));
-                    questions.setParse(questions.getParse().replaceAll("src=\'/"+StringUtils.tikuimages, "src=\'"+value+StringUtils.tikuimages));
+                    questions.setParse(replaceImagePath(questions.getParse(), value));
                 }
                 if(null!=questions.getIsSub()&&questions.getIsSub()==1){
                     questionIds.add(questions.getId());
@@ -220,28 +247,23 @@ public class LearnQuestionsServiceImpl implements ILearnQuestionsService
                 String title = question.getTitle();
                 if (null!=isFillTitleOption&&isFillTitleOption){
                     if(StringUtils.isNotBlank(question .getOptionA())) {
-                        String option = question.getOptionA().replaceAll("src=\"/"+StringUtils.tikuimages, "src=\""+value+StringUtils.tikuimages);
-                        option = option.replaceAll("src=\'/", "src=\'"+StringUtils.tikuimages+value+StringUtils.tikuimages);
+                        String option = replaceImagePath(question.getOptionA(), value);
                         title = title + "<br/>A." + option;
                     }
                     if(StringUtils.isNotBlank(question .getOptionB())) {
-                        String option = question.getOptionB().replaceAll("src=\"/"+StringUtils.tikuimages, "src=\""+value+StringUtils.tikuimages);
-                        option = option.replaceAll("src=\'/", "src=\'"+StringUtils.tikuimages+value+StringUtils.tikuimages);
+                        String option = replaceImagePath(question.getOptionB(), value);
                         title = title + "<br/>B." + option;
                     }
                     if(StringUtils.isNotBlank(question .getOptionC())) {
-                        String option = question.getOptionC().replaceAll("src=\"/"+StringUtils.tikuimages, "src=\""+value+StringUtils.tikuimages);
-                        option = option.replaceAll("src=\'/", "src=\'"+StringUtils.tikuimages+value+StringUtils.tikuimages);
+                        String option = replaceImagePath(question.getOptionC(), value);
                         title = title + "<br/>C." + option;
                     }
                     if(StringUtils.isNotBlank(question .getOptionD())) {
-                        String option = question.getOptionD().replaceAll("src=\"/"+StringUtils.tikuimages, "src=\""+value+StringUtils.tikuimages);
-                        option = option.replaceAll("src=\'/", "src=\'"+StringUtils.tikuimages+value+StringUtils.tikuimages);
+                        String option = replaceImagePath(question.getOptionD(), value);
                         title = title + "<br/>D." + option;
                     }
                     if(StringUtils.isNotBlank(question .getOptionE())) {
-                        String option = question.getOptionE().replaceAll("src=\"/"+StringUtils.tikuimages, "src=\""+value+StringUtils.tikuimages);
-                        option = option.replaceAll("src=\'/", "src=\'"+StringUtils.tikuimages+value+StringUtils.tikuimages);
+                        String option = replaceImagePath(question.getOptionE(), value);
                         title = title + "<br/>E." + option;
                     }
                 }
@@ -262,8 +284,7 @@ public class LearnQuestionsServiceImpl implements ILearnQuestionsService
                 value = "https://file.mingxuejinbang.com/tikubao/";
             }
 
-            return content.replaceAll("src=\"/"+StringUtils.tikuimages, "src=\"" + value+StringUtils.tikuimages)
-                    .replaceAll("src=\'/"+StringUtils.tikuimages, "src=\'" + value+StringUtils.tikuimages);
+            return replaceImagePath(content, value);
         }
         return content;
     }

+ 180 - 0
ie-system/src/main/java/com/ruoyi/system/utils/downloadpaper/DownloadPaperUtils.java

@@ -0,0 +1,180 @@
+package com.ruoyi.system.utils.downloadpaper;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.ruoyi.learn.domain.LearnQuestions;
+import org.apache.commons.lang3.StringUtils;
+
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+
+public class DownloadPaperUtils {
+
+    public static byte[] download(String paperName, List<LearnQuestions> questions) throws Exception {
+        boolean test = false;
+        String url;
+        // String url = "http://152.136.133.33:8808/paperdownload-0.0.1-SNAPSHOT/paperdownload/paperDownload/";
+        if (test) {
+            url = "http://47.98.207.100:8081/test/user/testPaperDownload"; // 测试用
+        } else {
+            url = "http://152.136.133.33:8808/paperdownload-0.0.1-SNAPSHOT/paperdownload/paperDownload/";
+        }
+        // String postData =
+        // IOUtils.toString(getClass().getClassLoader().getResourceAsStream("postdata.json"),
+        // "utf-8");
+        Map<String, String> param = new HashMap<String, String>();
+        param.put("docType", "docx");
+        param.put("answerType", "1");
+        param.put("fontSize", "1");
+        param.put("paperName", paperName);
+        param.put("paperSizeType", "A4");
+        // param.put("paperData", buildPaperData(paperName, questions));
+        if (test) {
+            param.put("type", "2"); // 测试用
+            param.put("postData", buildPaperData(paperName, questions));
+        } else {
+            param.put("paperData", buildPaperData(paperName, questions));
+        }
+        byte[] data = HttpUtils.post(url, param);
+        return data;
+    }
+
+    public static String buildPaperData(String paperName, List<LearnQuestions> questionsList) throws Exception {
+        String[] paperInfos = new String[] { "A4", "1", "docx" };//
+
+        JSONObject paperData = new JSONObject();
+        paperData.put("paperStruType", "1");
+        JSONObject mainTitle = new JSONObject();
+        mainTitle.put("mainTitleExist", true);
+        mainTitle.put("mainTitleName", paperName);
+
+        paperData.put("mainTitle", mainTitle);
+
+        /**
+        JSONObject subTitle = new JSONObject();
+        subTitle.put("subTitleExist", true);
+        subTitle.put("subTitleName", "");
+
+        paperData.put("subTitle", subTitle);
+
+
+        paperData.put("bindingLine", true);
+        paperData.put("securityMark", true);
+        paperData.put("gradeBar", true);
+        paperData.put("questionAndNotes", true);
+        paperData.put("qGradeBar", true);
+         **/
+        paperData.put("fontSize", 2);
+        paperData.put("subjectId", 1);
+        paperData.put("phaseId", 1);
+
+        paperData.put("paperSizeType", paperInfos[0]);
+        paperData.put("answerType", paperInfos[1]);
+        paperData.put("docType", paperInfos[2]);
+
+        /**
+        JSONObject paperInfoBar = new JSONObject();
+        paperInfoBar.put("paperInfoBarExist", true);
+        paperInfoBar.put("paperInfoBarName", "考试时间:120分钟;命题人:");
+        paperData.put("paperInfoBar", paperInfoBar);
+
+        JSONObject studentInfoBar = new JSONObject();
+        studentInfoBar.put("studentInfoBarExist", true);
+        studentInfoBar.put("studentInfoBarName", "学校=>___________姓名:___________班级:___________考号:___________");
+        paperData.put("studentInfoBar", studentInfoBar);
+
+        JSONObject needingAttention = new JSONObject();
+        needingAttention.put("needingAttentionExist", true);
+        needingAttention.put("attention", new String[] { "填写答题卡的内容用2B铅笔填写", "提前 10 分钟收取答题卡" });
+        paperData.put("needingAttention", needingAttention);
+        **/
+        JSONObject notesList1 = new JSONObject();
+        notesList1.put("title", "第I卷(选择题)");
+        notesList1.put("notes", "请点击修改第I卷的文字说明");
+
+        JSONObject notesList2 = new JSONObject();
+        notesList2.put("title", "第II卷(非选择题)");
+        notesList2.put("notes", "请点击修改第I卷的文字说明");
+
+        /**
+        JSONObject subPaperAndNotes = new JSONObject();
+        subPaperAndNotes.put("subPaperAndNotesExist", true);
+        subPaperAndNotes.put("notesList", new Object[] { notesList1, notesList2 });
+
+        paperData.put("subPaperAndNotes", subPaperAndNotes);
+         **/
+        JSONArray $paperQtypeList = new JSONArray();
+        Map<String, JSONObject> qtypeMap = new HashMap<String, JSONObject>();
+        for (LearnQuestions questions : questionsList) {
+            String qtpye = questions.getQtpye();
+            if (!qtypeMap.containsKey(qtpye)) {
+                JSONObject qm = new JSONObject();
+                qm.put("questionFristType", qtpye);
+                qm.put("questionId", questions.getId());
+                qm.put("questionSecondType", "");
+                qm.put("questionDesc", "");
+                qm.put("questionTypeNote", "");
+                qm.put("thisTypeSize", "4");
+                // qm.put("questionsList", new JSONObject());
+                qtypeMap.put(qtpye, qm);
+                $paperQtypeList.add(qm);
+            }
+
+            JSONObject $ques = new JSONObject();
+            $ques.put("questionContent", trim_first_div(questions.getTitle()));
+            $ques.put("questionAnswer", replace_latex(questions.getAnswer2()));// $this->replace_img($question['answer2']),
+            // knowledges
+            JSONObject questionLabelList = new JSONObject();
+            questionLabelList.put("labelName", replace_latex(questions.getKnowledges()));// ,//(string)$this->replace_img($question['knowledges']),
+            questionLabelList.put("importance", 1);
+            $ques.put("questionLabelList", questionLabelList);
+
+            $ques.put("questionSource", "");
+            $ques.put("questionDifficulty", questions.getDiff());
+            $ques.put("questionAnswerInfo", trim_first_div(questions.getParse()));
+
+            // options
+            String $options = "<table>";
+            if (StringUtils.isNotBlank(questions.getOptionA()))
+                $options = $options + "<tr><td>A." + replace_latex(questions.getOptionA()) + "</td></tr>";
+            if (StringUtils.isNotBlank(questions.getOptionB()))
+                $options = $options + "<tr><td>B." + replace_latex(questions.getOptionB()) + "</td></tr>";
+            if (StringUtils.isNotBlank(questions.getOptionC()))
+                $options = $options + "<tr><td>C." + replace_latex(questions.getOptionC()) + "</td></tr>";
+            if (StringUtils.isNotBlank(questions.getOptionD()))
+                $options = $options + "<tr><td>D." + replace_latex(questions.getOptionD()) + "</td></tr>";
+            if (StringUtils.isNotBlank(questions.getOptionE()))
+                $options = $options + "<tr><td>E." + replace_latex(questions.getOptionE()) + "</td></tr>";
+            $options = $options + "</table>";
+            if ($options == "<table></table>")
+                $options = "";
+            $ques.put("questionSelection", $options);
+
+            JSONObject qm = (JSONObject) qtypeMap.get(qtpye);
+            JSONArray quesLIst = (JSONArray) qm.get("questionsList");
+            if (quesLIst == null) {
+                quesLIst = new JSONArray();
+                qm.put("questionsList", quesLIst);
+            }
+            quesLIst.add($ques);
+        }
+        paperData.put("questionsTypeList", $paperQtypeList);
+
+        String result = replace_latex(JSONObject.toJSONString(paperData));
+//        System.out.println(result);
+        return result;
+    }
+
+    private static String trim_first_div(String title) {
+        return title;
+    }
+
+    private static String replace_latex(String str) {
+        if (str == null) {
+            return str;
+        }
+        return str.replace("&lt;", "<").replace("&gt;", ">");
+    }
+}

+ 284 - 0
ie-system/src/main/java/com/ruoyi/system/utils/downloadpaper/HttpUtils.java

@@ -0,0 +1,284 @@
+package com.ruoyi.system.utils.downloadpaper;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.PrintWriter;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.commons.collections4.MapUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.http.Consts;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.NameValuePair;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.entity.UrlEncodedFormEntity;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.entity.ContentType;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.entity.mime.MultipartEntityBuilder;
+import org.apache.http.entity.mime.content.ByteArrayBody;
+import org.apache.http.entity.mime.content.StringBody;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.message.BasicNameValuePair;
+import org.apache.http.util.EntityUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class HttpUtils {
+    private static Logger logger = LoggerFactory.getLogger(HttpUtils.class);
+
+    public static String get(String url) {
+        try {
+            HttpClient client = HttpClientBuilder.create().build();
+            HttpGet request = new HttpGet(url);
+            HttpResponse response = client.execute(request);
+            BufferedReader rd = new BufferedReader(new InputStreamReader(response.getEntity().getContent(), "utf-8"));
+
+            StringBuffer result = new StringBuffer();
+            String line = "";
+            while ((line = rd.readLine()) != null) {
+                result.append(line);
+            }
+            return result.toString();
+        } catch (IOException e) {
+            throw new RuntimeException("网络异常");
+        }
+
+    }
+
+    public static String postWithParameters(String url, Map<String, String> paras) {
+        try {
+            HttpClient client = HttpClientBuilder.create().build();
+            HttpPost post = new HttpPost(url);
+
+            if (MapUtils.isNotEmpty(paras)) {
+                List<NameValuePair> urlParameters = new ArrayList<NameValuePair>();
+                for (String key : paras.keySet()) {
+                    urlParameters.add(new BasicNameValuePair(key, paras.get(key)));
+                }
+                post.setEntity(new UrlEncodedFormEntity(urlParameters));
+            }
+            HttpResponse response = client.execute(post);
+
+            BufferedReader rd = new BufferedReader(new InputStreamReader(response.getEntity().getContent(), "utf-8"));
+
+            StringBuilder result = new StringBuilder();
+            String line = "";
+            while ((line = rd.readLine()) != null) {
+                result.append(line);
+            }
+            return result.toString();
+        } catch (IOException e) {
+            throw new RuntimeException(e.getMessage(), e);
+        }
+    }
+
+    public static byte[] postWithJsonRequestBody(String url, String requestBody) {
+        try {
+            HttpClient client = HttpClientBuilder.create().build();
+            HttpPost post = new HttpPost(url);
+
+            if (StringUtils.isNotBlank(requestBody)) {
+                post.setEntity(new StringEntity(requestBody, "utf-8"));
+            }
+            post.setHeader("Content-type", "application/json");
+
+            HttpResponse response = client.execute(post);
+            InputStream is = response.getEntity().getContent();
+
+            ByteArrayOutputStream baos = new ByteArrayOutputStream();
+            int cache = 1024;
+            byte[] buffer = new byte[cache];
+            int ch = 0;
+            while ((ch = is.read(buffer)) != -1) {
+                baos.write(buffer, 0, ch);
+            }
+            is.close();
+            
+            return baos.toByteArray();
+        } catch (IOException e) {
+            throw new RuntimeException(e.getMessage(), e);
+        }
+    }
+
+    /**
+     * 发送post请求
+     * 
+     * @param url
+     * @param header
+     * @param body
+     * @return
+     */
+    public static String doPost(String url, Map<String, String> header, String body) {
+        String result = "";
+        BufferedReader in = null;
+        PrintWriter out = null;
+        try {
+            // 设置 url
+            URL realUrl = new URL(url);
+            HttpURLConnection connection = (HttpURLConnection) realUrl.openConnection();
+            // 设置 header
+            for (String key : header.keySet()) {
+                connection.setRequestProperty(key, header.get(key));
+            }
+            // 设置请求 body
+            connection.setDoOutput(true);
+            connection.setDoInput(true);
+
+            // 设置连接超时和读取超时时间
+            connection.setConnectTimeout(20000);
+            connection.setReadTimeout(20000);
+            try {
+                out = new PrintWriter(connection.getOutputStream());
+                // 保存body
+                out.print(body);
+                // 发送body
+                out.flush();
+            } catch (Exception e) {
+                logger.error(e.getMessage(), e);
+            }
+
+            try {
+                // 获取响应body
+                in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
+                String line;
+                while ((line = in.readLine()) != null) {
+                    result += line;
+                }
+            } catch (Exception e) {
+                logger.error(e.getMessage(), e);
+            }
+        } catch (Exception e) {
+            logger.error(e.getMessage(), e);
+            // return null;
+        }
+        return result;
+    }
+
+    /**
+     * 发送get请求
+     * 
+     * @param url
+     * @param header
+     * @return
+     */
+    public static String doGet(String url, Map<String, String> header) {
+        String result = "";
+        BufferedReader in = null;
+        try {
+            // 设置 url
+            URL realUrl = new URL(url);
+            URLConnection connection = realUrl.openConnection();
+            // 设置 header
+            for (String key : header.keySet()) {
+                connection.setRequestProperty(key, header.get(key));
+            }
+            // 设置请求 body
+            in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
+            String line;
+            while ((line = in.readLine()) != null) {
+                result += line;
+            }
+        } catch (Exception e) {
+            return null;
+        }
+        return result;
+    }
+
+    public static List<NameValuePair> convertMapToPair(Map<String, String> params) {
+        List<NameValuePair> pairs = new ArrayList<NameValuePair>();
+        for (Map.Entry<String, String> entry : params.entrySet()) {
+            pairs.add(new BasicNameValuePair(entry.getKey(), entry.getValue()));
+        }
+        return pairs;
+    }
+
+    public static byte[] post(String url, Map<String, String> param) {
+        String result = null;
+        CloseableHttpResponse httpResponse = null;
+        CloseableHttpClient httpClient = HttpClients.createDefault();
+        HttpPost httpPost = new HttpPost(url);
+
+        try {
+            httpPost.setEntity(new UrlEncodedFormEntity(convertMapToPair(param), "utf-8"));
+            httpResponse = httpClient.execute(httpPost);
+            InputStream is = httpResponse.getEntity().getContent();
+
+            ByteArrayOutputStream baos = new ByteArrayOutputStream();
+            int cache = 1024;
+            byte[] buffer = new byte[cache];
+            int ch = 0;
+            while ((ch = is.read(buffer)) != -1) {
+                baos.write(buffer, 0, ch);
+            }
+            is.close();
+            
+            return baos.toByteArray();
+        } catch (Exception e) {
+            logger.error(e.getMessage(), e);
+            throw new RuntimeException(e.getMessage(), e);
+        } finally {
+            try {
+                if (httpResponse != null) {
+                    httpResponse.close();
+                }
+            } catch (IOException e) {
+                logger.error(e.getMessage(), e);
+            }
+            try {
+                if (httpClient != null) {
+                    httpClient.close();
+                }
+            } catch (IOException e) {
+                logger.error(e.getMessage(), e);
+            }
+        }
+
+    }
+
+    public static String postMulti(String url, Map<String, String> param, byte[] body) {
+        String result = null;
+        CloseableHttpResponse httpResponse = null;
+        CloseableHttpClient httpClient = HttpClients.createDefault();
+        HttpPost httpPost = new HttpPost(url);
+
+        MultipartEntityBuilder reqEntity = MultipartEntityBuilder.create();
+        reqEntity.addPart("content", new ByteArrayBody(body, ContentType.DEFAULT_BINARY, param.get("slice_id")));
+
+        for (Map.Entry<String, String> entry : param.entrySet()) {
+            StringBody value = new StringBody(entry.getValue(), ContentType.create("text/plain", Consts.UTF_8));
+            reqEntity.addPart(entry.getKey(), value);
+        }
+        HttpEntity httpEntiy = reqEntity.build();
+
+        try {
+            httpPost.setEntity(httpEntiy);
+            httpResponse = httpClient.execute(httpPost);
+            result = EntityUtils.toString(httpResponse.getEntity());
+        } catch (Exception e) {
+            logger.error(e.getMessage(), e);
+        } finally {
+            try {
+                if (httpClient != null) {
+                    httpClient.close();
+                }
+            } catch (IOException e) {
+                logger.error(e.getMessage(), e);
+            }
+        }
+        return result;
+    }
+}