From 48be5321afe6605c739996b36261f01ea39d535d Mon Sep 17 00:00:00 2001
From: Mlxa0324 <mlx950324@163.com>
Date: Mon, 26 Dec 2022 21:09:28 +0800
Subject: [PATCH] =?UTF-8?q?=E5=AF=BC=E5=85=A5word=E8=AF=95=E9=A2=98?=
 =?UTF-8?q?=EF=BC=8C=E6=9C=AA=E5=AE=8C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../com/ibeetl/admin/core/util/BeanUtil.java  |  83 ++++++
 web/pom.xml                                   |  27 ++
 .../java/cn/jlw/util/wmf2png/WmfToPng.java    | 207 +++++++++++++
 .../TeacherOpenCourseStudentSigninLogDao.java |   3 +
 ...cherOpenCourseStudentSigninSettingDao.java |   4 +-
 .../ibeetl/jlw/entity/MyXWPFParagraph.java    | 276 ++++++++++++++++++
 ...TeacherOpenCourseStudentSigninSetting.java |   6 +-
 .../vo/SummaryBySigninSettingIdsVO.java       |  39 +++
 .../jlw/service/ResourcesQuestionService.java | 256 ++++++++++++++++
 ...OpenCourseStudentSigninSettingService.java |  34 +++
 .../jlw/service/strategy/StrategyContext.java | 154 ++++++++++
 .../service/strategy/WordQuestionAnswer.java  |  46 +++
 .../service/strategy/WordQuestionOption.java  |  53 ++++
 .../service/strategy/WordQuestionOther.java   |  48 +++
 .../service/strategy/WordQuestionStem.java    |  57 ++++
 .../service/strategy/WordQuestionType.java    |  63 ++++
 .../jlw/service/strategy/WordStrategy.java    |  38 +++
 .../jlw/web/ResourcesQuestionController.java  |   9 +
 .../jlw/teacherOpenCourseStudentSigninLog.md  |  23 +-
 19 files changed, 1422 insertions(+), 4 deletions(-)
 create mode 100644 web/src/main/java/cn/jlw/util/wmf2png/WmfToPng.java
 create mode 100644 web/src/main/java/com/ibeetl/jlw/entity/MyXWPFParagraph.java
 create mode 100644 web/src/main/java/com/ibeetl/jlw/entity/vo/SummaryBySigninSettingIdsVO.java
 create mode 100644 web/src/main/java/com/ibeetl/jlw/service/strategy/StrategyContext.java
 create mode 100644 web/src/main/java/com/ibeetl/jlw/service/strategy/WordQuestionAnswer.java
 create mode 100644 web/src/main/java/com/ibeetl/jlw/service/strategy/WordQuestionOption.java
 create mode 100644 web/src/main/java/com/ibeetl/jlw/service/strategy/WordQuestionOther.java
 create mode 100644 web/src/main/java/com/ibeetl/jlw/service/strategy/WordQuestionStem.java
 create mode 100644 web/src/main/java/com/ibeetl/jlw/service/strategy/WordQuestionType.java
 create mode 100644 web/src/main/java/com/ibeetl/jlw/service/strategy/WordStrategy.java

diff --git a/admin-core/src/main/java/com/ibeetl/admin/core/util/BeanUtil.java b/admin-core/src/main/java/com/ibeetl/admin/core/util/BeanUtil.java
index b8ead246..2aff0450 100644
--- a/admin-core/src/main/java/com/ibeetl/admin/core/util/BeanUtil.java
+++ b/admin-core/src/main/java/com/ibeetl/admin/core/util/BeanUtil.java
@@ -196,4 +196,87 @@ public class BeanUtil extends cn.hutool.core.bean.BeanUtil {
         }).collect(Collectors.toList());
     }
 
+    /**
+     * 复制集合中的Bean属性<br>
+     * 此方法遍历集合中每个Bean,复制其属性后加入一个新的{@link List}中。
+     *
+     * @param collection  原Bean集合
+     * @param targetType  目标Bean类型
+     * @param copyOptions 拷贝选项
+     * @param <T>         Bean类型
+     * @return 复制后的List
+     * @since 5.6.4
+     */
+    public static <T extends BaseEntity> List<T> copyToListSupportExtMap(Collection<? extends BaseEntity> collection, Class<T> targetType, CopyOptions copyOptions) {
+        if (null == collection) {
+            return null;
+        }
+        if (collection.isEmpty()) {
+            return new ArrayList<>(0);
+        }
+        return collection.stream().map((source) -> {
+            final T target = ReflectUtil.newInstanceIfPossible(targetType);
+            copyProperties(source, target, copyOptions);
+            source.getTails().forEach((k, v) -> {
+                target.set(k, v);
+            });
+            return target;
+        }).collect(Collectors.toList());
+    }
+
+    /**
+     * 复制集合中的Bean属性<br>
+     * 此方法遍历集合中每个Bean,复制其属性后加入一个新的{@link List}中。
+     *
+     * @param collection  原Bean集合
+     * @param targetType  目标Bean类型
+     * @param <T>         Bean类型
+     * @return 复制后的List
+     * @since 5.6.4
+     */
+    public static <T extends BaseEntity> List<T> copyToListSupportExtMap(Collection<? extends BaseEntity> collection, Class<T> targetType) {
+        if (null == collection) {
+            return null;
+        }
+        if (collection.isEmpty()) {
+            return new ArrayList<>(0);
+        }
+        return collection.stream().map((source) -> {
+            final T target = ReflectUtil.newInstanceIfPossible(targetType);
+            copyProperties(source, target);
+            source.getTails().forEach((k, v) -> {
+                target.set(k, v);
+            });
+            return target;
+        }).collect(Collectors.toList());
+    }
+
+    /**
+     * 复制集合中的Bean属性<br>
+     * 此方法遍历集合中每个Bean,复制其属性后加入一个新的{@link List}中。
+     *
+     * @param source  原Bean
+     * @param target  目标Bean
+     * @return 复制后的目标Bean
+     * @since 5.6.4
+     */
+    public static <T extends BaseEntity, R extends BaseEntity> R copyAllPropertiesSupportExtMap(T source, R target) {
+//        copyProperties(source, target);
+
+        for (Field declaredField : source.getClass().getDeclaredFields()) {
+            String name = declaredField.getName();
+            declaredField.setAccessible(true);
+            try {
+                target.set(name, declaredField.get(source));
+            } catch (IllegalAccessException e) {
+            }finally {
+                declaredField.setAccessible(false);
+            }
+        }
+//        for (Map.Entry<String, Object> so : source.getTails().entrySet()) {
+//            target.set(so.getKey(), so.getValue());
+//        }
+        return target;
+    }
+
 }
diff --git a/web/pom.xml b/web/pom.xml
index d6fcce84..7f9858db 100644
--- a/web/pom.xml
+++ b/web/pom.xml
@@ -246,6 +246,33 @@
 			<groupId>org.quartz-scheduler</groupId>
 			<artifactId>quartz</artifactId>
 		</dependency>
+		<dependency>
+			<groupId>org.apache.xmlgraphics</groupId>
+			<artifactId>batik-all</artifactId>
+			<version>1.14</version>
+			<type>pom</type>
+			<exclusions>
+				<exclusion>
+					<groupId>commons-io</groupId>
+					<artifactId>commons-io</artifactId>
+				</exclusion>
+				<exclusion>
+					<groupId>org.slf4j</groupId>
+					<artifactId>slf4j-api</artifactId>
+				</exclusion>
+			</exclusions>
+		</dependency>
+		<dependency>
+			<groupId>net.arnx</groupId>
+			<artifactId>wmf2svg</artifactId>
+			<version>0.9.11</version>
+			<exclusions>
+				<exclusion>
+					<groupId>org.slf4j</groupId>
+					<artifactId>slf4j-api</artifactId>
+				</exclusion>
+			</exclusions>
+		</dependency>
     </dependencies>
 <!--	<build>-->
 <!--		<plugins>-->
