博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
结对项目 - 四则运算生成器 by陈俊旭 张婷
阅读量:5273 次
发布时间:2019-06-14

本文共 22277 字,大约阅读时间需要 74 分钟。

四则运算生成器

合作者:16计科5班 陈俊旭(3116004630) 16计科5班 张婷(3216004672)

 

1. 项目介绍

该项目需求为实现一个自动生成小学四则运算题目的命令行程序,并且可以通过命令行参数控制生成题目的个数还有控制题中数值的范围,并满足一些其它的需求。我们的结对项目完成的需求如下:

需求 是否实现
使用 -n 参数控制生成题目的个数
使用 -r 参数控制题目中数值(自然数、真分数和真分数分母)的范围
生成的题目中计算过程不会产生负数
形如 e1 ÷ e2 的子表达式结果为真分数
每道题目中出现的运算符个数不超过3个
程序一次运行生成的题目不能重复

 

2. 耗时预计

PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
Planning 计划 90
· Estimate · 估计这个任务需要多少时间 90
Development 开发 1260
· Analysis · 需求分析 (包括学习新技术) 180
· Design Spec · 生成设计文档 30
· Design Review · 设计复审 (和同事审核设计文档) 120
· Coding Standard · 代码规范 (为目前的开发制定合适的规范) 30
· Design · 具体设计 240
· Coding · 具体编码 300
· Code Review · 代码复审 240
· Test · 测试(自我测试,修改代码,提交修改) 120
Reporting 报告 150
· Test Report · 测试报告 60
· Size Measurement · 计算工作量 30
· Postmortem & Process Improvement Plan · 事后总结, 并提出过程改进计划 60
合计 1500

(实际耗时见下面的 7.实际耗时)

 

3. 效能分析

以下是生成 100万道范围为 0 到 20 的效能分析

参数为:

1489095-20180926204514806-922979267.png

经过多次测试,生成 100万道题目的平均用时为35s,我们选取其中的一次测试进行性能分析

首先是 CPU、内存以及 GC 等的使用情况:

1489095-20180926204537817-1539732598.png

其中我们主要研究内存和CPU的使用情况,可以看到生成这么多题目总计需要使用 1GB内存,而且CPU的利用率时高时低,经过分析CPU主要的计算用在表达式去重上;也就是每次生成一个表达式都需要与之前的所有表达式进行比较:

1489095-20180926204552350-709993177.png

1489095-20180926204613941-638013575.png

接着是各个类的实际使用情况,可以看到最多的是对字符串(转换成 char 数组)进行操作:

1489095-20180926204627603-1282194646.png

 

4. 设计实现过程

项目中定义的类和函数的功能如下:

  • com/laomi/bo/Expression.java: 定义了每道表达式的属性,包括运算符、运算数、括号等等
  • com/laomi/service/Calulator.java:用于计算每道表达式的答案
    • count():该函数用于获取具体的运算数,同时根据运算符号调用其它的各运算函数
  • com/laomi/service/ExerciseBook.java:用于生成问卷和答案,同时提供批改问卷的功能
    • generateFile():该函数用于生成问卷和答案文件
    • checkAnswers():该函数用于批改问卷并生成分数
  • com/laomi/service/Producer.java:真正生成表达式的地点,可以配置各种生成参数
    • produce():统筹各生成函数的主函数
  • com/laomi/service/Fraction.java: 实现小数转换分数,以及模拟分数的四则运算
  • com/laomi/view/Main.java:进行命令参数控制

