yangdamao пре 5 дана
родитељ
комит
db6e7a6846

+ 7 - 0
zhongzheng-admin/src/main/java/com/zhongzheng/controller/bank/QuestionController.java

@@ -315,4 +315,11 @@ public class QuestionController extends BaseController {
         return AjaxResult.success(result);
         return AjaxResult.success(result);
     }
     }
 
 
+    @ApiOperation("识别图片文案2")
+    @PostMapping("/image/word/two")
+    public AjaxResult<Void> getImageWordTwo(MultipartFile file,String major,String exam) {
+        iQuestionService.getImageWordTwo(file,major,exam);
+        return AjaxResult.success();
+    }
+
 }
 }

+ 8 - 0
zhongzheng-admin/src/main/java/com/zhongzheng/controller/common/CommonController.java

@@ -994,4 +994,12 @@ public class CommonController extends BaseController {
         List<UserStudyConditionVo> list = iOrderService.getUserStudyCondition(bo);
         List<UserStudyConditionVo> list = iOrderService.getUserStudyCondition(bo);
         return AjaxResult.success(list);
         return AjaxResult.success(list);
     }
     }
+
+    @ApiOperation("识别图片文案")
+    @PostMapping("/common/free/image/word")
+    public AjaxResult<Void> getImageWord(MultipartFile file,String major) {
+        iGoodsService.getImageWord(file,major);
+        return AjaxResult.success();
+    }
+
 }
 }

+ 6 - 0
zhongzheng-common/pom.xml

@@ -248,6 +248,12 @@
             <artifactId>mail</artifactId>
             <artifactId>mail</artifactId>
             <version>1.4.7</version>
             <version>1.4.7</version>
         </dependency>
         </dependency>
+        <!-- Markdown 解析 -->
+        <dependency>
+            <groupId>com.vladsch.flexmark</groupId>
+            <artifactId>flexmark-all</artifactId>
+            <version>0.62.2</version>
+        </dependency>
 
 
     </dependencies>
     </dependencies>
 
 

+ 77 - 0
zhongzheng-common/src/main/java/com/zhongzheng/common/utils/OCRClient.java

@@ -0,0 +1,77 @@
+package com.zhongzheng.common.utils;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.net.URLEncoder;
+import java.util.HashMap;
+import java.util.Map;
+
+public class OCRClient {
+
+    private final String appId;
+    private final String secretCode;
+    private final String baseUrl;
+
+    public OCRClient(String appId, String secretCode) {
+        this.appId = appId;
+        this.secretCode = secretCode;
+        this.baseUrl = "https://api.textin.com/ai/service/v1/pdf_to_markdown";
+    }
+
+    public String recognize(byte[] fileContent, HashMap<String, Object> options) throws IOException {
+        // Build URL with query parameters
+        StringBuilder queryParams = new StringBuilder();
+        // Add query parameters
+        for (Map.Entry<String, Object> entry : options.entrySet()) {
+            if (queryParams.length() > 0) {
+                queryParams.append("&");
+            }
+            queryParams.append(URLEncoder.encode(entry.getKey(), "UTF-8"))
+                    .append("=")
+                    .append(URLEncoder.encode(entry.getValue().toString(), "UTF-8"));
+        }
+
+        // Create full URL with query parameters
+        String fullUrl = baseUrl + (queryParams.length() > 0 ? "?" + queryParams : "");
+        URL url = new URL(fullUrl);
+
+        // Create and configure HTTP connection
+        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+        connection.setRequestMethod("POST");
+
+        // Set headers
+        connection.setRequestProperty("x-ti-app-id", appId);
+        connection.setRequestProperty("x-ti-secret-code", secretCode);
+        // 方式一:读取本地文件
+        connection.setRequestProperty("Content-Type", "application/octet-stream");
+        // 方式二:使用URL方式
+        // connection.setRequestProperty("Content-Type", "text/plain");
+
+        // Enable output and send file content
+        connection.setDoOutput(true);
+        try (OutputStream os = connection.getOutputStream()) {
+            os.write(fileContent);
+            os.flush();
+        }
+
+        // Read response
+        int responseCode = connection.getResponseCode();
+        if (responseCode == HttpURLConnection.HTTP_OK) {
+            try (BufferedReader in = new BufferedReader(
+                    new InputStreamReader(connection.getInputStream()))) {
+                StringBuilder response = new StringBuilder();
+                String inputLine;
+                while ((inputLine = in.readLine()) != null) {
+                    response.append(inputLine);
+                }
+                return response.toString();
+            }
+        } else {
+            throw new IOException("HTTP request failed with code: " + responseCode);
+        }
+    }
+}

+ 20 - 1
zhongzheng-common/src/main/java/com/zhongzheng/common/utils/http/HttpUtils.java

@@ -3,6 +3,7 @@ package com.zhongzheng.common.utils.http;
 import cn.hutool.core.lang.Validator;
 import cn.hutool.core.lang.Validator;
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONObject;
 import com.alibaba.fastjson.JSONObject;
+import com.squareup.okhttp.*;
 import org.apache.http.*;
 import org.apache.http.*;
 import org.apache.http.client.HttpClient;
 import org.apache.http.client.HttpClient;
 import org.apache.http.client.config.RequestConfig;
 import org.apache.http.client.config.RequestConfig;
@@ -177,6 +178,24 @@ public class HttpUtils
         return null;
         return null;
     }
     }
 
 
