往期开发(已经完成)

  • OJ 判题项目-介绍(一)
  • OJ 判题项目-初始化(二)
  • OJ 判题项目-前后端联调(三)
  • OJ 判题项目-简易判题机(四)
  • OJ 判题项目-原生代码沙箱(五)
  • OJ 判题项目-Docker 代码沙箱(六)
  • OJ 判题项目-微服务改造(七)

  • 今日后端开发

    • 判题模块架构

      先模拟跑通正常的业务流程

      1. 判题模块和代码沙箱的关系

        判题模块只需要调用代码沙箱执行代码 然后返回判题结果
        代码沙箱只负责接收输入和输出用例执行代码返回执行结果

      2. 判题模块和代码沙箱之间实现解耦

      3. 为什么代码沙箱需要接收一组测试用例?
        一般情况下测试时候会有很多个测试用例,比如力扣
        每次测试都要调用一次接口吗?或者是说每次调用都要编译相同的代码?
        所以要进行批量处理防止多次重复编译占用资源

    • 代码沙箱架构开发

      1. 定义代码沙箱结构,提高通用性

        定义代码沙箱接口

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        public interface CodeSandBox {

        /**
        * 提交代码接口
        *
        * @param executeCodeRequest
        * @return
        */
        ExecuteCodeResponse doExecuteCode(ExecuteCodeRequest executeCodeRequest);
        }

        定义统一的请求类和返回类使用建造者模式来构建参数

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        30
        31
        32
        33
        34
        35
        36
        37
        38
        39
        40
        41
        42
        43
        44
        45
        46
        47
        48
        1. 请求类
        @Data
        @Builder
        @AllArgsConstructor
        @NoArgsConstructor
        public class ExecuteCodeRequest {

        /**
        * 输入用例
        */
        private List<String> inputList;

        /**
        * 语言
        */
        private String language;

        /**
        * 代码
        */
        private String code;
        }
        2. 结果类
        @Data
        @Builder
        @AllArgsConstructor
        @NoArgsConstructor
        public class ExecuteCodeResponse {
        /**
        * 输出用例
        */
        private List<String> outputList;

        /**
        * 沙箱的运行信息
        */
        private String message;

        /**
        * 判题状态
        */
        private Integer status;

        /**
        * 判题信息
        */
        private JudgeInfo judgeInfo;
        }

        定义枚举类

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        30
        31
        public enum CodeSandBoxEnum {
        /**
        * 枚举值
        */
        EXAMPLE("example"),
        REMOTE("remote"),
        THIRD_PARTY("thirdParty");

        final private String name;

        CodeSandBoxEnum(String name) {
        this.name = name;
        }

        public String getValue() {
        return name;
        }

        public static CodeSandBoxEnum getCodeSandBoxEnum(String type) {
        switch (type) {
        case "example":
        return CodeSandBoxEnum.EXAMPLE;
        case "remote":
        return CodeSandBoxEnum.REMOTE;
        case "thirdParty":
        return CodeSandBoxEnum.THIRD_PARTY;
        default:
        return CodeSandBoxEnum.EXAMPLE;
        }
        }
        }
      2. 定义多种代码沙箱的实现类
        例如

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        /**
        * 示例沙箱
        */
        public class ExampleCodeSandBox implements CodeSandBox {

        @Override
        public ExecuteCodeResponse doExecuteCode(ExecuteCodeRequest executeCodeRequest) {
        ExecuteCodeResponse executeCodeResponse = new ExecuteCodeResponse();
        executeCodeResponse.setOutputList(new ArrayList<>());
        executeCodeResponse.setMessage("");
        executeCodeResponse.setStatus(0);
        executeCodeResponse.setJudgeInfo(new JudgeInfo());
        return executeCodeResponse;
        }
        }

        远程代码沙箱,示例代码沙箱(模拟业务流程),第三方代码沙箱

      3. 代码沙箱日志优化(增强)

        由于代码沙箱是第三方服务,每次调用可能会出现问题,所以要进行日志记录
        采用代理模式来实现日志的打印记录每次调用的状态、请求参数、返回结果

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        @Slf4j
        @AllArgsConstructor
        public class CodeSandBoxProxy implements CodeSandBox {

        private final CodeSandBox codeSandBox;

        @Override
        public ExecuteCodeResponse doExecuteCode(ExecuteCodeRequest executeCodeRequest) {
        log.info("代码沙箱的请求参数为:{}", executeCodeRequest.toString());
        ExecuteCodeResponse executeCodeResponse = codeSandBox.doExecuteCode(executeCodeRequest);
        log.info("代码沙箱的返回结果为:{}", executeCodeResponse.toString());
        return executeCodeResponse;
        }
        }
      4. 代码沙箱创建优化
        由于有多个代码沙箱后续维护更换沙箱时候要去更改实例的对象。
        这样维护成本高,还不高效,甚至会导致项目运行不起来
        所以这里使用工厂模式+spring 配置的方式来创建对应的实例

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        public class CodeSandBoxProxyFactory {

        /**
        * 静态工厂
        *
        * @param type
        * @return
        */
        public static CodeSandBoxProxy newCreateSandBoxFactory(String type) {
        CodeSandBoxEnum codeSandBoxEnum = CodeSandBoxEnum.getCodeSandBoxEnum(type);
        switch (codeSandBoxEnum) {
        case EXAMPLE:
        return new CodeSandBoxProxy(new ExampleCodeSandBox());
        case REMOTE:
        return new CodeSandBoxProxy(new RemoteCodeSandBox());
        case THIRD_PARTY:
        return new CodeSandBoxProxy(new ThirdPartyCodeSandBox());
        default:
        throw new RuntimeException("不存在此类沙箱");
        }
        }
        }

        1
        2
        codesandbox:
        type: remote

        使用读取配置类传入到工厂中得到对应的实体类

        1
        2
        @Value("${codesandbox.type:example}")
        private String type;
      5. 判题服务结果分析
        由于代码沙箱执行编译,需要先指定编程语言,这就导致了每个编程语言都有不同的判题策略,
        如果在返回结果时候进行判断将会有大量的 if-else 导致代码变得难读还不易维护
        所以采用策略模式来优化针对于不同语言的处理策略
        下面只举例 JAVA 其他语言实现方法一样

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        30
        31
        32
        33
        34
        35
        36
        37
        38
        39
        40
        41
        42
        43
        44
        45
        46
        47
        48
        49
        50
        51
        52
        53
        54
        55
        56
        57
        58
        59
        60
        61
        62
        63
        64
        65
        66
        67
        68
        69
        70
        71
        72
        73
        74
        75
        76
        77
        78
        79
        80
        81
        82
        83
        84
        85
        86
        87
        88
        89
        90
        91
        92
        93
        94
        95
        96
        97
        98
        99
        100
        101
        102
        103
        104
        105
        106
        107
        108
        109
        110
        111
        112
        113
        114
        115
        116
        117
        118
        119
        120
        121
        122
        123
        124
        125
        126
        127
        128
        129
        130
        1. 先策略内容实体类,再定义策略接口,然后传入策略内容

        1.1 策略实体类
        @Data
        public class JudgeContext {

        ExecuteCodeResponse executeCodeResponse;

        private List<String> outputList;

        private JudgeConfig judgeConfig;
        }

        1.2 策略接口
        public interface JudgeStrategy {

        /**
        * 策略判断接口
        *
        * @param judgeContext
        * @return
        */
        JudgeInfo doCodeSandBoxJudge(JudgeContext judgeContext);
        }
        2. 定义策略实现类
        public class JavaJudgeStrategyImpl implements JudgeStrategy {

        public static final Long JAVA_RUNNING_TIME = 10000L;

        private static final Long CHANGE_LIMIT = 1000L;

        @Override
        public JudgeInfo doCodeSandBoxJudge(JudgeContext judgeContext) {
        // 判断输出用例是否与配置的用例长度或者是结果是否一致
        ExecuteCodeResponse executeCodeResponse = judgeContext.getExecuteCodeResponse();
        // 问题的系统配置
        List<String> outputList = judgeContext.getOutputList();
        JudgeConfig judgeConfig = judgeContext.getJudgeConfig();
        // 沙箱运行执行返回的结果
        List<String> codeSandBoxOutputList = executeCodeResponse.getOutputList();
        JudgeInfo judgeInfo = executeCodeResponse.getJudgeInfo();
        JudgeInfo resultJudgeInfo = new JudgeInfo();
        resultJudgeInfo.setMemory(judgeInfo.getMemory());
        resultJudgeInfo.setTime(judgeInfo.getTime());
        // 判断输出是否错误
        if (outputList.size() != codeSandBoxOutputList.size()) {
        resultJudgeInfo.setMessage(JudgeQuestionEnum.WRONG_ANSWER.getValue());
        return resultJudgeInfo;
        }
        for (int i = 0; i < outputList.size(); i++) {
        if (!outputList.get(i).equals(codeSandBoxOutputList.get(i))) {
        resultJudgeInfo.setMessage(JudgeQuestionEnum.WRONG_ANSWER.getValue());
        return resultJudgeInfo;
        }
        }
        // 判断时间问题
        if (judgeConfig.getMemoryLimit() + JAVA_RUNNING_TIME < judgeInfo.getTime()) {
        resultJudgeInfo.setMessage(JudgeQuestionEnum.TIME_LIMIT_EXCEEDED.getValue());
        return resultJudgeInfo;
        }
        // 判断内存问题
        if ((judgeConfig.getMemoryLimit() * CHANGE_LIMIT) < judgeInfo.getMemory()) {
        resultJudgeInfo.setMessage(JudgeQuestionEnum.MEMORY_LIMIT_EXCEEDED.getValue());
        return resultJudgeInfo;
        }
        resultJudgeInfo.setMessage(JudgeQuestionEnum.ACCEPTED.getValue());
        return resultJudgeInfo;
        }
        }
        3. 定义策略枚举类
        public enum JudgeStrategyLanguageEnum {

        /**
        * 枚举值
        */
        DEFAULT("default"),
        JAVA("java"),
        JAVA_SCRIPT("javascript"),
        CPP("cpp");

        final private String name;

        JudgeStrategyLanguageEnum(String name) {
        this.name = name;
        }

        public String getValue() {
        return name;
        }

        public static JudgeStrategyLanguageEnum getJudgeStrategyLanguageEnum(String language) {
        switch (language) {
        case "default":
        return JudgeStrategyLanguageEnum.DEFAULT;
        case "java":
        return JudgeStrategyLanguageEnum.JAVA;
        case "javascript":
        return JudgeStrategyLanguageEnum.JAVA_SCRIPT;
        case "cpp":
        return JudgeStrategyLanguageEnum.CPP;
        default:
        return JudgeStrategyLanguageEnum.DEFAULT;
        }
        }
        }
        4. 定义策略管理者JudgeStrategyManager来管理策略的选择
        @Service
        public class JudgeStrategyManager {

        /**
        * 选择策略
        *
        * @param language
        * @param judgeContext
        * @return
        */
        public JudgeInfo getJudgeInfoByJudgeStrategy(String language, JudgeContext judgeContext) {
        JudgeStrategyLanguageEnum judgeStrategyLanguageEnum = JudgeStrategyLanguageEnum.getJudgeStrategyLanguageEnum(language);
        switch (judgeStrategyLanguageEnum) {
        case JAVA:
        return new JavaJudgeStrategyImpl().doCodeSandBoxJudge(judgeContext);
        case JAVA_SCRIPT:
        return new JavaScriptJudgeStrategyImpl().doCodeSandBoxJudge(judgeContext);
        case CPP:
        return new CppJudgeStrategyImpl().doCodeSandBoxJudge(judgeContext);
        default:
        return new DefaultJudgeStrategyImpl().doCodeSandBoxJudge(judgeContext);
        }
        }
        }
    • 判题服务开发

      • 判题服务业务流程分析
        1. 传入题目的提交信息和题目信息
        2. 如果用户包含了正在运行或等待中的题目则返回提交失败
        3. 如果不存在则继续执行并更新状态为判题中 Running 防止重复执行
        4. 将题目信息和提交信息传递给代码沙箱服务,得到执行结果
        5. 根据结果更新提交信息
      1. 定义问题提交接口

        1
        2
        3
        4
        5
        6
        7
        8
        /**
        * 问题提交
        *
        * @param questionSubmitAddRequest
        * @param loginUser
        * @return
        */
        Long doQuestionSubmit(QuestionSubmitAddRequest questionSubmitAddRequest, User loginUser);
      2. 实现问题提交接口
        由于要调用第三方服务所以可以先提交到数据库中,让用户看到状态
        然后使用异步的方式调用代码沙箱接口

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        30
        31
        32
        33
        /**
        * 问题提交
        *
        * @param questionSubmitAddRequest
        * @param loginUser
        * @return
        */
        @Override
        public Long doQuestionSubmit(QuestionSubmitAddRequest questionSubmitAddRequest, User loginUser) {
        Long questionId = questionSubmitAddRequest.getQuestionId();
        Question question = questionService.getById(questionId);
        ThrowUtils.throwIf(Objects.isNull(question), ErrorCode.OPERATION_ERROR, "问题不存在");
        QueryWrapper<QuestionSubmit> questionSubmitQueryWrapper = new QueryWrapper<>();
        questionSubmitQueryWrapper.eq("userId", loginUser.getId());
        questionSubmitQueryWrapper.eq("questionId", questionId);
        List<QuestionSubmit> isHaveQuestionSubmit = this.list(questionSubmitQueryWrapper);
        if (isHaveQuestionSubmit.size() > 0) {
        List<QuestionSubmit> collect = isHaveQuestionSubmit.stream().filter(questionSubmit -> questionSubmit.getStatus() == 1L || questionSubmit.getStatus() == 0L).collect(Collectors.toList());
        ThrowUtils.throwIf(collect.size() > 0, ErrorCode.OPERATION_ERROR, "已经有任务存在了");
        }
        String language = questionSubmitAddRequest.getLanguage();
        ThrowUtils.throwIf(QuestionSubmitLanguageEnum.getEnumByValue(language) == null, ErrorCode.OPERATION_ERROR, "编程语言错误");
        QuestionSubmit questionSubmit = new QuestionSubmit();
        BeanUtils.copyProperties(questionSubmitAddRequest, questionSubmit);
        questionSubmit.setUserId(loginUser.getId());
        boolean save = this.save(questionSubmit);
        ThrowUtils.throwIf(!save, ErrorCode.SYSTEM_ERROR);
        // todo 执行判题 异步执行
        CompletableFuture.runAsync(() -> {
        codeSandBoxService.doCodeSandBoxJudge(this.getById(questionSubmit.getId()), question);
        });
        return questionSubmit.getId();
        }
      3. 定义代码沙箱服务调用接口

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        public interface CodeSandBoxService {
        /**
        * 执行代码沙箱判断接口
        *
        * @param questionSubmit
        * @param question
        * @return
        */
        void doCodeSandBoxJudge(QuestionSubmit questionSubmit, Question question);
        }
      4. 实现代码沙箱服务调用接口

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        30
        31
        32
        33
        34
        35
        36
        37
        38
        39
        40
        41
        42
        43
        44
        45
        46
        47
        48
        49
        50
        51
        52
        53
        54
        55
        56
        57
        58
        59
        60
        61
        62
        63
        64
        65
        66
        67
        68
        69
        @Service
        public class CodeSandBoxServiceImpl implements CodeSandBoxService {


        @Autowired
        private JudgeStrategyManager judgeStrategyManager;

        @Autowired
        @Lazy
        private QuestionSubmitService questionSubmitService;

        @Autowired
        @Lazy
        private QuestionService questionService;

        @Value("${codesandbox.type:example}")
        private String type;

        @Transactional(rollbackFor = BusinessException.class)
        @Override
        public void doCodeSandBoxJudge(QuestionSubmit questionSubmit, Question question) {
        // 更新状态
        QuestionSubmit questionSubmitFirstUpdate = new QuestionSubmit();
        questionSubmitFirstUpdate.setId(questionSubmit.getId());
        questionSubmitFirstUpdate.setStatus(QuestionStatusEnum.RUNNING.getValue());
        boolean isUpdate = questionSubmitService.updateById(questionSubmitFirstUpdate);
        ThrowUtils.throwIf(!isUpdate, ErrorCode.OPERATION_ERROR, "更新任务状态失败!");
        // 构建沙箱执行请求体
        String language = questionSubmit.getLanguage();
        String code = questionSubmit.getCode();
        String judgeCaseStr = question.getJudgeCase();
        List<JudgeCase> questionJudgeCases = JSONUtil.toList(judgeCaseStr, JudgeCase.class);
        List<String> inputList = questionJudgeCases.stream().map(JudgeCase::getInput).collect(Collectors.toList());
        ThrowUtils.throwIf(inputList.size() == 0, ErrorCode.SYSTEM_ERROR);
        // todo 沙箱请求结果
        ExecuteCodeRequest executeCodeRequest = ExecuteCodeRequest.builder()
        .code(code)
        .language(language)
        .inputList(inputList)
        .build();
        // todo 调用沙箱得到结果
        CodeSandBoxProxy codeSandBoxProxy = CodeSandBoxProxyFactory.newCreateSandBoxFactory(type);
        ExecuteCodeResponse executeCodeResponse = codeSandBoxProxy.doExecuteCode(executeCodeRequest);
        // todo 封装策略上下文
        JudgeContext judgeContext = new JudgeContext();
        judgeContext.setExecuteCodeResponse(executeCodeResponse);
        List<String> outputList = questionJudgeCases.stream().map(JudgeCase::getOutput).collect(Collectors.toList());
        judgeContext.setOutputList(outputList);
        String judgeConfig = question.getJudgeConfig();
        JudgeConfig changeJudgeConfig = JSONUtil.toBean(judgeConfig, JudgeConfig.class);
        judgeContext.setJudgeConfig(changeJudgeConfig);
        // todo 调用策略来执行不同的语言判断逻辑返回JudgeInfo
        JudgeInfo judgeInfoByJudgeStrategy = judgeStrategyManager.getJudgeInfoByJudgeStrategy(language, judgeContext);
        // 更新题目状态
        question.setSubmitNum(question.getSubmitNum() + 1);
        if (Objects.equals(judgeInfoByJudgeStrategy.getMessage(), JudgeQuestionEnum.ACCEPTED.getValue())) {
        question.setAcceptedNum(question.getAcceptedNum() + 1);
        }
        boolean b = questionService.updateById(question);
        ThrowUtils.throwIf(!b, ErrorCode.OPERATION_ERROR, "更新题目状态失败!");
        // 更新状态
        QuestionSubmit questionSubmitSecondUpdate = new QuestionSubmit();
        questionSubmitSecondUpdate.setId(questionSubmit.getId());
        questionSubmitSecondUpdate.setStatus(QuestionStatusEnum.SUCCEED.getValue());
        questionSubmitSecondUpdate.setJudgeInfo(JSONUtil.toJsonStr(judgeInfoByJudgeStrategy));
        isUpdate = questionSubmitService.updateById(questionSubmitSecondUpdate);
        ThrowUtils.throwIf(!isUpdate, ErrorCode.OPERATION_ERROR, "更新任务状态失败!");
        }
        }
      5. controller 接口定义

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        /**
        * 提交判题
        *
        * @param questionSubmitAddRequest
        * @param request
        * @return resultNum 本次点赞变化数
        */
        @PostMapping("/question/submit")
        public BaseResponse<Long> doQuestionSubmit(@RequestBody QuestionSubmitAddRequest questionSubmitAddRequest,
        HttpServletRequest request) {
        if (questionSubmitAddRequest == null || questionSubmitAddRequest.getQuestionId() <= 0) {
        throw new BusinessException(ErrorCode.PARAMS_ERROR);
        }
        User loginUser = userService.getLoginUser(request);
        return ResultUtils.success(questionSubmitService.doQuestionSubmit(questionSubmitAddRequest, loginUser));
        }
  • 今日前端开发

    • 组件 Markdown 接入

      推荐使用的 md 编辑器: https://github.com/bytedance/bytemd

      1. 安装
      1
      2
      npm i @bytemd/vue-next
      npm i @bytemd/plugin-highlight @bytem/plugin-gfm
      1. 安装完成,封装组件代码如下:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      <template>
      <Editor
      :value="props.value"
      :plugins="plugins"
      mode="split"
      @change="props.handleChange"
      />
      </template>

      <script setup lang="ts">
      import gfm from "@bytemd/plugin-gfm";
      import { Editor } from "@bytemd/vue-next";
      import { withDefaults, defineProps } from "vue";
      import highlight from "@bytemd/plugin-highlight";
      import frontmatter from "@bytemd/plugin-frontmatter";

      interface Props {
      value: string;
      handleChange: (v: string) => void;
      }

      const plugins = [gfm(), highlight(), frontmatter()];

      const props = withDefaults(defineProps<Props>(), {
      value: "",
      handleChange: (v: string) => {
      console.log(v);
      },
      });
      </script>

      <style scoped></style>

      文档编辑器

    • 代码编辑器 CodeEditor 接入

      微软官方代码编辑器:https://github.com/microsoft/monaco-editor

      1. 安装
      1
      npm install monaco-editor
      1. 在 vue.config.js 配置 webpack 插件:
      1
      2
      3
      4
      5
      6
      7
      8
      const { defineConfig } = require("@vue/cli-service");
      const MonacoWebpackPlugin = require("monaco-editor-webpack-plugin");
      module.exports = defineConfig({
      transpileDependencies: true,
      chainWebpack(config) {
      config.plugin("monaco").use(new MonacoWebpackPlugin());
      },
      });
      1. 安装完成,封装组件代码如下:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      <template>
      <div id="code-container" ref="docCodeRef" style="min-height: 80vh"></div>
      </template>

      <script setup lang="ts">
      import * as monaco from "monaco-editor";
      import { defineProps, onMounted, ref, toRaw, watch, withDefaults } from "vue";

      interface Props {
      value: string;
      language: string;
      handleChange: (v: string) => void;
      }

      const props = withDefaults(defineProps<Props>(), {
      value: "",
      language: "java",
      handleChange: (v: string) => {
      console.log(v);
      },
      });
      const docCodeRef = ref();
      const codeEdit = ref();

      watch(
      () => props.language,
      () => {
      if (codeEdit.value) {
      monaco.editor.setModelLanguage(
      toRaw(codeEdit.value).getModel(),
      props.language
      );
      }
      }
      );

      onMounted(() => {
      if (!docCodeRef.value) {
      return;
      }
      codeEdit.value = monaco.editor.create(docCodeRef.value, {
      value: props.value,
      language: props.language,
      theme: "vs-dark",
      folding: true, // 是否折叠
      foldingHighlight: true, // 折叠等高线
      foldingStrategy: "indentation", // 折叠方式 auto | indentation
      showFoldingControls: "always", // 是否一直显示折叠 always | mouseover
      disableLayerHinting: true, // 等宽优化
      emptySelectionClipboard: false, // 空选择剪切板
      selectionClipboard: false, // 选择剪切板
      automaticLayout: true, // 自动布局
      codeLens: false, // 代码镜头
      scrollBeyondLastLine: false, // 滚动完最后一行后再滚动一屏幕
      colorDecorators: true, // 颜色装饰器
      accessibilitySupport: "off", // 辅助功能支持 "auto" | "off" | "on"
      lineNumbers: "on", // 行号 取值: "on" | "off" | "relative" | "interval" | function
      lineNumbersMinChars: 5, // 行号最小字符 number
      readOnly: false, //是否只读 取值 true | false
      });
      codeEdit.value.onDidChangeModelContent(() => {
      props.handleChange(toRaw(codeEdit.value).getValue());
      });
      });
      </script>

      <style scoped></style>

      代码编辑器

    • 使用 openAPI 生成后端接口代码

      1
      openapi --input http://localhost:8081/api/v2/api-docs --output ./src/service/chenoj-api --client axios

      由于使用的 Session 登录 所以启用 cookie 携带

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      export const OpenAPI: OpenAPIConfig = {
      BASE: "http://localhost:8092",
      VERSION: "1.0",
      WITH_CREDENTIALS: true,
      CREDENTIALS: "include",
      TOKEN: undefined,
      USERNAME: undefined,
      PASSWORD: undefined,
      HEADERS: undefined,
      ENCODE_PATH: undefined,
      };
    • 创建题目和更新题目页面开发,页面内容一致 可以复用

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
      122
      123
      124
      125
      126
      127
      128
      129
      130
      131
      132
      133
      134
      135
      136
      137
      138
      139
      140
      141
      142
      143
      144
      145
      146
      147
      148
      149
      150
      151
      152
      153
      154
      155
      156
      157
      158
      159
      160
      161
      162
      163
      164
      165
      166
      167
      168
      169
      170
      171
      172
      173
      174
      175
      176
      177
      178
      179
      180
      181
      182
      183
      184
      185
      186
      187
      188
      189
      190
      191
      192
      193
      194
      195
      196
      197
      198
      199
      200
      201
      202
      203
      204
      205
      206
      207
      208
      209
      210
      211
      212
      213
      214
      215
      216
      217
      218
      219
      220
      221
      222
      223
      224
      225
      226
      227
      228
      229
      230
      231
      232
      233
      234
      235
      236
      237
      238
      239
      240
      241
      242
      243
      244
      245
      246
      247
      248
      249
      250
      251
      252
      253
      254
      255
      256
      257
      258
      259
      260
      261
      262
      263
      264
      265
      266
      267
      268
      269
      270
      271
      272
      273
      274
      275
      276
      277
      278
      279
      280
      281
      282
      283
      284
      285
      286
      287
      288
      289
      290
      291
      292
      293
      294
      295
      296
      297
      298
      299
      300
      301
      302
      303
      304
      305
      306
      307
      308
      309
      310
      311
      <template>
      <div id="addQuestionView">
      <a-card :title="title" hoverable :header-style="bodyStyle">
      <a-form
      :model="form"
      :style="{ width: '70%', margin: '0 auto' }"
      @submit="handleSubmit"
      >
      <a-form-item field="title" label="标题:">
      <a-input v-model="form.title" placeholder="请输入问题标题" />
      </a-form-item>
      <a-form-item field="content" label="标签:">
      <a-input-tag
      v-model="form.tags"
      placeholder="请输入标签"
      :max-tag-count="4"
      allow-clear
      />
      </a-form-item>
      <a-form-item field="content" label="内容:">
      <MdEditor :value="form.content" :handle-change="handleContent" />
      </a-form-item>
      <a-form-item field="answer" label="答案:">
      <MdEditor :value="form.answer" :handle-change="handleAnswer" />
      </a-form-item>
      <a-form-item
      label="题目配置:"
      :content-flex="false"
      :merge-props="false"
      >
      <a-row :gutter="8">
      <a-col :span="12">
      <a-form-item
      field="judgeConfig.memoryLimit"
      label="内存限制(kb):"
      style="min-width: 500px"
      >
      <a-input-number
      :min="0"
      mode="button"
      :style="{ width: '220px' }"
      v-model="form.judgeConfig.memoryLimit"
      placeholder="内存限制(kb)"
      />
      </a-form-item>
      </a-col>
      <a-col :span="12">
      <a-form-item
      field="judgeConfig.timeLimit"
      label="时间限制(ms):"
      style="min-width: 520px"
      >
      <a-input-number
      :min="0"
      mode="button"
      :style="{ width: '220px' }"
      v-model="form.judgeConfig.timeLimit"
      placeholder="时间限制(ms)"
      />
      </a-form-item>
      </a-col>
      <a-col :span="12">
      <a-form-item
      field="judgeConfig.stackLimit"
      label="堆栈限制(kb):"
      style="min-width: 500px"
      >
      <a-input-number
      :min="0"
      mode="button"
      style="width: 220px"
      v-model="form.judgeConfig.stackLimit"
      placeholder="堆栈内存限制(kb)"
      />
      </a-form-item>
      </a-col>
      </a-row>
      </a-form-item>

      <a-form-item
      label="测试用例:"
      :content-flex="false"
      :merge-props="false"
      >
      <a-form-item
      v-for="(judgeCaseItem, index) of form.judgeCase"
      :field="`judgeCaseItem[${index}].value`"
      :label="`第${index + 1}组测试用例:`"
      :key="index"
      >
      <a-row :gutter="8">
      <a-col :span="12">
      <a-form-item field="judgeCase.input" no-style>
      <a-input
      v-model="judgeCaseItem.input"
      placeholder="请输入测试输入用例"
      />
      </a-form-item>
      </a-col>
      <a-col :span="12">
      <a-form-item field="judgeCase.output" no-style>
      <a-input
      v-model="judgeCaseItem.output"
      placeholder="请输入测试输出用例"
      />
      </a-form-item>
      </a-col>
      </a-row>
      <a-button
      @click="handleDelete(index)"
      :style="{ marginLeft: '10px' }"
      type="dashed"
      status="danger"
      >删除用例
      </a-button>
      </a-form-item>
      <div>
      <a-button
      type="dashed"
      status="success"
      style="margin-left: 30px"
      @click="handleAdd"
      >添加用例
      </a-button>
      </div>
      </a-form-item>
      <a-form-item>
      <a-button
      html-type="submit"
      type="primary"
      :loading="loading"
      style="width: 180px; margin: 0 auto"
      >{{ title }}
      </a-button>
      </a-form-item>
      </a-form>
      </a-card>
      </div>
      </template>

      <script setup lang="ts">
      import { onMounted, ref, watch } from "vue";
      import MdEditor from "@/components/MdEditor.vue";
      import { Message } from "@arco-design/web-vue";
      import {
      QuestionAddRequest,
      QuestionControllerService,
      } from "@/service/chenoj-api";
      import { useRoute, useRouter } from "vue-router";

      const bodyStyle = ref({
      display: "flex",
      alignContent: "center",
      justifyContent: "center",
      flexDirection: "column",
      letterSpacing: "5px",
      });

      const route = useRoute();
      const isUpdate = ref(false);
      const title = ref("添加题目");
      const questionId = ref<any>("");

      const handleGetUpdateQuestion = async () => {
      try {
      const res = await QuestionControllerService.getQuestionByIdUsingGet(
      questionId.value
      );
      if (res.code == 0) {
      const data = res.data;
      if (data) {
      if (data.tags) {
      data.tags = JSON.parse(data.tags);
      }
      if (data.judgeCase) {
      data.judgeCase = JSON.parse(data.judgeCase);
      } else {
      data.judgeCase = "";
      }
      if (data.judgeConfig) {
      data.judgeConfig = JSON.parse(data.judgeConfig);
      } else {
      data.judgeConfig = "";
      }
      if (data.answer == undefined) {
      data.answer = "";
      }
      form.value = JSON.parse(JSON.stringify(data));
      }
      } else {
      Message.error("请求数据失败!");
      }
      } catch (e) {
      Message.error("请求失败,请重试!");
      }
      };

      onMounted(() => {
      const fullPath = route.fullPath.split("/");
      if (fullPath[1] == "update") {
      isUpdate.value = true;
      title.value = "更新题目";
      questionId.value = fullPath[2].split("=")[1];
      handleGetUpdateQuestion();
      }
      });

      // 监听页面 刷新表单状态
      watch(
      () => route.fullPath,
      () => {
      if (route.fullPath[1] !== "update") {
      form.value = question;
      }
      }
      );

      const question = {
      answer: "",
      content: "",
      judgeCase: [
      {
      input: "",
      output: "",
      },
      ],
      judgeConfig: {
      memoryLimit: 100,
      stackLimit: 100,
      timeLimit: 100,
      },
      tags: [],
      title: "",
      };

      const form = ref(question);

      const loading = ref(false);

      const handleAnswer = (value: string) => {
      form.value.answer = value;
      };

      const handleContent = (value: string) => {
      form.value.content = value;
      };

      const handleAdd = () => {
      if (form.value.judgeCase) {
      if (form.value.judgeCase.length + 1 > 4) {
      Message.warning("用例最多四个");
      return;
      }
      form.value.judgeCase.push({
      input: "",
      output: "",
      });
      }
      };
      const handleDelete = (index: number) => {
      if (form.value.judgeCase) {
      if (form.value.judgeCase.length == 1) {
      Message.warning("用例最少为一个");
      return;
      }
      form.value.judgeCase.splice(index, 1);
      }
      };

      const router = useRouter();

      const handleSubmit = async () => {
      try {
      loading.value = true;
      if (isUpdate.value) {
      console.log(form);
      const res = await QuestionControllerService.updateQuestionUsingPost(
      form.value
      );
      if (res.code == 0) {
      Message.success("更新成功!");
      await router.push({
      path: "/list/question",
      replace: true,
      });
      } else {
      Message.error("更新失败,", res.message);
      }
      } else {
      const res = await QuestionControllerService.addQuestionUsingPost(
      form.value
      );
      if (res.code == 0) {
      Message.success("添加成功!");
      form.value = question;
      } else {
      Message.error("添加失败,", res.message);
      }
      }
      } catch (e) {
      Message.error("请求失败,请稍后重试!");
      }
      loading.value = false;
      };
      </script>

      <style scoped>
      #addQuestionView {
      }
      </style>

      效果图1
      效果图2

    • 路由结合权限

      比如:

      1
      2
      3
      4
      5
      6
      7
      8
      {
      path: "/list/submit/question",
      name: "提交题目列表",
      component: () => import("@/views/question/ListSubmitQuestionView.vue"),
      meta: {
      access: ACCESS.USER,
      },
      },
    • 题目列表开发

      与 BI 项目一致引入 table 组件进行封装即可

      题目列表

    • 题目浏览页开发

      与 BI 项目一致引入 table 组件进行封装即可

      题目浏览页面