diff --git a/web/src/main/java/cn/jlw/util/wmf2png/WmfToPng.java b/web/src/main/java/cn/jlw/util/wmf2png/WmfToPng.java
new file mode 100644
index 00000000..d6acd348
--- /dev/null
+++ b/web/src/main/java/cn/jlw/util/wmf2png/WmfToPng.java
@@ -0,0 +1,207 @@
+package cn.jlw.util.wmf2png;
+
+import net.arnx.wmf2svg.gdi.svg.SvgGdi;
+import net.arnx.wmf2svg.gdi.wmf.WmfParser;
+import org.apache.batik.transcoder.TranscoderException;
+import org.apache.batik.transcoder.TranscoderInput;
+import org.apache.batik.transcoder.TranscoderOutput;
+import org.apache.batik.transcoder.image.ImageTranscoder;
+import org.apache.batik.transcoder.image.JPEGTranscoder;
+import org.apache.batik.transcoder.wmf.tosvg.WMFTranscoder;
+import org.apache.commons.lang3.StringUtils;
+import org.w3c.dom.Document;
+
+import javax.xml.transform.OutputKeys;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
+import java.io.*;
+import java.util.zip.GZIPOutputStream;
+
+public class WmfToPng {
+
+    enum FileType {
+        SVG,
+        PNG;
+    }
+
+    public static void main(String[] args) {
+
+        String result = convert("d:\\80814378377.wmf");
+        System.out.println(result);
+
+    }
+
+    @Deprecated
+    public static String convert2(String path) throws TranscoderException,
+            IOException {
+        String wmfPath = path;
+        File wmf = new File(wmfPath);
+        FileInputStream wmfStream = new FileInputStream(wmf);
+        ByteArrayOutputStream imageOut = new ByteArrayOutputStream();
+        int noOfByteRead = 0;
+        while ((noOfByteRead = wmfStream.read()) != -1) {
+            imageOut.write(noOfByteRead);
+        }
+        imageOut.flush();
+        wmfStream.close();
+        // wmf 转换为svg
+        WMFTranscoder transcoder = new WMFTranscoder();
+        // TranscodingHints hints = new TranscodingHints();
+        // hints.put(WMFTranscoder.KEY_HEIGHT, 1000f);
+        // hints.put(WMFTranscoder.KEY_WIDTH, 1500f);
+        // transcoder.setTranscodingHints(hints);
+        TranscoderInput input = new TranscoderInput(new ByteArrayInputStream(
+                imageOut.toByteArray()));
+        ByteArrayOutputStream svg = new ByteArrayOutputStream();
+        TranscoderOutput output = new TranscoderOutput(svg);
+        transcoder.transcode(input, output);
+        String svgFile = StringUtils.replace(wmfPath, "wmf", "svg");
+        FileOutputStream fileOut = new FileOutputStream(svgFile);
+        fileOut.write(svg.toByteArray());
+        fileOut.flush();
+        fileOut.close();
+        svg.close();
+        // svg -> jpg
+        ImageTranscoder it = new JPEGTranscoder();
+        it.addTranscodingHint(JPEGTranscoder.KEY_QUALITY, new Float(0.5f));
+        ByteArrayOutputStream jpg = new ByteArrayOutputStream();
+        it.transcode(new TranscoderInput(new ByteArrayInputStream(svg
+                .toByteArray())), new TranscoderOutput(jpg));
+        String jpgFile = StringUtils.replace(wmfPath, "wmf", "jpg");
+        FileOutputStream jpgOut = new FileOutputStream(jpgFile);
+        jpgOut.write(jpg.toByteArray());
+        jpgOut.flush();
+        jpgOut.close();
+        jpg.close();
+        // Filor.deleteFile(svgFile);// 删除掉中间文件
+        return jpgFile;
+    }
+
+    public static String convert(String path, FileType fileType) {
+        try {
+            String svgFile = StringUtils.replace(path, "wmf", "svg");
+            String filePath = StringUtils.replace(path, "wmf", "png");
+            switch (fileType) {
+                case PNG:
+                    wmfToSvg(path, svgFile);
+                    svgToJpg(svgFile, filePath);
+                    break;
+                case SVG:
+                    wmfToSvg(path, svgFile);
+                    filePath = svgFile;
+                    break;
+            }
+
+            return filePath;
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return null;
+
+    }
+    public static String convert(String path) {
+        return convert(path, FileType.PNG);
+
+    }
+
+    /**
+     * 将svg转化为JPG
+     *
+     * @param src
+     * @param dest
+     */
+    public static String  svgToJpg(String src, String dest) {
+        FileOutputStream jpgOut = null;
+        FileInputStream svgStream = null;
+        ByteArrayOutputStream svgOut = null;
+        ByteArrayInputStream svgInputStream = null;
+        ByteArrayOutputStream jpg = null;
+        try {
+            // 获取到svg文件
+            File svg = new File(src);
+            svgStream = new FileInputStream(svg);
+            svgOut = new ByteArrayOutputStream();
+            // 获取到svg的stream
+            int noOfByteRead = 0;
+            while ((noOfByteRead = svgStream.read()) != -1) {
+                svgOut.write(noOfByteRead);
+            }
+            JPEGTranscoder it = new JPEGTranscoder();
+            it.addTranscodingHint(JPEGTranscoder.KEY_QUALITY, new Float(0.9f));
+            it.addTranscodingHint(ImageTranscoder.KEY_WIDTH, new Float(100));
+            jpg = new ByteArrayOutputStream();
+            svgInputStream = new ByteArrayInputStream(svgOut.toByteArray());
+            it.transcode(new TranscoderInput(svgInputStream),
+                    new TranscoderOutput(jpg));
+            jpgOut = new FileOutputStream(dest);
+            jpgOut.write(jpg.toByteArray());
+        } catch (Exception e) {
+            e.printStackTrace();
+        } finally {
+            try {
+                if (svgInputStream != null) {
+                    svgInputStream.close();
+                }
+                if (jpg != null) {
+                    jpg.close();
+                }
+                if (svgStream != null) {
+                    svgStream.close();
+                }
+                if (svgOut != null) {
+                    svgOut.close();
+                }
+                if (jpgOut != null) {
+                    jpgOut.flush();
+                    jpgOut.close();
+                }
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+        }
+        return dest;
+    }
+
+    /**
+     * 将wmf转换为svg
+     *
+     * @param src
+     * @param dest
+     */
+    public static void wmfToSvg(String src, String dest) {
+        boolean compatible = false;
+        try {
+            InputStream in = new FileInputStream(src);
+            WmfParser parser = new WmfParser();
+            final SvgGdi gdi = new SvgGdi(compatible);
+            parser.parse(in, gdi);
+
+            Document doc = gdi.getDocument();
+            OutputStream out = new FileOutputStream(dest);
+            if (dest.endsWith(".svgz")) {
+                out = new GZIPOutputStream(out);
+            }
+
+            output(doc, out);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    private static void output(Document doc, OutputStream out) throws Exception {
+        TransformerFactory factory = TransformerFactory.newInstance();
+        Transformer transformer = factory.newTransformer();
+        transformer.setOutputProperty(OutputKeys.METHOD, "xml");
+        transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
+        transformer.setOutputProperty(OutputKeys.INDENT, "yes");
+        transformer.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC,
+                "-//W3C//DTD SVG 1.0//EN");
+        transformer.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM,
+                "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd");
+        transformer.transform(new DOMSource(doc), new StreamResult(out));
+        out.flush();
+        out.close();
+    }
+}
diff --git a/web/src/main/java/com/ibeetl/jlw/dao/TeacherOpenCourseStudentSigninLogDao.java b/web/src/main/java/com/ibeetl/jlw/dao/TeacherOpenCourseStudentSigninLogDao.java
index fb50be98..59c0738b 100644
--- a/web/src/main/java/com/ibeetl/jlw/dao/TeacherOpenCourseStudentSigninLogDao.java
+++ b/web/src/main/java/com/ibeetl/jlw/dao/TeacherOpenCourseStudentSigninLogDao.java
@@ -1,6 +1,7 @@
 package com.ibeetl.jlw.dao;
 
 import com.ibeetl.jlw.entity.TeacherOpenCourseStudentSigninLog;
+import com.ibeetl.jlw.entity.vo.SummaryBySigninSettingIdsVO;
 import com.ibeetl.jlw.entity.vo.TeacherOpenCourseStudentSigninLogSummaryVO;
 import com.ibeetl.jlw.web.query.TeacherOpenCourseStudentSigninLogQuery;
 import org.beetl.sql.core.engine.PageQuery;
@@ -36,4 +37,6 @@ public interface TeacherOpenCourseStudentSigninLogDao extends BaseMapper<Teacher
      * @param teacherOpenCourseId
      */
     TeacherOpenCourseStudentSigninLogSummaryVO summary(Long teacherOpenCourseId);
+
+    List<SummaryBySigninSettingIdsVO> summaryBySigninSettingIds(String signinSettingIds);
 }
diff --git a/web/src/main/java/com/ibeetl/jlw/dao/TeacherOpenCourseStudentSigninSettingDao.java b/web/src/main/java/com/ibeetl/jlw/dao/TeacherOpenCourseStudentSigninSettingDao.java
index a9bc3b73..cdea4a2d 100644
--- a/web/src/main/java/com/ibeetl/jlw/dao/TeacherOpenCourseStudentSigninSettingDao.java
+++ b/web/src/main/java/com/ibeetl/jlw/dao/TeacherOpenCourseStudentSigninSettingDao.java
@@ -5,9 +5,8 @@ import com.ibeetl.jlw.web.query.TeacherOpenCourseStudentSigninSettingQuery;
 import org.beetl.sql.core.engine.PageQuery;
 import org.beetl.sql.mapper.BaseMapper;
 import org.beetl.sql.mapper.annotation.SqlResource;
-import org.springframework.stereotype.Repository;
 import org.beetl.sql.mapper.annotation.Update;
-import org.springframework.stereotype.Component;
+import org.springframework.stereotype.Repository;
 
 import java.util.List;
 
@@ -29,4 +28,5 @@ public interface TeacherOpenCourseStudentSigninSettingDao extends BaseMapper<Tea
     List<TeacherOpenCourseStudentSigninSetting> getByIds(String ids);
     List<TeacherOpenCourseStudentSigninSetting> getValuesByQuery(TeacherOpenCourseStudentSigninSettingQuery teacherOpenCourseStudentSigninSettingQuery);
     List<TeacherOpenCourseStudentSigninSetting> getValuesByQueryNotWithPermission(TeacherOpenCourseStudentSigninSettingQuery teacherOpenCourseStudentSigninSettingQuery);
+
 }