+    public static String sendPostBaidu(String url)
+    {
+        HttpClient client = HttpClients.createDefault();
+        HttpPost post = new HttpPost(url);
+        try {
+            post.addHeader("content-type", "application/json");
+            System.out.println("url = " + url);
+            HttpResponse res = client.execute(post);
+            if (res.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
+                String result = EntityUtils.toString(res.getEntity());// 返回json格式
+                return result;
+            }
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+        return null;
+    }
+
     public static String sendPostJsonHeader(String url, String json, Map<String, String> headersMap)
     public static String sendPostJsonHeader(String url, String json, Map<String, String> headersMap)
     {
     {
    //     System.out.println(json);
    //     System.out.println(json);
@@ -529,7 +548,7 @@ public class HttpUtils
      * @throws IOException 读写异常
      * @throws IOException 读写异常
      */
      */
     private static String post(String url, Map<String, String> paramMap, String encoding) throws IOException, NoSuchAlgorithmException, KeyManagementException {
     private static String post(String url, Map<String, String> paramMap, String encoding) throws IOException, NoSuchAlgorithmException, KeyManagementException {
-        log.debug("http 请求 url: {} , 请求参数: {}", url, appendUrl("", paramMap).replace("?", ""));
+//        log.debug("http 请求 url: {} , 请求参数: {}", url, appendUrl("", paramMap).replace("?", ""));
         encoding = encoding == null ? UTF8 : encoding;
         encoding = encoding == null ? UTF8 : encoding;
         // 创建post方式请求对象
         // 创建post方式请求对象
         HttpPost httpPost = new HttpPost(url);
         HttpPost httpPost = new HttpPost(url);

+ 15 - 0
zhongzheng-system/src/main/java/com/zhongzheng/modules/bank/bo/QuestionWaiBuBo.java

@@ -0,0 +1,15 @@
+package com.zhongzheng.modules.bank.bo;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+@Data
+public class QuestionWaiBuBo implements Serializable {
+
+    private Long eduId;
+    private Long proId;
+    private Long businessId;
+    private Long subId;
+    private String content;
+}

+ 6 - 0
zhongzheng-system/src/main/java/com/zhongzheng/modules/bank/mapper/QuestionMapper.java

@@ -2,6 +2,8 @@ package com.zhongzheng.modules.bank.mapper;
 
 
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.zhongzheng.modules.bank.bo.QuestionQueryBo;
 import com.zhongzheng.modules.bank.bo.QuestionQueryBo;
+import com.zhongzheng.modules.bank.bo.QuestionWaiBuBo;
+import com.zhongzheng.modules.bank.domain.Exam;
 import com.zhongzheng.modules.bank.domain.Question;
 import com.zhongzheng.modules.bank.domain.Question;
 import com.zhongzheng.modules.bank.vo.QuestionVo;
 import com.zhongzheng.modules.bank.vo.QuestionVo;
 import com.zhongzheng.modules.course.bo.CourseQueryBo;
 import com.zhongzheng.modules.course.bo.CourseQueryBo;
@@ -39,4 +41,8 @@ public interface QuestionMapper extends BaseMapper<Question> {
     List<BankGoodsExamVo> getBankGoodsExamList(@Param("goodsId") Long goodsId);
     List<BankGoodsExamVo> getBankGoodsExamList(@Param("goodsId") Long goodsId);
 
 
     Question getQuestionByTenant(@Param("code") String code,@Param("newTenantId") Long newTenantId);
     Question getQuestionByTenant(@Param("code") String code,@Param("newTenantId") Long newTenantId);
+
+    Question getQuestionByWaiBu(QuestionWaiBuBo bo);
+
+    Exam getExamByWaiBu(QuestionWaiBuBo bo);
 }
 }

+ 2 - 0
zhongzheng-system/src/main/java/com/zhongzheng/modules/bank/service/IQuestionService.java

@@ -121,4 +121,6 @@ public interface IQuestionService extends IService<Question> {
 	Map<String, Object> importXlsxQuestionList(List<QuestionImportV4> questionList2,String param);
 	Map<String, Object> importXlsxQuestionList(List<QuestionImportV4> questionList2,String param);
 
 
 	Map<String, Object> importImage(List<QuestionImageVo> questionList2);
 	Map<String, Object> importImage(List<QuestionImageVo> questionList2);
+
+    void getImageWordTwo(MultipartFile file, String major,String exam);
 }
 }

+ 542 - 4
zhongzheng-system/src/main/java/com/zhongzheng/modules/bank/service/impl/QuestionServiceImpl.java

@@ -15,15 +15,14 @@ import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
 import com.baomidou.mybatisplus.core.toolkit.StringUtils;
 import com.baomidou.mybatisplus.core.toolkit.StringUtils;
 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
 import com.github.pagehelper.Page;
 import com.github.pagehelper.Page;
 import com.github.pagehelper.PageInfo;
 import com.github.pagehelper.PageInfo;
 import com.zhongzheng.common.core.page.TableDataInfo;
 import com.zhongzheng.common.core.page.TableDataInfo;
 import com.zhongzheng.common.core.redis.RedisCache;
 import com.zhongzheng.common.core.redis.RedisCache;
 import com.zhongzheng.common.exception.CustomException;
 import com.zhongzheng.common.exception.CustomException;
-import com.zhongzheng.common.utils.DateUtils;
-import com.zhongzheng.common.utils.SecurityUtils;
-import com.zhongzheng.common.utils.ServletUtils;
-import com.zhongzheng.common.utils.ToolsUtils;
+import com.zhongzheng.common.utils.*;
 import com.zhongzheng.common.utils.http.HttpUtils;
 import com.zhongzheng.common.utils.http.HttpUtils;
 import com.zhongzheng.modules.alioss.service.OssService;
 import com.zhongzheng.modules.alioss.service.OssService;
 import com.zhongzheng.modules.bank.bo.*;
 import com.zhongzheng.modules.bank.bo.*;
@@ -77,6 +76,8 @@ import java.io.*;
 import java.math.BigDecimal;
 import java.math.BigDecimal;
 import java.net.HttpURLConnection;
 import java.net.HttpURLConnection;
 import java.net.URL;
 import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Paths;
 import java.text.NumberFormat;
 import java.text.NumberFormat;
 import java.util.*;
 import java.util.*;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeUnit;
@@ -4603,6 +4604,543 @@ public class QuestionServiceImpl extends ServiceImpl<QuestionMapper, Question> i
         return Collections.emptyMap();
         return Collections.emptyMap();
     }
     }
 
 
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void getImageWordTwo(MultipartFile file, String major,String exam) {
+        OCRClient client = new OCRClient("de165480ad1fe94abd16f29a8897b7dd", "6904aae4f8e468d376a3a56f91398e78");
+
+        HashMap<String, Object> options = new HashMap<>();
+        options.put("apply_document_tree", 1);
+        options.put("apply_image_analysis", 0);
+        options.put("apply_merge", 1);
+        options.put("catalog_details", 1);
+        options.put("dpi", 144);
+        options.put("formula_level", 1);
+        options.put("get_excel", 1);
+        options.put("get_image", "both");
+        options.put("markdown_details", 1);
+        options.put("page_count", 1000);
+        options.put("page_details", 1);
+        options.put("page_start", 1);
+        options.put("paratext_mode", "annotation");
+        options.put("parse_mode", "scan");
+        options.put("raw_ocr", 0);
+        options.put("table_flavor", "html");
+
+        try {
+            // Read image file
+            // 方式一:读取本地文件
+            byte[] fileContent =  file.getBytes();
+//            byte[] fileContent = Files.readAllBytes(Paths.get("example.png"));
+
+            // 方式二:使用URL方式(需要将headers中的Content-Type改为'text/plain')
+            // byte[] fileContent = "https://example.com/path/to/your.pdf".getBytes(StandardCharsets.UTF_8);
+
+            String response = client.recognize(fileContent, options);
+            // 保存完整的JSON响应到result.json文件
+            Files.write(Paths.get("result.json"), response.getBytes());
+
+            // 解析JSON响应以提取markdown内容
+            ObjectMapper mapper = new ObjectMapper();
+            JsonNode jsonNode = mapper.readTree(response);
+            if (jsonNode.has("result") && jsonNode.get("result").has("markdown")) {
+                String markdown = jsonNode.get("result").get("markdown").asText();
+                List<String> strings = extractPureText(markdown);
+                questionReset(strings,major,exam);
+            }
+
+            Files.delete(Paths.get("result.json"));
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+
+    private void questionReset(List<String> questions,String majorName,String exam){
+
+        //题库业务层次
+        CourseEducationType educationType = iCourseEducationTypeService
+                .getOne(new LambdaQueryWrapper<CourseEducationType>().eq(CourseEducationType::getEducationName, "考前培训").eq(CourseEducationType::getStatus, 1).last("limit 1"));
+        Long eduId = educationType.getId();
+        CourseProjectType projectType = iCourseProjectTypeService
+                .getOne(new LambdaQueryWrapper<CourseProjectType>().eq(CourseProjectType::getProjectName, "施工现场专业人员").eq(CourseProjectType::getEducationId, eduId).eq(CourseProjectType::getStatus, 1).last("limit 1"));
+        Long proId = projectType.getId();
+        CourseBusiness business = iCourseBusinessService
+                .getOne(new LambdaQueryWrapper<CourseBusiness>().eq(CourseBusiness::getBusinessName, "七大员").eq(CourseBusiness::getProjectId, proId).eq(CourseBusiness::getStatus, 1).last("limit 1"));
+        Long businessId = business.getId();
+        List<CourseSubjectProject> list = iCourseSubjectProjectService.list(new LambdaQueryWrapper<CourseSubjectProject>().eq(CourseSubjectProject::getProjectId, proId));
+        List<Long> subIds = list.stream().map(CourseSubjectProject::getSubjectId).collect(Collectors.toList());
+        CourseSubject courseSubject = iCourseSubjectService.getOne(new LambdaQueryWrapper<CourseSubject>().in(CourseSubject::getId, subIds).eq(CourseSubject::getSubjectName, majorName).last("limit 1"));
+        Long subId = courseSubject.getId();
+
+        //添加试卷
+        Long examId = 0L;
+        QuestionWaiBuBo bo1 = new QuestionWaiBuBo();
+        bo1.setEduId(eduId);
+        bo1.setProId(proId);
+        bo1.setBusinessId(businessId);
+        bo1.setSubId(subId);
+        bo1.setContent(exam);
+        Exam examEntity = baseMapper.getExamByWaiBu(bo1);
+        if (ObjectUtils.isNull(examEntity)){
+            //新增
+            Exam exam2 = new Exam();
+            exam2.setExamName(exam);
+            exam2.setCode(ServletUtils.getEncoded("SJ"));
+            exam2.setCreateTime(DateUtils.getNowTime());
+            exam2.setUpdateTime(DateUtils.getNowTime());
+            exam2.setPrefixName(String.format("外部系统拉取试卷(%s)",DateUtils.getDate()));
+            exam2.setPublishStatus(1L);
+            exam2.setYear(2026L);
+            exam2.setStatus(1);
+            exam2.setDoType(1);
+            //试卷类型ID
+            ExamPaper paper = iExamPaperService.getOne(new LambdaUpdateWrapper<ExamPaper>().eq(ExamPaper::getStatus, 1).eq(ExamPaper::getPaperName, "普通练习").last("limit 1"));
+            exam2.setExamPaperId(paper.getPaperId());
+            iExamService.save(exam2);
+            //业务乘次
+            QuestionBusiness questionBusiness = new QuestionBusiness();
+            questionBusiness.setEducationTypeId(eduId);
+            questionBusiness.setProjectId(proId);
+            questionBusiness.setBusinessId(businessId);
+            questionBusiness.setSubjectId(subId);
+            questionBusiness.setType(2);
+            questionBusiness.setMajorId(exam2.getExamId());
+            iQuestionBusinessService.save(questionBusiness);
+            examId = exam2.getExamId();
+        }else {
+            examId = examEntity.getExamId();
+        }
+
+        //题库ID
+        Long questionId = 0L;
+        //处理题库
+        Integer start = 0;
+        Integer end = 0;
+        for (int i = 0; i < questions.size(); i++) {
+            String s = questions.get(i);
+            if (s.contains("/100题")){
+                start = i;
+            }
+            if (s.contains("考生答案")){
+                end = i;
+            }
+        }
+        List<String> content = questions.subList(start + 1, end);
+        //获取第一个判断题型
+        String bt = content.get(0);
+        if (bt.contains("理解题")){
+            if (StringUtils.isBlank(exam)){
+                throw new CustomException("案例题需要补充试卷名称");
+            }
+            //就是案例题,分解获取案例题文本
+            Integer start1 = 0;
+            Integer end1 = 0;
+            for (int i = 0; i < content.size(); i++) {
+                String s = content.get(i);
+                List<String> list1 = Arrays.asList("多选题", "单选题", "判断题");
+                if (list1.stream().anyMatch(x -> s.contains(x))){
+                    end1 = i;
+                }
+            }
+            //案例题文案
+            List<String> anLi = content.subList(start1, end1);
+            //提取文本
+            String anLiStr = "";
+            for (String text : anLi) {
+                // 排除条件(这些不是问题描述)
+                if (text.matches("^[0-9]{1,3}\\..*题$") ||  // 题号类型
+                        text.matches(".*[0-9]分$") ||          // 简单分数
+                        text.matches("^[A-E][|、::]?.*") ||   // 选项
+                        text.length() < 10) {                  // 太短的文本
+                    continue;
+                }
+                anLiStr = text;
+            }
+            if (StringUtils.isBlank(anLiStr)) {
+                throw new CustomException("案例题文案获取失败");
+            }
+            //案例题序号
+            String anLiXu = bt.replaceAll("^([0-9]+)、.*", "$1");
+            //检查是否已经存在
+            QuestionWaiBuBo bo = new QuestionWaiBuBo();
+            bo.setEduId(eduId);
+            bo.setProId(proId);
+            bo.setBusinessId(businessId);
+            bo.setSubId(subId);
+            String keyWord = String.format("[%s]案例题序号:%s",exam,anLiXu);
+            bo.setContent(keyWord);
+            Question entity = baseMapper.getQuestionByWaiBu(bo);
+            if (ObjectUtils.isNotNull(entity)){
+                //修改
+                entity.setContent(anLiStr);
+                updateById(entity);
+            }else {
+                //新增
+                Question question = new Question();
+                question.setContent(anLiStr);
+                question.setType(4);
+                question.setStatus(1);
+                question.setCreateTime(DateUtils.getNowTime());
+                question.setUpdateTime(DateUtils.getNowTime());
+                question.setCode(ServletUtils.getEncoded("TM"));
+                question.setPrefixName(String.format("外部系统拉取(%s)[%s]案例题序号:%s",DateUtils.getDate(),exam,anLiXu));
+                question.setPublishStatus(1);
+                question.setFromPlat(1);
+                save(question);
+                QuestionBusiness questionBusiness = new QuestionBusiness();
+                questionBusiness.setEducationTypeId(eduId);
+                questionBusiness.setProjectId(proId);
+                questionBusiness.setBusinessId(businessId);
+                questionBusiness.setSubjectId(subId);
+                questionBusiness.setType(1);
+                questionBusiness.setMajorId(question.getQuestionId());
+                iQuestionBusinessService.save(questionBusiness);
+                saveExamQuestion(question.getQuestionId(),examId);
+            }
+
+            //案例题小题
+            content = content.subList(end1,content.size());
+            bt = content.get(0);
+        }
+        // 匹配带小数的数字
+        Pattern pattern = Pattern.compile("\\d+(\\.\\d+)");
+        Matcher matcher = pattern.matcher(bt);
+        if (matcher.find()){
+            if (StringUtils.isBlank(exam)){
+                throw new CustomException("案例题需要补充试卷名称");
+            }
+            //带有小数点是案例题
+            String group = matcher.group(0);
+            List<String> list1 = Arrays.asList(group.split("\\."));
+            //案例序号
+            String xuh = list1.get(0);
+            //案例小题序号
+            String xuxh = list1.get(1);
+            String content2 = String.format("[%s]案例题序号:%s",exam, xuh);
+
+            //解析题库
+            List<QuestionInfoVo> infoVoList = new ArrayList<>();
+            if (bt.contains("单选题")){
+                List<String> questionList = filterContent(content);
+                //第一个是标题
+                String s = questionList.get(0);
+                QuestionInfoVo infoVo = new QuestionInfoVo();
+                infoVo.setContent(s);
+                infoVo.setType(1);
+                infoVo.setWaiBuSort(Integer.valueOf(xuxh));
+                List<QuestionDetailVo> detailVoList = new ArrayList<>();
+                for (int i = 1; i < questionList.size(); i++) {
+                    String s1 = questionList.get(i);
+                    QuestionDetailVo detailVo = new QuestionDetailVo();
+                    detailVo.setContent(s1);
+                    detailVo.setOptionsId(i);
+                    detailVoList.add(detailVo);
+                }
+                infoVo.setOptionsList(detailVoList);
+                //分解答案
+                String answer = getAnswer(questions);
+                infoVo.setAnswerQuestion(simpleConvert(answer));
+                infoVoList.add(infoVo);
+
+            }
+
+            if (bt.contains("多选题")){
+                List<String> questionList = filterContent(content);
+                //第一个是标题
+                String s = questionList.get(0);
+                QuestionInfoVo infoVo = new QuestionInfoVo();
+                infoVo.setContent(s);
+                infoVo.setType(2);
+                infoVo.setWaiBuSort(Integer.valueOf(xuxh));
+                List<QuestionDetailVo> detailVoList = new ArrayList<>();
+                for (int i = 1; i < questionList.size(); i++) {
+                    String s1 = questionList.get(i);
+                    QuestionDetailVo detailVo = new QuestionDetailVo();
+                    detailVo.setContent(s1);
+                    detailVo.setOptionsId(i);
+                    detailVoList.add(detailVo);
+                }
+                infoVo.setOptionsList(detailVoList);
+                //分解答案
+                String answer = getAnswer(questions);
+                infoVo.setAnswerQuestion(simpleConvert(answer));
+                infoVoList.add(infoVo);
+            }
+
+            if (bt.contains("判断题")){
+                List<String> questionList = filterContent(content);
+                //第一个是标题
+                String s = questionList.get(0);
+                QuestionInfoVo infoVo = new QuestionInfoVo();
+                infoVo.setContent(s);
+                infoVo.setType(3);
+                infoVo.setWaiBuSort(Integer.valueOf(xuxh));
+                //分解答案
+                String answer = getAnswer(questions);
+                infoVo.setAnswerQuestion(answer.contains("错") ? "0":"1");
+                infoVoList.add(infoVo);
+            }
+            //判断是否已经在
+            QuestionWaiBuBo bo = new QuestionWaiBuBo();
+            bo.setEduId(eduId);
+            bo.setProId(proId);
+            bo.setBusinessId(businessId);
+            bo.setSubId(subId);
+            bo.setContent(content2);
+            Question entity = baseMapper.getQuestionByWaiBu(bo);
+            if (ObjectUtils.isNotNull(entity)){
+                //已存在,添加案例小题
+                String jsonStr = entity.getJsonStr();
+                if (StringUtils.isNotBlank(jsonStr)){
+                    List<QuestionInfoVo> infoVoList1 = JSONArray.parseArray(jsonStr, QuestionInfoVo.class);
+                    infoVoList1.addAll(infoVoList);
+                    List<QuestionInfoVo> collect = infoVoList1.stream().sorted(Comparator.comparingInt(QuestionInfoVo::getWaiBuSort)).collect(Collectors.toList());
+                    entity.setJsonStr(JSONArray.toJSONString(collect));
+                }else {
+                    entity.setJsonStr(JSONArray.toJSONString(infoVoList));
+                }
+                updateById(entity);
+                questionId = entity.getQuestionId();
+            }else {
+                //新增
+                Question question = new Question();
+                question.setContent(content2);
+                question.setType(4);
+                question.setStatus(1);
+                question.setCreateTime(DateUtils.getNowTime());
+                question.setUpdateTime(DateUtils.getNowTime());
+                question.setCode(ServletUtils.getEncoded("TM"));
+                question.setPrefixName(String.format("外部系统拉取(%s)[%s]案例题序号:%s",DateUtils.getDate(),exam,xuh));
+                question.setPublishStatus(1);
+                question.setFromPlat(1);
+                question.setJsonStr(JSONArray.toJSONString(infoVoList));
+                save(question);
+                questionId = question.getQuestionId();
+                saveExamQuestion(questionId,examId);
+            }
+
+        }else {
+            //判断题,多选题,单选题
+            if (bt.contains("单选题")){
+                List<String> questionList = filterContent(content);
+                //第一个是标题
+                String s = questionList.get(0);
+                Question question = new Question();
+                question.setContent(s);
+                question.setType(1);
+                question.setStatus(1);
+                question.setCreateTime(DateUtils.getNowTime());
+                question.setUpdateTime(DateUtils.getNowTime());
+                question.setCode(ServletUtils.getEncoded("TM"));
+                question.setPrefixName(String.format("外部系统拉取(%s)",DateUtils.getDate()));
+                question.setPublishStatus(1);
+                question.setFromPlat(1);
+                List<QuestionOptionVo> optionVos = new ArrayList<>();
+                for (int i = 1; i < questionList.size(); i++) {
+                    String s1 = questionList.get(i);
+                    QuestionOptionVo questionOptionVo = new QuestionOptionVo();
+                    questionOptionVo.setContent(s1);
+                    questionOptionVo.setOptionsId(i);
+                    optionVos.add(questionOptionVo);
+                }
+                question.setJsonStr(JSONArray.toJSONString(optionVos));
+                //分解答案
+                String answer = getAnswer(questions);
+                question.setAnswerQuestion(simpleConvert(answer));
+                save(question);
+                questionId = question.getQuestionId();
+                saveExamQuestion(questionId,examId);
+            }
+
+            if (bt.contains("多选题")){
+                List<String> questionList = filterContent(content);
+                //第一个是标题
+                String s = questionList.get(0);
+                Question question = new Question();
+                question.setContent(s);
+                question.setType(2);
+                question.setStatus(1);
+                question.setCreateTime(DateUtils.getNowTime());
+                question.setUpdateTime(DateUtils.getNowTime());
+                question.setCode(ServletUtils.getEncoded("TM"));
+                question.setPrefixName(String.format("外部系统拉取(%s)",DateUtils.getDate()));
+                question.setPublishStatus(1);
+                question.setFromPlat(1);
+                List<QuestionOptionVo> optionVos = new ArrayList<>();
+                for (int i = 1; i < questionList.size(); i++) {
+                    String s1 = questionList.get(i);
+                    QuestionOptionVo questionOptionVo = new QuestionOptionVo();
+                    questionOptionVo.setContent(s1);
+                    questionOptionVo.setOptionsId(i);
+                    optionVos.add(questionOptionVo);
+                }
+                question.setJsonStr(JSONArray.toJSONString(optionVos));
+                //分解答案
+                String answer = getAnswer(questions);
+                question.setAnswerQuestion(simpleConvert(answer));
+                save(question);
+                questionId = question.getQuestionId();
+                saveExamQuestion(questionId,examId);
+            }
+
+            if (bt.contains("判断题")){
+                List<String> questionList = filterContent(content);
+                //第一个是标题
+                String s = questionList.get(0);
+                Question question = new Question();
+                question.setContent(s);
+                question.setType(3);
+                question.setStatus(1);
+                question.setCreateTime(DateUtils.getNowTime());
+                question.setUpdateTime(DateUtils.getNowTime());
+                question.setCode(ServletUtils.getEncoded("TM"));
+                question.setPrefixName(String.format("外部系统拉取(%s)",DateUtils.getDate()));
+                question.setPublishStatus(1);
+                question.setFromPlat(1);
+                //分解答案
+                String answer = getAnswer(questions);
+                question.setAnswerQuestion(answer.contains("错") ? "0":"1");
+                save(question);
+                questionId = question.getQuestionId();
+                saveExamQuestion(questionId,examId);
+            }
+
+        }
+        if (questionId <= 0){
+            throw new CustomException("题库解析失败");
+        }
+
+        QuestionBusiness questionBusiness = new QuestionBusiness();
+        questionBusiness.setEducationTypeId(eduId);
+        questionBusiness.setProjectId(proId);
+        questionBusiness.setBusinessId(businessId);
+        questionBusiness.setSubjectId(subId);
+        questionBusiness.setType(1);
+        questionBusiness.setMajorId(questionId);
+        iQuestionBusinessService.save(questionBusiness);
+
+    }
+
+    private void saveExamQuestion(Long questionId,Long examId){
+        ExamQuestion examQuestion = new ExamQuestion();
+        examQuestion.setExamId(examId);
+        examQuestion.setQuestionId(questionId);
+        //获取最新排序
+        Integer sort = 0;
+        ExamQuestion one = iExamQuestionService.getOne(new LambdaUpdateWrapper<ExamQuestion>().eq(ExamQuestion::getExamId, examId).orderByDesc(ExamQuestion::getSort));
+        if (ObjectUtils.isNotNull(one)){
+            sort = one.getSort() +1;
+        }
+        examQuestion.setSort(sort);
+        iExamQuestionService.save(examQuestion);
+    }
+
+    public static String simpleConvert(String letters) {
+        String result = letters.toUpperCase()
+                .replaceAll("[^A-E]", "")  // 只保留A-E
+                .replace("A", "1")
+                .replace("B", "2")
+                .replace("C", "3")
+                .replace("D", "4")
+                .replace("E", "5");
+
+        // 添加逗号分隔
+        return String.join(",", result.split(""));
+    }
+
+    private String getAnswer(List<String> question){
+        //找正确答案
+        Integer index =0;
+        for (int i = 0; i < question.size(); i++) {
+            String s = question.get(i);
+            if (s.equals("正确答案")){
+                index = i;
+            }
+        }
+        return question.get(index+3);
+    }
+
+    private List<String> filterContent(List<String> lines) {
+        List<String> result = new ArrayList<>();
+
+        for (String line : lines) {
+            String trimmed = line.trim();
+
+            // 跳过:题目编号、分值、单独选项字母
+            if (trimmed.matches("\\d+、.*") ||
+                    trimmed.matches("\\d+分") ||
+                    trimmed.matches("[A-E]$")) {
+                continue;
+            }
+
+            // 处理选项内容行(如 "D 高处作业...")
+            if (trimmed.matches("[A-E]\\s+.*")) {
+                // 去除开头的选项字母
+                trimmed = trimmed.substring(1).trim();
+            }
+
+            // 如果以 "|" 开头,去除它
+            if (trimmed.startsWith("|")) {
+                trimmed = trimmed.substring(1).trim();
+            }
+
+            //去除首字母或者| 这种符号
+            trimmed = trimmed.replaceAll("^[A-E][\\||]?", "");
+            // 去除结尾的括号
+//            trimmed = trimmed.replaceAll("[()()]+$", "");
+
+            if (!trimmed.isEmpty()) {
+                result.add(trimmed);
+            }
+        }
+
+        return result;
+    }
+
+    /**
+     * 提取纯文本信息,过滤掉所有链接、HTML标签和Markdown符号
+     */
+    public static List<String> extractPureText(String input) {
+        List<String> result = new ArrayList<>();
+
+        // 按行分割
+        String[] lines = input.split("\\n");
+
+        for (String line : lines) {
+            // 移除HTML注释 <!-- ... -->
+            line = line.replaceAll("<!--.*?-->", "");
+
+            // 移除Markdown图片链接 ![...](...)
+            line = line.replaceAll("!\\[.*?\\]\\(.*?\\)", "");
+
+            // 移除普通链接
+            line = line.replaceAll("https?://\\S+", "");
+
+            // 移除HTML标签
+            line = line.replaceAll("<[^>]+>", "");
+
+            // 移除Markdown格式标记 ** **
+            line = line.replaceAll("\\*\\*", "");
+
+            // 移除其他常见Markdown标记
+            line = line.replaceAll("#{1,6}\\s*", "")  // 标题
+                    .replaceAll("`{1,3}", "")       // 代码标记
+                    .replaceAll("\\[.*?\\]\\(.*?\\)", "")  // 普通链接
+                    .replaceAll("\\*|_", "");       // 斜体和粗体标记
+
+            // 修剪空白字符
+            line = line.trim();
+
+            // 只添加非空行
+            if (!line.isEmpty()) {
+                result.add(line);
+            }
+        }
+
+        return result;
+    }
+
 
 
     private void handlePhoto(List<ExternalQuestionDetailVo> questionDetailVos) {
     private void handlePhoto(List<ExternalQuestionDetailVo> questionDetailVos) {
         String prefix = "\\Uploads\\qdytopic\\";
         String prefix = "\\Uploads\\qdytopic\\";

+ 2 - 0
zhongzheng-system/src/main/java/com/zhongzheng/modules/bank/vo/QuestionInfoVo.java

@@ -17,6 +17,8 @@ public class QuestionInfoVo implements Serializable {
 
 
 	private String analysisContent;
 	private String analysisContent;
 
 
+	private Integer waiBuSort;
+
 	private String answerQuestion;
 	private String answerQuestion;
 
 
 	private String content;
 	private String content;

+ 3 - 0
zhongzheng-system/src/main/java/com/zhongzheng/modules/goods/service/IGoodsService.java

@@ -17,6 +17,7 @@ import com.zhongzheng.modules.goods.vo.*;
 import com.zhongzheng.modules.grade.vo.SyncGoodsExport;
 import com.zhongzheng.modules.grade.vo.SyncGoodsExport;
 import com.zhongzheng.modules.order.domain.OrderGoods;
 import com.zhongzheng.modules.order.domain.OrderGoods;
 import com.zhongzheng.modules.system.domain.SysTenant;
 import com.zhongzheng.modules.system.domain.SysTenant;
+import org.springframework.web.multipart.MultipartFile;
 
 
 import java.util.Collection;
 import java.util.Collection;
 import java.util.List;
 import java.util.List;
@@ -184,4 +185,6 @@ public interface IGoodsService extends IService<Goods> {
 	boolean disposeQuestion(Long goodsId);
 	boolean disposeQuestion(Long goodsId);
 
 
     List<QuestionAnswerVo> getQuestionAnswer(Long goodsId);
     List<QuestionAnswerVo> getQuestionAnswer(Long goodsId);
+
+    void getImageWord(MultipartFile file, String major);
 }
 }

+ 163 - 4
zhongzheng-system/src/main/java/com/zhongzheng/modules/goods/service/impl/GoodsServiceImpl.java

@@ -14,13 +14,20 @@ import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
 import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
 import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
 import com.github.pagehelper.Page;
 import com.github.pagehelper.Page;
+import com.vladsch.flexmark.html.HtmlRenderer;
+import com.vladsch.flexmark.parser.Parser;
+import com.vladsch.flexmark.util.ast.Node;
+import com.vladsch.flexmark.util.data.MutableDataSet;
 import com.zhongzheng.common.annotation.DataScope;
 import com.zhongzheng.common.annotation.DataScope;
 import com.zhongzheng.common.core.redis.RedisCache;
 import com.zhongzheng.common.core.redis.RedisCache;
 import com.zhongzheng.common.exception.BaseException;
 import com.zhongzheng.common.exception.BaseException;
 import com.zhongzheng.common.exception.CustomException;
 import com.zhongzheng.common.exception.CustomException;
 import com.zhongzheng.common.type.EncryptHandler;
 import com.zhongzheng.common.type.EncryptHandler;
 import com.zhongzheng.common.utils.*;
 import com.zhongzheng.common.utils.*;
+import com.zhongzheng.common.utils.file.FileUtils;
 import com.zhongzheng.common.utils.http.HttpUtils;
 import com.zhongzheng.common.utils.http.HttpUtils;
 import com.zhongzheng.modules.activity.domain.ActivityGoodsPrice;
 import com.zhongzheng.modules.activity.domain.ActivityGoodsPrice;
 import com.zhongzheng.modules.activity.service.IActivityGoodsPriceService;
 import com.zhongzheng.modules.activity.service.IActivityGoodsPriceService;
@@ -29,6 +36,7 @@ import com.zhongzheng.modules.bank.domain.*;
 import com.zhongzheng.modules.bank.mapper.QuestionMapper;
 import com.zhongzheng.modules.bank.mapper.QuestionMapper;
 import com.zhongzheng.modules.bank.service.*;
 import com.zhongzheng.modules.bank.service.*;
 import com.zhongzheng.modules.bank.vo.QuestionAnswerVo;
 import com.zhongzheng.modules.bank.vo.QuestionAnswerVo;
+import com.zhongzheng.modules.bank.vo.QuestionOptionVo;
 import com.zhongzheng.modules.base.domain.*;
 import com.zhongzheng.modules.base.domain.*;
 import com.zhongzheng.modules.base.service.*;
 import com.zhongzheng.modules.base.service.*;
 import com.zhongzheng.modules.course.bo.*;
 import com.zhongzheng.modules.course.bo.*;
@@ -99,17 +107,21 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Service;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.multipart.MultipartFile;
 
 
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
+import java.io.*;
 import java.math.BigDecimal;
 import java.math.BigDecimal;
 import java.net.HttpURLConnection;
 import java.net.HttpURLConnection;
 import java.net.URL;
 import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardOpenOption;
 import java.util.*;
 import java.util.*;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.concurrent.atomic.AtomicReference;
+import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 import java.util.stream.Collectors;
 import java.util.stream.IntStream;
 import java.util.stream.IntStream;
@@ -5925,6 +5937,153 @@ public class GoodsServiceImpl extends ServiceImpl<GoodsMapper, Goods> implements
         return answerVos;
         return answerVos;
     }
     }
 
 
+    @Override
+    public void getImageWord(MultipartFile file, String major) {
+
+        String grant_type = "client_credentials";
+        String client_id = "K0Mv0vrfZVpgvQLcGttvBcmz";
+        String client_secret = "rS2gCLcrpNMul0pPUBkSeVJLoaq5xn6Z";
+
+        //获取百度access_token
+        String tokenUrl = String.format("https://aip.baidubce.com/oauth/2.0/token?grant_type=%s&client_id=%s&client_secret=%s",grant_type,client_id,client_secret);
+        String tokenRespone = HttpUtils.sendPostBaidu(tokenUrl);
+        String access_token = "";
+        if (!tokenRespone.contains("\"expires_in\": 2592000")) {
+            JSONObject jsonObject = JSONObject.parseObject(tokenRespone);
+            access_token = jsonObject.get("access_token").toString();
+        }
+
+        if (StringUtils.isBlank(access_token)){
+            throw new CustomException("access_token获取失败");
+        }
+
+        //文档解析请求接口
+        String task_type = "ocr_recognition";
+        String lang = "zh";
+        String file_name = file.getOriginalFilename();
+        //文件Base
+        String file_data = "";
+        try {
+            byte[] fileBytes = file.getBytes();
+            file_data =  Base64.getEncoder().encodeToString(fileBytes);
+        }catch (Exception e){
+            e.printStackTrace();
+        }
+
+        String task_id = "";
+
+        String analysisUrl = String.format("https://aip.baidubce.com/rest/2.0/brain/online/v2/paddle-vl-parser/task?access_token=%s",access_token);
+        HashMap<String, String> paramMap = new HashMap<>();
+        paramMap.put("file_data",file_data);
+        paramMap.put("file_name",file_name);
+        paramMap.put("task_type",task_type);
+        paramMap.put("lang",lang);
+        try {
+            String analysisRespone = HttpUtils.postFormBody(analysisUrl, paramMap);
+            JSONObject jsonObject = JSONObject.parseObject(analysisRespone);
+            if (jsonObject.get("error_code").toString().equals("0")) {
+                Object result = jsonObject.get("result");
+                JSONObject jsonObject1 = JSONObject.parseObject(JSON.toJSONString(result));
+                task_id = jsonObject1.get("task_id").toString();
+            }
+        }catch (Exception e){
+            e.printStackTrace();
+        }
+
+        if (StringUtils.isBlank(task_id)){
+            throw new CustomException("task_id任务ID获取失败");
+        }
+
+
+        //文档解析获取结果接口
+        String markdown_url = "";
+        String resultUrl = String.format("https://aip.baidubce.com/rest/2.0/brain/online/v2/paddle-vl-parser/task/query?access_token=%s",access_token);
+        HashMap<String, String> resultMap = new HashMap<>();
+        resultMap.put("task_id",task_id);
+        try {
+            Thread.sleep(2000);
+            String resultRespone = HttpUtils.postFormBody(resultUrl,resultMap);
+            JSONObject jsonObject = JSONObject.parseObject(resultRespone);
+            if (jsonObject.get("error_code").toString().equals("0")) {
+                Object result = jsonObject.get("result");
+                JSONObject jsonObject1 = JSONObject.parseObject(JSON.toJSONString(result));
+                markdown_url = jsonObject1.get("markdown_url").toString();
+            }
+        }catch (Exception e){
+            e.printStackTrace();
+        }
+
+        if (StringUtils.isBlank(markdown_url)){
+            throw new CustomException("markdown_url文档解析结果失败");
+        }
+        try {
+            String zhiyuan = System.getProperty("user.dir");
+            String toPath = zhiyuan + "/zhongzheng-admin/src/main/resources/zhiyuan";
+            String s = downloadMarkdownFile(markdown_url, toPath);
+            List<String> strings = Files.readAllLines(Paths.get(s), StandardCharsets.UTF_8);
+            String join = String.join("\n", strings);
+            System.out.println("join = " + join);
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+
+    }
+
+    /**
+     * 下载Markdown文件
+     */
+    public static String downloadMarkdownFile(String fileUrl, String saveDirectory) throws Exception {
+        URL url = new URL(fileUrl);
+        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+        conn.setRequestMethod("GET");
+        conn.setConnectTimeout(30000);
+
+        // 获取文件名
+        String fileName = "downloaded_file.md";
+        String contentDisposition = conn.getHeaderField("Content-Disposition");
+        if (contentDisposition != null && contentDisposition.contains("filename=")) {
+            String[] parts = contentDisposition.split("filename=");
+            if (parts.length > 1) {
+                fileName = parts[1].replaceAll("[\"\']", "").trim();
+            }
+        }
+
+        // 如果从URL获取不到,使用URL中的文件名
+        if (fileName.equals("downloaded_file.md")) {
+            String path = fileUrl.split("\\?")[0];
+            String[] pathParts = path.split("/");
+            if (pathParts.length > 0) {
+                String lastPart = pathParts[pathParts.length - 1];
+                if (!lastPart.isEmpty()) {
+                    fileName = lastPart;
+                }
+            }
+        }
+
+        // 清理文件名
+        fileName = fileName.replaceAll("[<>:\"/\\\\|?*]", "_");
+
+        // 确保目录存在
+        Path saveDir = Paths.get(saveDirectory);
+        Files.createDirectories(saveDir);
+
+        // 完整保存路径
+        Path savePath = saveDir.resolve(fileName);
+
+        // 下载文件
+        try (InputStream inputStream = conn.getInputStream();
+             OutputStream outputStream = Files.newOutputStream(savePath, StandardOpenOption.CREATE)) {
+
+            byte[] buffer = new byte[8192];
+            int bytesRead;
+
+            while ((bytesRead = inputStream.read(buffer)) != -1) {
+                outputStream.write(buffer, 0, bytesRead);
+            }
+        }
+        return savePath.toString();
+    }
+
     private static String htmlToTextSimple(String html) {
     private static String htmlToTextSimple(String html) {
         if (html == null) return "";
         if (html == null) return "";
 
 

+ 36 - 0
zhongzheng-system/src/main/resources/mapper/modules/bank/QuestionMapper.xml

@@ -741,4 +741,40 @@
         select * from question where `code` = #{code} and tenant_id = #{newTenantId}
         select * from question where `code` = #{code} and tenant_id = #{newTenantId}
     </select>
     </select>
 
 
+    <select id="getQuestionByWaiBu" parameterType="com.zhongzheng.modules.bank.bo.QuestionWaiBuBo" resultType="com.zhongzheng.modules.bank.domain.Question">
+        SELECT
+            q.*
+        FROM
+            `question` q
+                LEFT JOIN question_business qb ON q.question_id = qb.major_id
+        WHERE
+            q.`status` = 1
+            AND qb.type = 1
+            AND qb.education_type_id = #{eduId}
+            AND qb.project_id = #{proId}
+            AND qb.business_id = #{businessId}
+            AND qb.subject_id = #{subId}
+            AND q.prefix_name like concat('%', #{content}, '%')
+            ORDER BY q.create_time DESC
+              LIMIT 1
+    </select>
+
+    <select id="getExamByWaiBu" parameterType="com.zhongzheng.modules.bank.bo.QuestionWaiBuBo" resultType="com.zhongzheng.modules.bank.domain.Exam">
+        SELECT
+            q.*
+        FROM
+            `exam` q
+                LEFT JOIN question_business qb ON q.exam_id = qb.major_id
+        WHERE
+            q.`status` = 1
+          AND qb.type = 2
+          AND qb.education_type_id = #{eduId}
+          AND qb.project_id = #{proId}
+          AND qb.business_id = #{businessId}
+          AND qb.subject_id = #{subId}
+          AND q.exam_name = #{content}
+            ORDER BY q.create_time DESC
+            LIMIT 1
+    </select>
+
 </mapper>
 </mapper>