(以上只是列举了部分函数的功能,具体的实现请看 5.代码说明

下面是关键函数 produce()的流程图

st=>start: 题目数量 n 和运算范围 r, 已生成表达式数量 ie=>end: 返回所有符合条件的表达式集合cond=>condition: i < nnums=>operation: 生成运算数和运算符号parenthesis=>operation: 生成括号fraction=>operation: 转换为分数answer=>operation: 计算表达式答案st->condcond(yes)->nums->parenthesis->fraction->answer->econd(no)->e

 

5. 代码说明

Bo层

com/laomi/bo/Expression.java

public class Expression {    private Number[] nums; // 运算数(小数形式)    private String[] ultimateNumber; // 运算数(分数形式)    private char[] ops; // 运算符    private int len; // 运算数个数    private int[][] parenthesis; // 括号位置    private boolean isCorrect; // 表达式是否合法    private double answer; // 答案(小数形式)    private String ultimateAnswer; // 答案(分数形式)    // 此处省略 Get 和 Set 方法        /**     * 将表达式转化为一个数组     * @return 数组     */        public List
zhangting() { List
ting = new LinkedList<>(); Fraction.convertToFraction(this); setNumsToString(Fraction.elements); for (int i = 0; i < len; i++) { int left = parenthesis[i][0]; int right = parenthesis[i][1]; while (left > 0) { ting.add("("); left--; } ting.add(numsToString[i] + ""); while (right > 0) { ting.add(")"); right--; } if (i < len - 1) { ting.add(ops[i] + ""); } } return ting; }// public String printOrigin() { StringBuilder s = new StringBuilder(); for (int i = 0; i < len; i++) { int left = parenthesis[i][0]; int right = parenthesis[i][1]; while (left > 0) { s.append("("); left--; } s.append(nums[i]).append(" "); while (right > 0) { s = new StringBuilder(s.toString().trim()); s.append(") "); right--; } if (i < len - 1) { s.append(ops[i]).append(" "); } } return s.toString().trim(); } @Override public String toString() { StringBuilder s = new StringBuilder(); for (int i = 0; i < len; i++) { while (parenthesis[i][0] > 0) { s.append("("); parenthesis[i][0]--; }// s.append(ultimateNumber[i]).append(" "); s.append(numsToString[i]).append(" "); while (parenthesis[i][1] > 0) { s = new StringBuilder(s.toString().trim()); s.append(") "); parenthesis[i][1]--; } if (i < len - 1) { s.append(ops[i]).append(" "); } } return s.toString(); } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Expression that = (Expression) o; Number[] comThat; if(len!=that.len) { return false; } Number[] thatnums = new Number[that.len]; Number[] thisnums = new Number[len]; for(int i=0;i

Service层

com/laomi/service/Producer.java

public class Producer {    private static final char[] OPERATIONS = {'+', '-', 'x', '÷'};    /**     * 生成括号概率, 范围为 0~1, 数值越大生成括号概率越大     */    private double parenthesisFactor = 0.7;    /**     * 生成分数概率, 范围为 0~1, 数字越大生成分数概率越大     */    private double fractionFactor = 0.2;    /**     * 参与运算的数值的个数     */    private int maxLen = 4;    /**     * 各个算数值的最大值(不包括)     */    private int numberBound;    /**     * 生成表达式的数量     */    private int amount;    /**     * 生成表达式     * @return 表达式集合     */    public Set
produce() { Set
expressions = new HashSet<>(); int count = 0; while (count < amount) { Expression e = new Expression(ThreadLocalRandom.current().nextInt(2, maxLen + 1)); for (int i = 0; i < e.getLen(); i++) { // 生成运算数 e.getNums()[i] = getRandomNumber(); if (i < e.getLen() - 1) { // 生成运算符号 e.getOps()[i] = getRandomOperation(); } } // 生成括号 polish(e, 0, e.getLen() - 1); // 计算答案 String answer = Calculator.count(e.zhangting()); // 小数转换为分数 Fraction.convertToFraction(e); if (answer == null || answer.contains("-")) { e.setCorrect(false); } else { e.setUltimateAnswer(answer); } if (e.isCorrect() && !expressions.contains(e)) { expressions.add(e); count++; } } return expressions; } /** * 生成随机运算符 * @return 随即运算符 */ private char getRandomOperation() { return OPERATIONS[new Random().nextInt(OPERATIONS.length)]; } /** * 生成 0(包括 0)以上的随机数字 * * @return 随机数字 */ private Number getRandomNumber() { if (new Random().nextDouble() < fractionFactor) { double d = ThreadLocalRandom.current().nextDouble(0, numberBound); d = BigDecimal.valueOf(d).setScale(2, RoundingMode.HALF_UP).doubleValue(); if (d == (int) d) { return (int) d; } else { return d; } } else { return new Random().nextInt(numberBound); } } /** * 递归生成括号 * @param e 表达式 * @param start 从哪里开始生成括号 * @param end 到哪里停止生成括号 */ private void polish(Expression e, int start, int end) { if (end > start && new Random().nextDouble() < parenthesisFactor) { int middle = ThreadLocalRandom.current().nextInt(start, end); // 避免在表达式最外层套括号 if (!(start == 0 && end == e.getLen() - 1)) { if (new Random().nextDouble() < parenthesisFactor) { e.getParenthesis()[start][0]++; e.getParenthesis()[end][1]++; } } polish(e, start, middle); polish(e, middle + 1, end); } } public double getParenthesisFactor() { return parenthesisFactor; } public void setParenthesisFactor(double parenthesisFactor) { this.parenthesisFactor = parenthesisFactor; } public double getFractionFactor() { return fractionFactor; } public void setFractionFactor(double fractionFactor) { this.fractionFactor = fractionFactor; } public int getMaxLen() { return maxLen; } public void setMaxLen(int maxLen) { this.maxLen = maxLen; } public int getNumberBound() { return numberBound; } public void setNumberBound(int numberBound) { this.numberBound = numberBound; } public int getAmount() { return amount; } public void setAmount(int amount) { this.amount = amount; }}

com/laomi/service/Calculator.java

public class Calculator {    /**     * 对表达式进行计算     * @param e 表达式     * @return 运算结果     */    public static String count(List
e) { Stack
stack = new Stack<>(); for (String arg : e) { if (!")".equals(arg)) { stack.push(arg); } else { List
subExpression = new LinkedList<>(); while (!stack.empty()) { if ("(".equals(stack.peek())) { stack.pop(); String result = countWithoutParenthesis(subExpression); if (result == null) { return null; } stack.push(result + ""); break; } else { subExpression.add(0, stack.pop()); } } } } String result; if (stack.size() > 1) { List
subExpression = new LinkedList<>(); while (!stack.empty()) { subExpression.add(0, stack.pop()); } result = countWithoutParenthesis(subExpression); if (result == null) { return null; } } else { result = stack.pop(); } return result; } /** * 计算没有括号参与的情景, 类似 3 + 2 / 10 * @param e 表达式 * @return 运算结果 */ private static String countWithoutParenthesis(List
e) { List
exp = new CopyOnWriteArrayList<>(e); int start = 0; int index; while ((index = getOperationIndex(exp, start, "x", "÷")) != -1) { if( operation(exp, index) == -1) { return null; } start = index - 1; } start = 0; while ((index = getOperationIndex(exp, start, "+", "-")) != -1) { if (operation(exp, index) == -1) { return null; } start = index - 1; } return exp.get(0); } /** * 获取表达式特定操作符的下标 * @param e 表达式 * @param start 从哪里开始获取 * @param ops 特定的操作符 * @return 下标 */ private static int getOperationIndex(List
e, int start, String... ops) { for (int i = start; i < e.size(); i++) { String arg = e.get(i); for (String op : ops) { if (op.equals(arg)) { return i; } } } return -1; } /** * 根据操作符对操作数进行操纵 * @param e 表达式 * @param index 操作符下标 * @return 操作成功返回1, 失败返回-1 */ private static int operation(List
e, int index) { String op = e.get(index); String pre = e.get(index-1); String post = e.get(index+1); // 从后往前删除 e.remove(index + 1); e.remove(index); e.remove(index - 1); switch (op) { case "x": e.add(index - 1, Fraction.fractionMultiply(pre,post)+ ""); break; case "÷": if (Fraction.fractionCompare(pre,post).equals(">")) { // 避免产生假分数 return -1; } String divide = Fraction.fractionDivide(pre, post); if (divide == null) { // 出现除以0的情况 return -1; } e.add(index - 1, divide); break; case "+": e.add(index - 1, Fraction.fractionAdd(pre,post) + ""); break; case "-": if (Fraction.fractionCompare(pre,post).equals("<")) { // 避免产生负数 return -1; } e.add(index - 1, Fraction.fractionSubstract(pre,post) + ""); break; default: break; } return 1; }}

com/laomi/service/ExerciseBook.java

public class ExerciseBook {    /**     * 生成问卷和答案文本     * @param expressions 表达式列表     */    public static void generateFile(List
expressions) { DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss"); LocalDateTime now = LocalDateTime.now(); String current = dtf.format(now); // 生成问卷 StringBuilder question = new StringBuilder(); //将表达式中的假分数化成真分数 for (int i = 0; i < expressions.size(); i++) { Expression e = expressions.get(i); for(int j=0;j
Correct = new ArrayList<>(); List
False = new ArrayList<>(); File excersice = new File(exerciseFile); String answer_timestamp = exerciseFile.substring(exerciseFile.lastIndexOf("_") + 1, exerciseFile.lastIndexOf(".")); File answer = new File(System.getProperty("user.dir") + "/" + "Answer_" + answer_timestamp + ".txt"); try { FileReader readuser = new FileReader(new File(userAnswerPath)); FileReader readanswer = new FileReader(answer); BufferedReader bufferuser = new BufferedReader(readuser); BufferedReader bufferanswer = new BufferedReader(readanswer); String s_answer = null; String s_user = null; while ((s_answer = bufferanswer.readLine()) != null && (s_user = bufferuser.readLine()) != null) { if (s_answer.equals(s_user)) { Correct.add(s_answer.substring(0, s_answer.lastIndexOf("."))); } else { False.add(s_answer.substring(0, s_answer.lastIndexOf("."))); } } } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } File Grade = new File(System.getProperty("user.dir") + "/" + "Grade" + ".txt"); StringBuilder s_grade = new StringBuilder("Correct: "); s_grade.append(Correct.size() + "("); for (int i = 0; i < Correct.size(); i++) { s_grade.append(Correct.get(i) + ","); } s_grade.append(")" + "\n"); s_grade.append("Wrong: "); s_grade.append(False.size() + "("); for (int i = 0; i < False.size(); i++) { s_grade.append(False.get(i) + ","); } s_grade.append(")" + "\n"); String tograde = s_grade.toString(); try { PrintStream ps = new PrintStream(new FileOutputStream(Grade)); ps.append(tograde);// 在已有的基础上添加字符串 } catch (FileNotFoundException e) { e.printStackTrace(); } }}

com/laomi/service/Fraction.java

public class Fraction {    public static String[] elements;    //最大公约数    private static Integer gcd(Integer a, Integer b)    {        if(a==0){            return b;        }        return gcd(b%a,a);    }    private static void getGCD(Integer a, Integer b)    {        int gcd = gcd(a,b);        if(gcd!=0) {            a /= gcd(a,b);            b /= gcd(a,b);        }    }    //最小公倍数    private static Integer lcm(Integer a, Integer b)    {        if(gcd(a,b)!=0)        {            return a*b/gcd(a,b);        }        else        {            return 0;        }    }    /**     * 将随机生成的小数改为分数     * @param number 要改成分数的小数     * @return String     */    public static String toFraction(double number)    {        int zhenshu = 0;        zhenshu = (int)Math.floor(number);        //System.out.println(zhenshu);        double decimal = number-zhenshu;        //System.out.println(decimal);        int cnt = 0;        Integer all = 0;        Integer base = 1;        String s = (BigDecimal.valueOf(decimal).toString());        int start = s.indexOf(".")+1;        int len = min(s.length()-start,start+1);        for(int i=start;i<=len;i++)        {            all = all+(Integer)(s.charAt(i)-'0')*base;            base = base*10;        }        String res = null;        Integer g = gcd(all,base);        if(g!=0) {            all /= g;            base /= g;            String frac = all + "/" + base;            if(all==0)            {                res = ""+zhenshu;            }            else {                res = fractionAdd(String.valueOf(zhenshu), frac);            }        }        return res;    }    /**     * 将分数改成真分数     * @param number 要改成分数的小数     * @return String     */    public static String toRealFraction(String number)    {        String res = null;        if(number.contains("/"))        {            int top = Integer.parseInt(number.substring(0,number.indexOf("/")));            int down = Integer.parseInt(number.substring(number.indexOf("/")+1));            if(top>down)            {                int zhenshu = top/down;                int res_top = top-zhenshu*down;                int res_down = down;                if(res_top!=0)                {                    int g = gcd(res_top,res_down);                    res_top/=g;                    res_down/=g;                    res = zhenshu+"'"+res_top+"/"+res_down;                }                else{                    res = ""+zhenshu;                }            }            else            {                int g = gcd(top,down);                if(g!=0)                {                    top/=g;                    down/=g;                }                res = top+"/"+down;            }            if(top==0)            {                res = "0";            }        }       // System.out.println(res);        return res;    }    /**     * 将Expression的表达式中的算子出现的小数类型转换为真分数     * @param e 计算表达式Expression类     */    public static void convertToFraction(Expression e)    {        int len  = e.getLen();        elements = new String[len];        for(int i=0;i
next_) { ans = ">"; } else if(pre_
next_top) { ans = ">"; } else if(pre_top
next_top) { ans = ">"; } else if(pre_top
next_top) { ans = ">"; } else if(pre_top

View层

com/laomi/view/Main.java

public class Main {    private static final String AMOUNT = "-n";    private static final String NUMBER_BOUND = "-r";    private static final String EXERCISE_FILE = "-e";    private static final String ANSWER_FILE = "-a";    public static void main(String[] args) {        if (checkArgs(args, "-n", "-r")) {            generate(args);        } else if (checkArgs(args, "-e", "-a")) {            correct(args);        } else {            System.out.println("参数不合法");        }    }    private static void generate(String[] args) {        int amount = -1;        int numberBound = -1;        try {            amount = Integer.parseInt(args[findArgIndex(args, AMOUNT)]);            numberBound = Integer.parseInt(args[findArgIndex(args, NUMBER_BOUND)]);        } catch (Exception ignore) {            System.out.println("参数不合法");            System.exit(-1);        }        if (amount == -1 || numberBound == -1) {            System.out.println("参数不合法");            System.exit(-1);        }        Producer producer = new Producer();        producer.setAmount(amount);        producer.setNumberBound(numberBound);        // 生成表达式        Set
expressions = producer.produce(); // 分别生成算术表达式和答案文件 ExerciseBook.generateFile(new ArrayList<>(expressions)); System.out.println("成功生成 " + amount + " 道算术题"); } private static void correct(String[] args) { String exerciseFile = null; String answerFile = null; try { exerciseFile = args[findArgIndex(args, EXERCISE_FILE)]; answerFile = args[findArgIndex(args, ANSWER_FILE)]; } catch (Exception ignore) { System.out.println("参数不合法"); System.exit(-1); } ExerciseBook.checkAnswers(exerciseFile, answerFile); System.out.println("分数已批改完毕 d=====( ̄▽ ̄*)b"); } private static boolean checkArgs(String[] args, String...target) { if (target == null || args == null) { return false; } for (String t : target) { boolean exist = false; for (String arg : args) { if (t.equals(arg)) { exist = true; break; } } if (!exist) { return false; } } return true; } private static int findArgIndex(String[] args, String target) { if (target == null || args == null) { return -1; } for (int i = 0; i < args.length; i++) { if (target.equals(args[i])) { return i + 1; } } return -1; }}

 

6. 测试说明

以下所有的测试文件都位于 的 sample 文件夹内

生成题目

1489095-20180926204649416-1798242823.png

1489095-20180926204703582-1285755621.png

1489095-20180926204718992-1529817007.png

批改作业

1489095-20180926204731926-492584316.png

1489095-20180926204811791-831550136.png

1489095-20180926204825469-792081639.png

 

7. 实际耗时

PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
Planning 计划 90 50
· Estimate · 估计这个任务需要多少时间 90 50
Development 开发 1260 1330
· Analysis · 需求分析 (包括学习新技术) 180 120
· Design Spec · 生成设计文档 30 10
· Design Review · 设计复审 (和同事审核设计文档) 120 150
· Coding Standard · 代码规范 (为目前的开发制定合适的规范) 30 10
· Design · 具体设计 240 360
· Coding · 具体编码 300 360
· Code Review · 代码复审 240 300
· Test · 测试(自我测试,修改代码,提交修改) 120 20
Reporting 报告 150 280
· Test Report · 测试报告 60 80
· Size Measurement · 计算工作量 30 20
· Postmortem & Process Improvement Plan · 事后总结, 并提出过程改进计划 60 180
合计 1500 1660

 

8. 项目小结

这个项目从发布题目到最后真是开始写代码,我们花了很多的时在讨论具体应该如何实现上。相较于上个个人项目的单打独斗一个人思考需求和实现,结对编程很明显我们能在讨论过程发现彼此想的不周到的地方。用了几天时间做完这个项目,我们发现,程序中的错误减少了,质量提高了不少,这给我们后来测试代码提供了更多宝贵的时间和精力。除此之外,在两个人的结对过程中,由于有相互监督,因此无论是谁充当键盘手,他的想法都要受到对方的评价。正式因为有这种压力在,所以相比于上个单词统计项目,这个项目的代码质量也比之前更上了一个阶梯。在实际结对编程中,我们划分了模块,算式表达式的生成和答案计算主要由陈同学担任键盘手,而张同学则作为监督者提供指导意见;而到了去重以及分母转换模块,则换成张同学做键盘手,陈同学在负责围观和监督,两种身份不同转换,也算是一种劳逸结合。

陈同学对张同学对结对表现的评价:这个项目最终能很快写完离不开张同学的指导,跟她一起结对编程真的非常愉快( ̄︶ ̄*))。比如说我们在讨论如何生成括号时,我先提出了一种使用模拟的暴力方法进行括号的匹配,我那时候执意认为这样子可以求解,折腾了一个晚上后还是投降了。最终还是张同学给我提供了可以采用二叉树的思想递归生成括号的思路,我才能找到合理的解决方法,这样的场合还有很多很多。可以说如果没有结对编程,没有张同学在旁边认真听我叽叽呱呱的话,很多地方我真的无从下笔,她是一名很懂得倾听的coder,你会注意到她在很认真地听你讲话。张同学真的是一名非常认真负责的同学,在转换真分数的问题上我们一开始没有考虑周全,导致后期改代码非常麻烦,这里面也有我的问题,是我没有及时将具体的编码实现跟张同学讲,害得她要改好多东西....她真的特别辛苦。当然,在这个过程中能看到张同学的进步是我最高兴的,将来还有很多很多合作,一起加油吧~

张同学对陈同学的结对表现的评价:首先商业互吹下陈俊旭同学,跟陈同学结对学到很多东西,刚开始对这个项目只是对某些需求的实现有想法,但是由于统筹能力缺乏对整个项目的整体设计比较懵,所以陈同学在设计这方面较我而言做了很多工作。在和陈同学结对的过程中学到了很多,包括对封装性的进一步了解和对整体设计有了一些感悟。结对过程一开始由陈同学当键盘手,而我是在旁边负责及时差错和提供思路。<( ̄︶ ̄)↗[GO!]但是到了后面的分数计算和表达式查重就由我接锅,结果被自己坑了一把,没有看清陈同学在之前写的表达式的具体计算方式是由小数计算,所以在得到答案是严重失去了精度,只好模拟分数的四则运算过程。实际上我是对整个项目的结构的修改有一定的不好的影响,而且代码规范还是没有达到陈同学的预期.....((/- -)/。但是和陈同学的合作过程很愉快,我有什么问题他都会详细为我解答,作为一个messy coder估计我的结对伙伴很头疼但是感谢他坚持了下来~~

转载于:https://www.cnblogs.com/zkyyo/p/9709591.html

你可能感兴趣的文章
bzoj:3616: War
查看>>
qrcode length overflow (1632>1056)--qrcode.js使用过程中二维码长度溢出解决办法
查看>>
我踩过的听过的那些坑
查看>>
CSS 制作3D旋转视频
查看>>
JQ全选反选
查看>>
sql语句中in和exists的区别
查看>>
python-处理文件
查看>>
eclipse6.5 自动注册代码
查看>>
spring配置文件比较全的约束
查看>>
Sqlit--学习教程(附加数据库)
查看>>
9月2日笔记
查看>>
文件拷贝 上传下载 输入流输出流个人小结,仅供自己使用
查看>>
OptimalSolution(2)--二叉树问题(2)BST、BBT、BSBT
查看>>
342.4的幂
查看>>
Anroid 搭建Maven私服(Android Studio)
查看>>
Beta 冲刺 (2/7)
查看>>
PHP开发环境配置系列(三)-项目源码映射
查看>>
腾讯官方教程
查看>>
React跨域问题解决
查看>>
windows系统下删除Apache服务器有两种方法:
查看>>