diff --git a/web/src/main/java/com/ibeetl/jlw/entity/MyXWPFParagraph.java b/web/src/main/java/com/ibeetl/jlw/entity/MyXWPFParagraph.java
new file mode 100644
index 00000000..ad6c61cb
--- /dev/null
+++ b/web/src/main/java/com/ibeetl/jlw/entity/MyXWPFParagraph.java
@@ -0,0 +1,276 @@
+package com.ibeetl.jlw.entity;
+
+import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.ReUtil;
+import cn.jlw.util.wmf2png.WmfToPng;
+import com.alibaba.excel.util.StringUtils;
+import com.google.common.collect.Maps;
+import com.microsoft.schemas.vml.impl.CTShapeImpl;
+import lombok.Data;
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.io.FileUtils;
+import org.apache.logging.log4j.util.Strings;
+import org.apache.poi.xwpf.usermodel.*;
+import org.apache.xmlbeans.XmlObject;
+
+import javax.xml.namespace.QName;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.util.*;
+
+/**
+ * <p>
+ *  扩展的Poi XWPFParagraph 类
+ * </p>
+ *
+ * @author mlx
+ * @date 2022/5/29
+ * @modified
+ */
+@Data
+@Slf4j
+public class MyXWPFParagraph {
+
+    private XWPFParagraph xwpfParagraph;
+
+    public MyXWPFParagraph() {
+
+    }
+
+    /**
+     * 重新改写 XWPFParagraph 中的getText方法,适用于现有项目
+     * @param absoluteDirPath      文件夹的绝对路径
+     * @param wordFileName      word文档临时文件的名称
+     * @return
+     */
+    public String getText(String absoluteDirPath, String wordFileName) {
+        StringBuffer out = new StringBuffer();
+        Iterator i$ = this.xwpfParagraph.getIRuns().iterator();
+
+        while(i$.hasNext()) {
+            IRunElement run = (IRunElement)i$.next();
+            if (run instanceof XWPFRun) {
+                XWPFRun xRun = (XWPFRun)run;
+                // 获取该段落所有图片
+                List<XWPFPicture> pics = xRun.getEmbeddedPictures();
+
+                // 只处理该段落的图片,比全文检索更优
+                if (ObjectUtil.isNotEmpty(pics)) {
+                    for (int i = 0; i < pics.size(); i++) {
+                        XWPFPictureData pictureData = pics.get(i).getPictureData();
+                        // 获取到图片的网络地址
+                        String picHttpUrl = createFileReturnHttpUrl(absoluteDirPath,
+                                getNewFileName(wordFileName, i, pictureData), pictureData.getData(), true);
+                        out.append(String.format("<img src=\"%s\"/>", picHttpUrl));
+                    }
+                }
+                if (!xRun.getCTR().isSetRsidDel()) {
+                    List<Map<String, XWPFPictureData>> xwpfPictureDataList = getXWPFPictureDataList(xRun,true);
+                    Map<String, Map<String, String>> styleMap = getStyle(xRun);
+                    for (int i = 0; i < xwpfPictureDataList.size(); i++) {
+                        for (int j = 0; j < xwpfPictureDataList.get(i).values().size(); j++) {
+                            XWPFPictureData pictureData = new ArrayList<>(xwpfPictureDataList.get(i).values()).get(j);
+                            // 资源id
+                            String rid = (new ArrayList<>(xwpfPictureDataList.get(i).keySet())).get(j);
+                            // 获取到图片的网络地址
+                            String picHttpUrl = createFileReturnHttpUrl(absoluteDirPath,
+                                    getNewFileName(wordFileName, 0, pictureData), pictureData.getData(), true);
+                            out.append(String.format("<img src=\"%s\" width=\"%s\" height=\"%s\"/>"
+                                    , picHttpUrl
+                                    , styleMap.getOrDefault(rid, Maps.newHashMap()).getOrDefault("width", "")
+                                    , styleMap.getOrDefault(rid, Maps.newHashMap()).getOrDefault("height", "")
+                                    ));
+                        }
+                    }
+
+                    if (ObjectUtil.isEmpty(xwpfPictureDataList)) {
+                        out.append(xRun);
+                    }
+                }
+            } else if (run instanceof XWPFSDT) {
+                out.append(((XWPFSDT)run).getContent().getText());
+            } else {
+                out.append(run);
+            }
+        }
+
+        out.append(this.xwpfParagraph.getFootnoteText());
+
+        return out.toString();
+    }
+
+    /**
+     * 通过id获取图片
+     * @param blipID    资源id
+     * @return
+     */
+    private XWPFPictureData getParagraphPictures(String blipID) {
+        if (this.xwpfParagraph.getPart() instanceof XWPFDocument) {
+            XWPFDocument part = (XWPFDocument) this.xwpfParagraph.getPart();
+            return part.getPictureDataByID(blipID);
+        }
+        return null;
+    }
+
+    /**
+     * 获取imagedata标签
+     * @param xRun
+     * @return
+     */
+    private XmlObject[] getImageXmlObjectList(XWPFRun xRun) {
+        return xRun.getCTR().selectPath("declare namespace v=\"urn:schemas-microsoft-com:vml\" .//v:imagedata");
+    }
+
+    /**
+     * 获取id
+     * @param xmlObject
+     * @return
+     */
+    private String getId(XmlObject xmlObject) {
+        XmlObject temp = xmlObject.selectAttribute(new QName("http://schemas.openxmlformats.org/officeDocument/2006/relationships", "id", "r"));
+        return null != temp && temp.validate() ? temp.getDomNode().getNodeValue() : "";
+    }
+
+    /**
+     * 获取id
+     * @param xRun
+     * @return
+     */
+    private String getId(XWPFRun xRun) {
+        return getId(xRun.getCTR());
+    }
+
+    /**
+     * 获取样式
+     * @param xRun
+     * @return  图片id对应的样式信息
+     */
+    private Map<String, Map<String, String>> getStyle(XWPFRun xRun) {
+        Map<String, Map<String, String>> res = new HashMap<>(10);
+        try {
+            XmlObject[] xmlObjects = xRun.getCTR().selectPath("declare namespace v='urn:schemas-microsoft-com:vml' .//v:shape");
+
+            for (XmlObject xmlObject : xmlObjects) {
+                String id = ReUtil.get("rId\\w+", xmlObject.toString(), 0);
+                String style = ((CTShapeImpl) xmlObject).xgetStyle().getStringValue();
+                if (StringUtils.isNotBlank(id) && StringUtils.isNotBlank(style)) {
+                    res.put(id, listConvertToMap(style.replaceAll("p.", "px").split("[:=;]")));
+                }
+            }
+        }catch (Exception e) {
+            log.error(e.getMessage());
+        }
+        return res;
+    }
+
+    /**
+     * list转Map
+     * @param arr
+     * @return
+     */
+    private static Map<String, String> listConvertToMap(String[] arr) {
+        Map<String, String> result = new HashMap<>();
+        if (ObjectUtil.isNotEmpty(arr) && arr.length >= 2) {
+            for (int i = 0; i < arr.length - 1; i+=2) {
+                result.put(arr[i], arr[i + 1]);
+            }
+        }
+        return result;
+    }
+
+    /**
+     * WPS编码格式下的图片获取
+     * @param xRun
+     * @return
+     */
+    private List<XWPFPictureData> getXWPFPictureDataList(XWPFRun xRun) {
+        List<XWPFPictureData> result = new ArrayList<>();
+        List<Map<String, XWPFPictureData>> xwpfPictureDataList = getXWPFPictureDataList(xRun, true);
+        for (Map<String, XWPFPictureData> stringXWPFPictureDataMap : xwpfPictureDataList) {
+            result.add((XWPFPictureData) stringXWPFPictureDataMap.values().toArray()[0]);
+        }
+        return result;
+    }
+
+    /**
+     * WPS编码格式下的图片获取
+     * @param xRun
+     * @return
+     */
+    private List<Map<String, XWPFPictureData>> getXWPFPictureDataList(XWPFRun xRun, boolean is) {
+        List<Map<String, XWPFPictureData>> pics = new ArrayList();
+        XmlObject[] picts = getImageXmlObjectList(xRun);
+        XmlObject[] var4 = picts;
+        int var5 = picts.length;
+
+        for(int var6 = 0; var6 < var5; ++var6) {
+            XmlObject pict = var4[var6];
+            String id = getId(pict);
+            if (Strings.isNotBlank(id)) {
+                pics.add(MapUtil.of(id, getParagraphPictures(id)));
+            }
+        }
+        return pics;
+    }
+
+
+    /**
+     * 创建文件,并获取该文件的网络地址
+     * @param absolutePath      绝对路径
+     * @param fileName          文件名称
+     * @param data              文件字节集
+     * @return
+     */
+    public String createFileReturnHttpUrl(String absolutePath, String fileName, byte[] data) {
+        return createFileReturnHttpUrl(absolutePath, fileName, data, false);
+    }
+
+    /**
+     *
+     * @param absolutePath      文件夹相对路径
+     * @param fileName          图片名称
+     * @param data              图片数据
+     * @param tryConvertWmf2Png    尝试 wmf是否转换成png
+     * @return
+     */
+    @SneakyThrows
+    public String createFileReturnHttpUrl(String absolutePath, String fileName, byte[] data, boolean tryConvertWmf2Png) {
+        File file = new File(absolutePath);
+        final String filePath = absolutePath + fileName;
+        String resultFilePath = filePath;
+        if (!file.exists()) {file.mkdirs();}
+        BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(filePath));
+        bos.write(data);
+        bos.close();
+        if (tryConvertWmf2Png && fileName.toLowerCase().contains("wmf")) {
+            /** 原文件修改为png后缀 */
+//            resultFilePath = absolutePath + fileName.replaceAll("\\.[^\\.]+$", ".png");
+            resultFilePath = WmfToPng.convert(filePath);
+            /** 转换完成以后,删除WMF文件 */
+            FileUtils.forceDelete(new File(filePath));
+        }
+//        return FileEntity.absoluteUrl2HttpUrl(resultFilePath);
+
+        return resultFilePath;
+    }
+
+    /**
+     * 获取文件的新命名
+     * @param tempFileName  临时文件的名称
+     * @param index         循环中的索引
+     * @param pictureData   poi 图片对象
+     * @return
+     */
+    public String getNewFileName(String tempFileName, int index, XWPFPictureData pictureData) {
+        // 文件全名称
+//        String rawName = pictureData.getFileName();
+        // 文件类型后缀
+        String fileExt = pictureData.suggestFileExtension();
+        // 获取word里图片的关系id
+        String rid = pictureData.getParent().getRelationId(pictureData);
+        return String.format("%s_%s_%s.%s", tempFileName , rid , index , fileExt);
+    }
+}
diff --git a/web/src/main/java/com/ibeetl/jlw/entity/TeacherOpenCourseStudentSigninSetting.java b/web/src/main/java/com/ibeetl/jlw/entity/TeacherOpenCourseStudentSigninSetting.java
index d3a41245..d8999012 100644
--- a/web/src/main/java/com/ibeetl/jlw/entity/TeacherOpenCourseStudentSigninSetting.java
+++ b/web/src/main/java/com/ibeetl/jlw/entity/TeacherOpenCourseStudentSigninSetting.java
@@ -129,7 +129,11 @@ public class TeacherOpenCourseStudentSigninSetting extends BaseEntity{
 
     // 学生总人数
 
-    @FetchSql("select count(1) from student t where find_in_set(t.class_id, #schoolClassIdsRender#) ")
+    @FetchSql("select count(1) from teacher_open_course_merge_student t " +
+            "inner join student ta on ta.student_id = t.student_id and ta.student_status = 1 " +
+            "where 1 " +
+            "and t.teacher_open_course_id = #teacherOpenCourseId# " +
+            "and find_in_set(ta.class_id, #schoolClassIdsRender#) ")
     @UpdateIgnore
     @InsertIgnore
     private Integer signinStudentTotalCount;
diff --git a/web/src/main/java/com/ibeetl/jlw/entity/vo/SummaryBySigninSettingIdsVO.java b/web/src/main/java/com/ibeetl/jlw/entity/vo/SummaryBySigninSettingIdsVO.java
new file mode 100644
index 00000000..e35b0987
--- /dev/null
+++ b/web/src/main/java/com/ibeetl/jlw/entity/vo/SummaryBySigninSettingIdsVO.java
@@ -0,0 +1,39 @@
+package com.ibeetl.jlw.entity.vo;
+
+import com.ibeetl.admin.core.entity.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 功能描述: <br>
+ *
+ * @author: mlx
+ * @description:
+ * @date: 2022/12/26 19:53
+ * @version: 1.0
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+public class SummaryBySigninSettingIdsVO extends BaseEntity {
+
+    /**
+     * 签到配置ID
+     */
+    private Long teacherOpenCourseStudentSigninSettingId;
+    /**
+     * 签到总人数
+     */
+    private Float signTotalCount;
+    /**
+     * 签到人数
+     */
+    private Float signCount;
+    /**
+     * 缺席人数
+     */
+    private Float signLackCount;
+    /**
+     * 到课率 100最大
+     */
+    private Float attendRate;
+}
diff --git a/web/src/main/java/com/ibeetl/jlw/service/ResourcesQuestionService.java b/web/src/main/java/com/ibeetl/jlw/service/ResourcesQuestionService.java
index 4b5ea55c..a45f65f2 100644
--- a/web/src/main/java/com/ibeetl/jlw/service/ResourcesQuestionService.java
+++ b/web/src/main/java/com/ibeetl/jlw/service/ResourcesQuestionService.java
@@ -19,8 +19,10 @@ import com.ibeetl.jlw.entity.UniversitiesCollegesJurisdictionExperimentalSystem;
 import com.ibeetl.jlw.entity.dto.QuestionSettingDTO;
 import com.ibeetl.jlw.entity.vo.QuestionTypeCountVO;
 import com.ibeetl.jlw.enums.ResourcesQuestionTypeEnum;
+import com.ibeetl.jlw.service.strategy.StrategyContext;
 import com.ibeetl.jlw.web.query.CourseInfoQuery;
 import com.ibeetl.jlw.web.query.ResourcesQuestionQuery;
+import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.poi.hssf.usermodel.HSSFWorkbook;
 import org.apache.poi.ss.usermodel.Cell;
@@ -58,6 +60,7 @@ import static java.util.stream.Collectors.joining;
  */
 
 @Service
+@Slf4j
 @Transactional
 public class ResourcesQuestionService extends CoreBaseService<ResourcesQuestion>{
 
@@ -70,6 +73,8 @@ public class ResourcesQuestionService extends CoreBaseService<ResourcesQuestion>
     @Autowired private CorePlatformService platformService;
     @Autowired private ResourcesQuestionService resourcesQuestionService;
 
+    @Autowired private StrategyContext strategyContext;
+
     public PageQuery<ResourcesQuestion> queryByCondition(PageQuery query){
         PageQuery ret =  resourcesQuestionDao.queryByCondition(query);
         queryListAfter(ret.getList());
@@ -768,4 +773,255 @@ public class ResourcesQuestionService extends CoreBaseService<ResourcesQuestion>
         dictParser(g);
         return g;
     }
+
+//    @SneakyThrows
+//    public JsonResult importQBankWordTemplate(final Integer isPublic, @NotEmpty List<FileEntity> fileEntityList, CoreUser coreUser, String referer) {
+//        // 公共库题目导入,用于下面的部分验证
+//        final String publicReferer = "/jlw/question/importExcel.do";
+//        /** 找出段落, 逐段落处理 */
+//        AtomicReference<FileInputStream> inputStream = new AtomicReference<>();
+//        AtomicReference<XWPFDocument> xDocument = new AtomicReference<>();
+//
+//        //装结果 key:卷子访问地址 value:{标题,题目List}
+//        Map<String,Object[]> resultMap = new HashMap<>();
+//        /** 仅用于存放标题 */
+//        final String[] title = { Strings.EMPTY };
+//        /** 标题暂存,可能是多个 */
+//        final StringBuilder titleStb = new StringBuilder();
+//
+//        /** 循环文件集合 */
+//        Long finalCorSchoolId = corSchoolId;
+//        fileEntityList.forEach(fileEntity -> {
+//            /** 上传文件的临时路径 */
+//            String importPath = fileEntity.getAbsoluteUrl();
+//            /** 新建文件夹绝对路径 */
+//            String absoluteDirPath = String.format("%s%s%s%s%s%s", GetFile.p , SE , "filesystem" , SE , "temp" , SE );
+//            /** 新建文件的绝对路径 */
+//            String absolutePath = absoluteDirPath + fileEntity.getTempName();
+//            //各种流
+//            try {
+//                inputStream.set(new FileInputStream(importPath));
+//                xDocument.set(new XWPFDocument(inputStream.get()));
+//                /** 集合拷贝 */
+//                List<MyXWPFParagraph> myParagraphs = BeanCopyUtil.copyListProperties(xDocument.get().getParagraphs() , MyXWPFParagraph::new, (t, d) -> {
+//                    d.setXwpfParagraph(t);
+//                });
+//
+//                /** 需要存库的题目集合 */
+//                List<ResourcesQuestion> questionList = new CopyOnWriteArrayList<>();
+//                /** 题目类型 */
+//                AtomicReference<StrategyContext.QuestionTypeConcatEnum> typeConcatEnum = new AtomicReference<>();
+//                /** 段落类型 */
+//                AtomicReference<StrategyContext.QuestionParagraphTypeEnum> paragraphTypeEnum = new AtomicReference<>();
+//                /** 每题分数 */
+//                AtomicReference<BigDecimal> questionScore = new AtomicReference<>(BigDecimal.ZERO);
+//                /** 题干 */
+//                AtomicReference<String> questionStem = new AtomicReference<>(Strings.EMPTY);
+//                /** 答案 */
+//                AtomicReference<String> questionAnswer = new AtomicReference<>(Strings.EMPTY);
+//                /** 题目实体 */
+//                AtomicReference<ResourcesQuestion> question = new AtomicReference<>();
+//                /** 题干计数 */
+//                AtomicInteger recordsCount = new AtomicInteger();
+//                /** 遍历所有段落 */
+//                myParagraphs.forEach(e -> {
+//                    /** 策略中的参数设置 */
+//                    strategyContext.setText(e.getText(absoluteDirPath, fileEntity.getTempName())).start(c -> {
+//                        /** 第一行 */
+//                        if (recordsCount.get()  == 1 && StringUtils.isBlank(title[0])) {
+//                            /** 暴力取值 */
+//                            titleStb.append(title[0] = myParagraphs.get(0).getText(absoluteDirPath, fileEntity.getTempName()));
+//                        }
+//                        /** k 策略枚举:v 处理后的段落文字 */
+//                        c.forEach((k, v) -> {
+//                            /** 记录段落类型, 未查询类型之外的类型 */
+//                            if (!k.equals(OTHER)) {paragraphTypeEnum.set(k);}
+//                            switch(k) {
+//                                /** 未匹配到的段落 */
+//                                case OTHER: {
+//                                    /** 如果非标题字符,可能是前一个段落的副行。需要追加到相应的段落中 */
+//                                    insertCorrespondingParagraph(typeConcatEnum, paragraphTypeEnum.get() ,v, question, questionStem, questionAnswer);
+//                                } break;
+//                                /** 题目类型,分值,该类型的总分 */
+//                                case TYPE: {
+//                                    /** 题目类型作用域 */
+//                                    String[] sp = v.split(",");
+//                                    typeConcatEnum.set(StrategyContext.QuestionTypeConcatEnum.matchText(sp[2]));
+//                                    questionScore.set(NumberUtil.toBigDecimal(sp[0]).setScale(1,  RoundingMode.HALF_UP));
+//                                } break;
+//                                /** 题干 */
+//                                case STEM: {
+//                                    final String numStartRegex = "^\\d+[\\.\\、\\.]";
+//                                    if(ReUtil.contains(numStartRegex, v)) {
+//                                        /** 题干的编号需要移除掉 */
+//                                        questionStem.set(v.replaceFirst(numStartRegex, Strings.EMPTY).trim());
+//                                        /** 题数++ */
+//                                        recordsCount.incrementAndGet();
+//                                        /** 清空变量 */
+//                                        questionAnswer.set(Strings.EMPTY);
+//
+//                                        /** 带有编号的段落,是主题干。需要创建新的题目对象 */
+//                                        Question qs = new Question();
+//                                        /** 题目类型,查询插入操作 */
+//                                        QuestionType questionType = selectOrInsert(typeConcatEnum.get(), coreUser);
+//                                        qs.setQuestionTypeId(questionType.getQuestionTypeId());
+//                                        qs.putQuestionTypeConcat(questionType.getQuestionTypeConcat());
+//                                        qs.putQuestionTypeName(questionType.getQuestionTypeName());
+//                                        qs.setSourceCorSchoolId(finalCorSchoolId);
+//                                        qs.setQuestionScore(questionScore.get());
+//                                        qs.setQuestionAddTime(new Date());
+//
+//                                        if (StringUtils.isNotBlank(questionStem.get())){
+//                                            qs.setQuestionStem(questionStem.get().replace("<","&lt;").replace(">","&gt;"));
+//                                        }else {
+//                                            qs.setQuestionStem(questionStem.get());
+//                                        }
+//
+//                                        qs.setQuestionStatus(1);
+//                                        qs.set("sign", 0);
+//                                        qs.setUserId(coreUser.getId());
+//                                        qs.setOrgId(coreUser.getOrgId());
+//
+//                                        question.set(qs);
+//                                        questionList.add(question.get());
+//                                    }
+//                                } break;
+//                                /** 选项 */
+//                                case OPTION: {
+//                                    putQuestionOption(v, question.get());
+//                                } break;
+//                                /** 答案 questionAnswer不包含答案: 字样 */
+//                                case ANSWER: {
+//                                    questionAnswer.set(questionAnswer.get().concat(v));
+//                                    // questionAnswer.get() 默认不包含 【答案】 字样
+//                                    questionAnswer.set(secondFormat(questionAnswer.get(), typeConcatEnum.get()));
+//                                    question.get().setQuestionAnswer(questionAnswer.get());
+//                                } break;
+//                            }
+//                        });
+//                    });
+//                });
+//
+//                if (referer.contains(publicReferer)) {
+//                    if(questionList.size() == 0){
+//                        titleStb.append("</br><font size=\"0.5\" color=\"red\">没有解析到题目</font>");
+//                    }else if(ObjectUtil.isEmpty(typeConcatEnum.get())) {
+//                        titleStb.append("</br><font size=\"0.5\" color=\"red\">没有解析到题型</font>");
+//                    }else {
+//                        titleStb.append("</br><font size=\"0.5\" color=\"blue\">解析结果:");
+//                        titleStb.append(recordsCount.get());
+//                        titleStb.append(" 道题目</font>");
+//                    }
+//                }
+//
+//                Object ids[]        = getCourseIdsNew(title[0], groupByCourseId.get(), groupByCourseName.get());
+//                final Long courseId       = (Long)ids[0];
+//                final Long chapterId      = (Long)ids[1];
+//                String finalTitle   = !referer.contains(publicReferer)? titleStb.toString(): titleStb.append(ids[2] + Strings.EMPTY).toString();
+//
+//                /** 最后检测属性, 并行处理,没有向外部非线程安全的变量写入东西,无需担心丢数据 */
+//                questionList.parallelStream().forEach(item -> {
+//                    item.set("chapterId", ObjectUtil.defaultIfNull(chapterId, courseId));
+//                    questionValidator.checkQuestion(item);
+//                    if (referer.contains(publicReferer)) {
+//                        checkUnique(isPublic, item);
+//                    }
+//                });
+//
+//                /** 本次上传的题目判断唯一性 */
+//                checkLocalQuestionsUnique(questionList);
+//
+//                /** 拼成前端需要的数据 */
+//                resultMap.put(url2HttpUrl(fileEntity.getUrl()),new Object[]{finalTitle,questionList});
+//            } catch (Exception ex) { ex.printStackTrace(); }
+//            finally {
+//                try {inputStream.get().close();xDocument.get().close();}
+//                catch (Exception ioe) {ioe.printStackTrace();}
+//            }
+//        });
+//
+//        return JsonResult.success(resultMap);
+//    }
+//
+//    /**
+//     * 验证本地待上传的题库 是否存在重复,
+//     * 本次上传的题目判断唯一性
+//     * @param questionList
+//     */
+//    private void checkLocalQuestionsUnique(List<Question> questionList) {
+//        if (ObjectUtil.isEmpty(questionList)) { return; }
+//        // 默认给50道题的容量
+//        final Set<String> hashMap = new LinkedHashSet<>(50);
+//        final List<String> strings = new ArrayList<>(50);
+//        for (Question question : questionList) {
+//            String questionToString = question.toString();
+//            // 如果添加失败,则代表题目已经存在
+//            if (!hashMap.add(questionToString)) {
+//                // 告知前端 错误异常
+//                question.set("errMsg", String.format("可能与上面的第 【%s】 题重复",strings.indexOf(questionToString) + 1));
+//            }
+//            else { strings.add(questionToString); }
+//        }
+//
+//        hashMap.clear();
+//        strings.clear();
+//    }
+//
+//    /**
+//     * 检测题目唯一
+//     * @param question
+//     */
+//    public void checkUnique(Integer isPublic, @NotNull Question question) {
+//        /** 正则去尽可能的精准匹配, 模糊查询img标签和()() 符号 */
+//        question.set("questionStemRegexp", null);
+//        try {
+//            question.set("questionStemRegexp", "^" + question.getQuestionStemRegexp() + "$");
+//            if (publicSqlManager.getMapper(QuestionDao.class).checkUnique(question) > 0) {
+//                question.set("errMsg", ObjectUtil.defaultIfNull(question.get("errMsg"), "") + "该题目可能存在重复!");
+//            }
+//        }catch (Exception e){
+//            log.error("报错了:"+question.getQuestionStemRegexp());
+//            e.printStackTrace();
+//        }
+//    }
+//
+//    /**
+//     * 格式化答案
+//     * <ol>
+//     * <p>情况一:</p>
+//     * <li><p>ABC</p> 只取 ABC</li>
+//     * <li>转大写</li>
+//     * <li>ABC => 转换成A,B,C</li>
+//     * </ol>
+//     * <ol>
+//     * <p>情况二:</p>
+//     * <li><p>对</p> 只取 对</li>
+//     * <li>转大写</li>
+//     *
+//     * @param QuestionAnswer 答案
+//     * @param questionTypeConcatEnum
+//     * @return
+//     */
+//    public String secondFormat(String questionAnswer, StrategyContext.QuestionTypeConcatEnum typeConcatEnum) {
+//        if (StringUtils.isBlank(questionAnswer)) { return questionAnswer; }
+//        // 去除各种空格
+//        questionAnswer = org.springframework.util.StringUtils.trimAllWhitespace(questionAnswer);
+//        // <p>ABC</p>  只获取ABC 删除所有</>标签元素。
+//        String replaced = questionAnswer.replaceAll(" |</?[a-zA-Z\\u4e00-\\u9fa5]*?[^<]>", "");
+//        // 取中文或者字母, 也有移除空格等符号的操作
+//        String res = ObjectUtil.defaultIfNull(ReUtil.getGroup0("[A-Za-z\\u4e00-\\u9fa5]+", replaced), replaced).toUpperCase();
+//        // 如果只有字母,则拆分规则: ABC => A,B,C
+//        if (typeConcatEnum.equals(MULTIPLE) && ReUtil.isMatch("^[A-Za-z]+$",res)) {
+//            // ABC => A,B,C
+//            String[] split = res.split("");
+//            Arrays.sort(split);
+//            res = ArrayUtil.join(split, ",");
+//        }
+//        // 判断题逻辑
+//        if (typeConcatEnum.equals(DECIDE) && ReUtil.isMatch("(正确)|(错误)|A|B",res)) {
+//            // ABC => A,B,C
+//            res = res.replaceAll("(正确)|A", "对").replaceAll("(错误)|B", "错");
+//        }
+//        return res;
+//    }
 }
\ No newline at end of file
diff --git a/web/src/main/java/com/ibeetl/jlw/service/TeacherOpenCourseStudentSigninSettingService.java b/web/src/main/java/com/ibeetl/jlw/service/TeacherOpenCourseStudentSigninSettingService.java
index 571b425b..a4f81176 100644
--- a/web/src/main/java/com/ibeetl/jlw/service/TeacherOpenCourseStudentSigninSettingService.java
+++ b/web/src/main/java/com/ibeetl/jlw/service/TeacherOpenCourseStudentSigninSettingService.java
@@ -1,6 +1,7 @@
 package com.ibeetl.jlw.service;
 
 import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.util.ObjectUtil;
 import cn.jlw.util.ToolUtils;
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONObject;
@@ -8,8 +9,10 @@ import com.ibeetl.admin.core.service.CoreBaseService;
 import com.ibeetl.admin.core.util.PlatformException;
 import com.ibeetl.admin.core.web.JsonResult;
 import com.ibeetl.admin.core.web.JsonReturnCode;
+import com.ibeetl.jlw.dao.TeacherOpenCourseStudentSigninLogDao;
 import com.ibeetl.jlw.dao.TeacherOpenCourseStudentSigninSettingDao;
 import com.ibeetl.jlw.entity.TeacherOpenCourseStudentSigninSetting;
+import com.ibeetl.jlw.entity.vo.SummaryBySigninSettingIdsVO;
 import com.ibeetl.jlw.enums.StartStatusEnum;
 import com.ibeetl.jlw.validator.TeacherOpenCourseStudentSigninSettingQueryValidator;
 import com.ibeetl.jlw.web.query.TeacherOpenCourseStudentSigninSettingQuery;
@@ -26,6 +29,10 @@ import javax.validation.constraints.NotNull;
 import java.util.ArrayList;
 import java.util.Date;
 import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.stream.Collectors;
+
+import static com.ibeetl.admin.core.util.BeanUtil.copyAllPropertiesSupportExtMap;
 
 /**
  * 学生签到设置 Service
@@ -39,14 +46,41 @@ import java.util.List;
 public class TeacherOpenCourseStudentSigninSettingService extends CoreBaseService<TeacherOpenCourseStudentSigninSetting> implements DeleteResourcesBy{
 
     @Autowired private TeacherOpenCourseStudentSigninSettingDao teacherOpenCourseStudentSigninSettingDao;
+    @Autowired private TeacherOpenCourseStudentSigninLogDao teacherOpenCourseStudentSigninLogDao;
     @Autowired private TeacherOpenCourseStudentSigninSettingQueryValidator settingQueryValidator;
 
     public PageQuery<TeacherOpenCourseStudentSigninSetting>queryByCondition(PageQuery query){
         PageQuery ret =  teacherOpenCourseStudentSigninSettingDao.queryByCondition(query);
         queryListAfter(ret.getList());
+//        setSummaryInfo(ret.getList());
         return ret;
     }
 
+    /**
+     * 根据签到ID,设置汇总数据
+     * @param list
+     */
+    private void setSummaryInfo(List<TeacherOpenCourseStudentSigninSetting> list) {
+        if (ObjectUtil.isNotEmpty(list)) {
+            String signinSettingIds = list.stream().map(item -> item.getTeacherOpenCourseStudentSigninSettingId().toString()).collect(Collectors.joining(","));
+            List<SummaryBySigninSettingIdsVO> vos = teacherOpenCourseStudentSigninLogDao.summaryBySigninSettingIds(signinSettingIds);
+
+            CopyOnWriteArrayList<SummaryBySigninSettingIdsVO> vosSync = new CopyOnWriteArrayList<>(vos);
+
+            for (SummaryBySigninSettingIdsVO vo : vosSync) {
+                Long settingId = vo.getTeacherOpenCourseStudentSigninSettingId();
+                for (TeacherOpenCourseStudentSigninSetting signinSetting : list) {
+                    // 比对id,进行值拷贝操作了
+                    if (signinSetting.getTeacherOpenCourseStudentSigninSettingId().equals(settingId)) {
+                        copyAllPropertiesSupportExtMap(vo, signinSetting);
+                        vosSync.removeIf(item -> item.getTeacherOpenCourseStudentSigninSettingId().equals(settingId));
+                        continue;
+                    }
+                }
+            }
+        }
+    }
+
     public PageQuery<TeacherOpenCourseStudentSigninSetting>queryByConditionQuery(PageQuery query){
         PageQuery ret =  teacherOpenCourseStudentSigninSettingDao.queryByConditionQuery(query);
         queryListAfter(ret.getList());
diff --git a/web/src/main/java/com/ibeetl/jlw/service/strategy/StrategyContext.java b/web/src/main/java/com/ibeetl/jlw/service/strategy/StrategyContext.java
new file mode 100644
index 00000000..10b53cdb
--- /dev/null
+++ b/web/src/main/java/com/ibeetl/jlw/service/strategy/StrategyContext.java
@@ -0,0 +1,154 @@
+package com.ibeetl.jlw.service.strategy;
+
+import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.util.ReUtil;
+import lombok.AccessLevel;
+import lombok.Data;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.experimental.Accessors;
+import org.apache.commons.lang3.StringUtils;
+import org.assertj.core.util.Lists;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.stereotype.Component;
+
+import javax.validation.constraints.NotNull;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Consumer;
+
+/**
+ * <p>
+ *  策略Context
+ * </p>
+ *
+ * @author mlx
+ * @date 2022/5/30
+ * @modified
+ */
+@Component
+@Lazy
+@Data
+@Accessors(chain=true)
+public class StrategyContext {
+
+    /**
+     * 待处理的文本
+     */
+    private String text;
+
+    /**
+     * 是否保存处理后的字符串到 {@link #result} 属性中
+     */
+    private boolean isSaveResult;
+    /**
+     * 策略集合
+     */
+    @Setter(AccessLevel.PRIVATE)
+    @Getter(AccessLevel.PRIVATE)
+    private List<WordStrategy> strategyList = Lists.newArrayList();
+    /**
+     * 处理之后的结果集
+     */
+    @Setter(AccessLevel.PRIVATE)
+    private List<Map<QuestionParagraphTypeEnum, String>> result = Inner.getInstance();
+
+    static class Inner {
+        private static List<Map<QuestionParagraphTypeEnum, String>> result = new ArrayList<>();
+
+        static List<Map<QuestionParagraphTypeEnum, String>> getInstance() {
+            return result;
+        }
+    }
+
+    public StrategyContext() {
+        strategyList.add(new WordQuestionOption());
+        strategyList.add(new WordQuestionAnswer());
+        strategyList.add(new WordQuestionStem());
+        strategyList.add(new WordQuestionType());
+        // 必须放在最后一个,用于上述判断完以后,都不成立的话,就认为它是标题。
+        strategyList.add(new WordQuestionOther());
+    }
+
+    /**
+     * 开始处理
+     * @param consumer  每一个段落的消费
+     */
+    public void start(Consumer<Map<QuestionParagraphTypeEnum, String>> consumer) {
+
+        for (WordStrategy wordStrategy : strategyList) {
+            if (StringUtils.isNotBlank(text) && wordStrategy.support(text)) {
+//                System.out.print("策略:" + wordStrategy.getClass().getSimpleName() + ", 值:\t");
+                /** 得到处理后的值 */
+                String processedResult = wordStrategy.process(text, consumer);
+                if (isSaveResult) {
+                    result.add(MapUtil.of(wordStrategy.getTypeEnum(), processedResult));
+                }
+                return;
+            }
+        }
+    }
+
+    /**
+     * 题目类型
+     */
+    @Getter
+    public enum QuestionTypeConcatEnum {
+
+        SINGLE("单选题", 1, "单.+题"),
+        MULTIPLE("多选题",2, "多.+题"),
+        DECIDE("判断题",3, "[判断]{1,2}.+题"),
+        SHORT("简答题",4, "简.+题");
+
+        private String typeName;
+        private Integer typeConcat;
+        /**
+         * 取值正则
+         */
+        private String regex;
+
+        QuestionTypeConcatEnum(String typeName, Integer typeConcat, String regex) {
+            this.typeName = typeName;
+            this.typeConcat = typeConcat;
+            this.regex = regex;
+        }
+
+        /**
+         * 文本匹配枚举
+         * @param text
+         * @return
+         */
+        public static QuestionTypeConcatEnum matchText(@NotNull String text) {
+            for (QuestionTypeConcatEnum typeEnum:  QuestionTypeConcatEnum.values()) {
+                if (ReUtil.contains(typeEnum.regex, text)) {
+                    return typeEnum;
+                }
+            }
+            return null;
+        }
+    }
+
+    /**
+     * 段落类型
+     * Integer typeConcat = null;//展示方式(1单选 2多选 3判断 4简答)
+     * int type = 0; // 用于记录当前行内容类型 0标题 1题型 2题干 3选项 4答案
+     */
+    @Getter
+    public enum QuestionParagraphTypeEnum {
+
+        OTHER(WordQuestionOther.class, 0),
+        TYPE(WordQuestionType.class, 1),
+        STEM(WordQuestionStem.class, 2),
+        OPTION(WordQuestionOption.class, 3),
+        ANSWER(WordQuestionAnswer.class, 4);
+
+        private Class<? extends WordStrategy> clz;
+        private Integer type;
+
+        QuestionParagraphTypeEnum(Class<? extends WordStrategy> clz, Integer type) {
+            this.clz = clz;
+            this.type = type;
+        }
+    }
+}
diff --git a/web/src/main/java/com/ibeetl/jlw/service/strategy/WordQuestionAnswer.java b/web/src/main/java/com/ibeetl/jlw/service/strategy/WordQuestionAnswer.java
new file mode 100644
index 00000000..652ba8fe
--- /dev/null
+++ b/web/src/main/java/com/ibeetl/jlw/service/strategy/WordQuestionAnswer.java
@@ -0,0 +1,46 @@
+package com.ibeetl.jlw.service.strategy;
+
+import lombok.Getter;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Consumer;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * <p>
+ *  取题目答案
+ * </p>
+ *
+ * @author mlx
+ * @date 2022/5/30
+ * @modified
+ */
+public class WordQuestionAnswer implements WordStrategy<String> {
+
+    private final String REGEX = "^答案[\\:\\:  ]";
+
+    @Getter
+    private StrategyContext.QuestionParagraphTypeEnum typeEnum = StrategyContext.QuestionParagraphTypeEnum.ANSWER;
+    /**
+     * 例子: A.  A、a、a.
+     * @param paragraph     段落字符串
+     * @return
+     */
+    @Override
+    public boolean support(String paragraph) {
+        Matcher matcher = Pattern.compile(REGEX).matcher(paragraph.trim());
+        return matcher.find() && matcher.start() == 0;
+    }
+
+
+    @Override
+    public String process(String paragraph, Consumer<Map<StrategyContext.QuestionParagraphTypeEnum, String>> consumer) {
+        paragraph = paragraph.replaceFirst(REGEX, "").trim();
+        Map<StrategyContext.QuestionParagraphTypeEnum, String> strategyEnumStringMap = new HashMap<>(1);
+        strategyEnumStringMap.put(typeEnum, paragraph.trim());
+        consumer.accept(strategyEnumStringMap);
+        return paragraph;
+    }
+}
diff --git a/web/src/main/java/com/ibeetl/jlw/service/strategy/WordQuestionOption.java b/web/src/main/java/com/ibeetl/jlw/service/strategy/WordQuestionOption.java
new file mode 100644
index 00000000..184b1007
--- /dev/null
+++ b/web/src/main/java/com/ibeetl/jlw/service/strategy/WordQuestionOption.java
@@ -0,0 +1,53 @@
+package com.ibeetl.jlw.service.strategy;
+
+
+import lombok.Getter;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Consumer;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * <p>
+ *  取题目选项
+ * </p>
+ *
+ * @author mlx
+ * @date 2022/5/30
+ * @modified
+ */
+public class WordQuestionOption implements WordStrategy<String> {
+
+    /**
+     * 举例: A.  A、 A. A  A
+     */
+    private final String REGEX = "^[A-Za-z][\\.\\、\\.  ]";
+
+    @Getter
+    private StrategyContext.QuestionParagraphTypeEnum typeEnum = StrategyContext.QuestionParagraphTypeEnum.OPTION;
+
+    /**
+     * 例子: A.  A、a、a.
+     * @param paragraph     段落字符串
+     * @return
+     */
+    @Override
+    public boolean support(String paragraph) {
+        Matcher matcher = Pattern.compile(REGEX).matcher(paragraph.trim());
+        return matcher.find() && matcher.start() == 0;
+    }
+
+
+    @Override
+    public String process(String paragraph, Consumer<Map<StrategyContext.QuestionParagraphTypeEnum, String>> consumer) {
+        /** 删除这里,可以使用到之前使用的逻辑 */
+//        paragraph = paragraph.replaceFirst(REGEX, "").trim();
+        paragraph = paragraph.trim();
+        Map<StrategyContext.QuestionParagraphTypeEnum, String> map = new HashMap<>(1);
+        map.put(typeEnum, paragraph.trim());
+        consumer.accept(map);
+        return paragraph;
+    }
+}
diff --git a/web/src/main/java/com/ibeetl/jlw/service/strategy/WordQuestionOther.java b/web/src/main/java/com/ibeetl/jlw/service/strategy/WordQuestionOther.java
new file mode 100644
index 00000000..1c6d4208
--- /dev/null
+++ b/web/src/main/java/com/ibeetl/jlw/service/strategy/WordQuestionOther.java
@@ -0,0 +1,48 @@
+package com.ibeetl.jlw.service.strategy;
+
+
+import lombok.Getter;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Consumer;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * <p>
+ *  取word的未匹配到的部分
+ * </p>
+ *
+ * @author mlx
+ * @date 2022/5/30
+ * @modified
+ */
+public class WordQuestionOther implements WordStrategy<String> {
+
+    private final String REGEX = ".*";
+
+    @Getter
+    private StrategyContext.QuestionParagraphTypeEnum typeEnum = StrategyContext.QuestionParagraphTypeEnum.OTHER;
+
+
+    /**
+     * @param paragraph     段落字符串
+     * @return
+     */
+    @Override
+    public boolean support(String paragraph) {
+        Matcher matcher = Pattern.compile(REGEX).matcher(paragraph.trim());
+        return matcher.find() && matcher.start() >= 0;
+    }
+
+
+    @Override
+    public String process(String paragraph, Consumer<Map<StrategyContext.QuestionParagraphTypeEnum, String>> consumer) {
+        Map<StrategyContext.QuestionParagraphTypeEnum, String> map = new HashMap<>(1);
+        paragraph = paragraph.trim();
+        map.put(typeEnum, paragraph);
+        consumer.accept(map);
+        return paragraph;
+    }
+}
diff --git a/web/src/main/java/com/ibeetl/jlw/service/strategy/WordQuestionStem.java b/web/src/main/java/com/ibeetl/jlw/service/strategy/WordQuestionStem.java
new file mode 100644
index 00000000..91f3d6ea
--- /dev/null
+++ b/web/src/main/java/com/ibeetl/jlw/service/strategy/WordQuestionStem.java
@@ -0,0 +1,57 @@
+package com.ibeetl.jlw.service.strategy;
+
+
+import lombok.Getter;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Consumer;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * <p>
+ *  取题干
+ *  因为不同的表  所以必须用不同的策略去区分,题目和选项。
+ * </p>
+ *
+ * @author mlx
+ * @date 2022/5/30
+ * @modified
+ */
+public class WordQuestionStem implements WordStrategy<String> {
+
+    /**
+     * 匹配img标签 或者 1. 1、 开头的这类的题干
+     */
+//    private final String REGEX = "(\\d+[\\.\\、\\.])|(<img.*/>)";
+    private final String REGEX = "^\\d+[\\.\\、\\.]";
+
+    @Getter
+    private StrategyContext.QuestionParagraphTypeEnum typeEnum = StrategyContext.QuestionParagraphTypeEnum.STEM;
+
+    /**
+     * 例子: 12. 12、
+     * @param paragraph     段落字符串
+     * @return
+     */
+    @Override
+    public boolean support(String paragraph) {
+        Matcher matcher = Pattern.compile(REGEX).matcher(paragraph.trim());
+        return matcher.find() && matcher.start() == 0;
+    }
+
+    @Override
+    public String process(String paragraph, Consumer<Map<StrategyContext.QuestionParagraphTypeEnum, String>> consumer) {
+        // 保留图片部分,删除编号部分
+        final String regex = "<img.*/>";
+        if (!(Pattern.compile(regex).matcher(paragraph).find())) {
+            /*.replaceFirst("\\d+[\\.\\、]", "")*/
+            paragraph = paragraph.trim();
+        }
+        Map<StrategyContext.QuestionParagraphTypeEnum, String> map = new HashMap<>(1);
+        map.put(typeEnum, paragraph);
+        consumer.accept(map);
+        return paragraph;
+    }
+}
diff --git a/web/src/main/java/com/ibeetl/jlw/service/strategy/WordQuestionType.java b/web/src/main/java/com/ibeetl/jlw/service/strategy/WordQuestionType.java
new file mode 100644
index 00000000..6aac6834
--- /dev/null
+++ b/web/src/main/java/com/ibeetl/jlw/service/strategy/WordQuestionType.java
@@ -0,0 +1,63 @@
+package com.ibeetl.jlw.service.strategy;
+
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.ReUtil;
+import lombok.Getter;
+import org.apache.logging.log4j.util.Strings;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Consumer;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * <p>
+ *  取题类型
+ * </p>
+ *
+ * @author mlx
+ * @date 2022/5/30
+ * @modified
+ */
+public class WordQuestionType implements WordStrategy<String> {
+
+    private final String REGEX = "([每题]{1,2}.+共?.+分[\\))]?$";
+
+    @Getter
+    private StrategyContext.QuestionParagraphTypeEnum typeEnum = StrategyContext.QuestionParagraphTypeEnum.TYPE;
+
+    /**
+     * @param paragraph     段落字符串
+     * @return
+     */
+    @Override
+    public boolean support(String paragraph) {
+        Matcher matcher = Pattern.compile(REGEX).matcher(paragraph.trim());
+        return matcher.find() && matcher.start() >= 0;
+    }
+
+
+    @Override
+    public String process(String paragraph, Consumer<Map<StrategyContext.QuestionParagraphTypeEnum, String>> consumer) {
+        /** 定义返回集合的长度 */
+        final int len = 3;
+        List<String> allGroup0 = ReUtil.findAllGroup0("\\d*\\.*\\d+", paragraph);
+        Map<StrategyContext.QuestionParagraphTypeEnum, String> map = new HashMap<>(1);
+        /** 控制返回的长度必须是 {@link len} */
+        if (ObjectUtil.isNotEmpty(allGroup0) && allGroup0.size() < len) {
+            for (int i = 0; i < len - allGroup0.size() - 1; i++) {
+                /** 追加一个null分的记录占位 */
+                allGroup0.add("500");
+            }
+        }
+        paragraph = paragraph.trim();
+        /** 原数组放在最后一项 */
+        allGroup0.add(paragraph);
+        String joinStr = Strings.join(allGroup0, ',');
+        map.put(typeEnum, joinStr);
+        consumer.accept(map);
+        return joinStr;
+    }
+}
diff --git a/web/src/main/java/com/ibeetl/jlw/service/strategy/WordStrategy.java b/web/src/main/java/com/ibeetl/jlw/service/strategy/WordStrategy.java
new file mode 100644
index 00000000..4b765b9f
--- /dev/null
+++ b/web/src/main/java/com/ibeetl/jlw/service/strategy/WordStrategy.java
@@ -0,0 +1,38 @@
+package com.ibeetl.jlw.service.strategy;
+
+import javax.validation.constraints.NotNull;
+import java.util.Map;
+import java.util.function.Consumer;
+
+/**
+ * <p>
+ * word 策略
+ * </p>
+ *
+ * @author mlx
+ * @date 2022/5/30
+ * @modified
+ */
+public interface WordStrategy<T> {
+
+    /**
+     * 设置段落的类型
+     * @return
+     */
+    StrategyContext.QuestionParagraphTypeEnum getTypeEnum();
+
+    /**
+     * 通过验证,才能执行 process
+     * @param paragraph     段落字符串
+     * @return
+     */
+    boolean support(@NotNull T paragraph);
+
+    /**
+     * 执行方法体
+     * @param paragraph 段落字符串
+     * @param consumer  消费
+     * @return
+     */
+    String process(@NotNull T paragraph, Consumer<Map<StrategyContext.QuestionParagraphTypeEnum, T>> consumer);
+}
diff --git a/web/src/main/java/com/ibeetl/jlw/web/ResourcesQuestionController.java b/web/src/main/java/com/ibeetl/jlw/web/ResourcesQuestionController.java
index 12c51511..d18fdcf5 100644
--- a/web/src/main/java/com/ibeetl/jlw/web/ResourcesQuestionController.java
+++ b/web/src/main/java/com/ibeetl/jlw/web/ResourcesQuestionController.java
@@ -625,4 +625,13 @@ public class ResourcesQuestionController{
     public JsonResult<List<QuestionTypeCountVO>> questionTypeGroupInfo(@RequestParam(required = false) Long courseInfoId, @SCoreUser CoreUser coreUser) {
         return JsonResult.success(resourcesQuestionService.getGroupQuestionTypeCount(courseInfoId));
     }
+
+    //导入模板信息(word)
+//    @PostMapping(MODEL + "/importQBankWordTemplate.json")
+//    @Function("question.query")
+//    @ResponseBody
+//    public JsonResult importQBankWordTemplate(Integer isPublic,@RFile List<FileEntity> fileEntityList,@SCoreUser CoreUser coreUser, @RequestReferer String referer) {
+//        JsonResult jsonResult = resourcesQuestionService.importQBankWordTemplate(isPublic,fileEntityList,coreUser,referer);
+//        return jsonResult;
+//    }
 }
diff --git a/web/src/main/resources/sql/jlw/teacherOpenCourseStudentSigninLog.md b/web/src/main/resources/sql/jlw/teacherOpenCourseStudentSigninLog.md
index 54365f2f..5fc5acab 100644
--- a/web/src/main/resources/sql/jlw/teacherOpenCourseStudentSigninLog.md
+++ b/web/src/main/resources/sql/jlw/teacherOpenCourseStudentSigninLog.md
@@ -600,4 +600,25 @@ summary
   where 1=1
   @if(!isEmpty(teacherOpenCourseId)){
   and t.teacher_open_course_id =#teacherOpenCourseId#
-  @}
\ No newline at end of file
+  @}
+
+summaryBySigninSettingIds
+===
+* 签到根据签到ID汇总数据
+
+  SELECT
+  t.teacher_open_course_student_signin_setting_id,
+  count(
+  DISTINCT ( t.student_id )) AS sign_total_count,
+  sum( teacher_open_course_student_signin_log_tag = 10 ) AS sign_count,
+  sum( teacher_open_course_student_signin_log_tag = 20 ) AS sign_lack_count,
+  round( sum( teacher_open_course_student_signin_log_tag = 10 ) / count( DISTINCT ( t.student_id )), 4 ) * 100 AS attend_rate
+  FROM
+  teacher_open_course_student_signin_log t
+  where 1
+  @if(!isEmpty(teacherOpenCourseStudentSigninSettingId)) {
+    and find_in_set(t.teacher_open_course_student_signin_setting_id, #signinSettingIds#)
+  @}
+  GROUP BY
+  t.teacher_open_course_student_signin_setting_id
+  
\ No newline at end of file