基于Spring+Vue的前后端分离的计算器

news2024/11/26 12:18:30

麻雀虽小,五脏俱全

该项目已部署上线:http://calculator.wushf.top/
并通过Gitee Go流水线实现持续部署。

需求分析

image.png

  • 表达式求值
    • 支持加减乘除四则运算、支持高精度
  • 获取日志

Api文档定义

前后端分离,人不分离

通过Apifox定义接口细节,协调前后端开发工作。
image.png

软件架构设计

Spring-MVC

把软件系统分为三个基本部分:模型(Model)、视图(View)和控制器(Controller)。

image.png
Controller起到不同层面间的组织作用,用于控制应用程序的流程。它处理事件并作出响应。本项目共定义了一个controller

  • CalculateController:接收/calculate的请求,返回JSON

Model由实体Bean来实现,包括PojoMappingService对象,在源代码/pojo目录下:

  • /pojo
    • CalculateLogPojoORM技术,对象关系映射自数据库表项calculate_log
    • CalculateRequestPojoDTO数据传输对象,封装发送到/calculatePOST请求。
    • QueryPojoDTO数据传输对象,封装发送到/calculateGET请求。
    • ResultPojo<T>DTO数据传输对象,封装全局的请求返回对象。
  • /mapping
    • LogMapper:持久层实现,负责面向本地文件的日志读写。
    • LocalLogMapper:持久层实现,负责面向关系型数据MySql的日志读写。
  • /service
    • CalculateService:计算服务,提供静态方法用于实现表达式求值
    • LogService:日志服务,用于调用持久层方法,实现日志记录
    • QueryService:日志服务,用于调用持久层方法,实现日志读取

View层通过Vue实现,前后端分离。

Vue-MVVM

Model–view–viewmodel:使用命令Command控制,使用Binding来绑定相关数据

image.png
Modelscript标签下实现。
View视图层在templatestyle标签下实现。
本项目需求较简单,主要代码在App.vue中完成。

Vue端详细设计

image.png

声名响应式对象

image.png

  • history:日志列表
  • expression:输入的表达式
  • res:运算结果
  • queryFrom:查询操作的query参数,取值filesql,表示从文件中读或数据库中读
  • queryPage:查询操作的query参数,值类型为数字,表示查询第几页,默认为0
  • querySize:查询操作的query参数,值类型为数字,每页的大小,默认为10

查询历史记录

该操作的query参数从全局的响应式对象中获取。
input标签可能会将用户输入的数据类型改为string,需要判断是否为数字,如果类型是字符串,通过正则表达式判断是否为整数字符串。

const getHistory = () => {

  if (queryFrom.value != "sql" && queryFrom.value != 'file') {
    Notification.error('sql or file');
    return;
  } else if ((typeof queryPage.value === 'number' || /^\d+$/.test(queryPage.value)) && queryPage.value % 1 === 0 && queryPage.value >= 0
             && (typeof querySize.value === 'number' || /^\d+$/.test(querySize.value)) && querySize.value % 1 === 0 && querySize.value > 0) {
    axios({
      method: "get",
      url: baseUrl + "/calculate",
      params: {
        from: queryFrom.value,
        page: queryPage.value,
        size: querySize.value
      }
    }).then(({data}) => {
      history.splice(0, history.length, ...data?.data);
    })
  } else {
    Notification.error('别搞');
  }
}

当用户输入内容非法时,通过arco.design提供的通知组件,告知用户错误原因。
image.png

发送计算请求

需要对用户输入的字符串进行预处理:

  • 如果最后一个字符是运算符而非数字,那么递归地删除最后一个运算符字符
const calculate = () => {
  expression.value.replace(/([^+\-*v]|^)-/g, '$1+-');
  let expressionLength = expression.value.length;
  let lastChar = expressionLength > 0 ? expression.value[expressionLength - 1] : '';
  let prefixExpression = expressionLength == 0 ? expression.value.substring(0, expression.value.length - 1) : "";
  while (operatorSet.indexOf(lastChar) != -1) {
    expression.value = prefixExpression;
    expressionLength = expression.value.length;
    lastChar = expressionLength > 0 ? expression.value[expressionLength - 1] : '';
    prefixExpression = expressionLength == 0 ? expression.value.substring(0, expression.value.length - 1) : "";
  }
  axios({
    method: "post",
    url: baseUrl + "/calculate",
    data: {
      expression: expression.value
    }
  }).then(({data}) => {
    res.value = data?.data;
  }).catch(() => {
    res.value = "Error"
  })
}

输入操作

输入操作可能的输入比较多,包括数字、运算符、小数点.等,需要分别判断。
image.png
对输入事件只定义一个input函数,具体的内容通过函数传参输入,提高可重用性。
通过将编译器指定为typescript<script setup lang="ts">,实现类型判断、语法检查等操作。确保数字字符在输入时,也是string类型,而非number

const pointFlag = ref(false);
const input = (cmd: string) => {
  expression.value = String(expression.value);
  const expressionLength = expression.value.length;
  const lastChar = expressionLength > 0 ? expression.value[expressionLength - 1] : '';
  const prefixExpression = expressionLength > 1 ? expression.value.substring(0, expressionLength - 1) : "";
  if (res.value == "Calculating") {
    return;
  } else if (cmd == "C") {
    expression.value = res.value;
    res.value = "";
  } else if (cmd == "<-") {
    expression.value = prefixExpression;
  } else if (operatorSet.indexOf(cmd) != -1) {
    if (cmd != '-' && expressionLength == 0) {
      return;
    } else if (operatorSet.indexOf(lastChar) != -1 || lastChar == '.') {
      expression.value = prefixExpression + cmd;
    } else {
      expression.value = expression.value + cmd;
      pointFlag.value = false;
    }
  } else if (cmd == '=') {
    calculate();
    res.value = "Calculating"
    pointFlag.value = false;
  } else if (cmd == '.') {
    if (lastChar != '.' && !pointFlag.value && operatorSet.indexOf(lastChar) == -1) {
      expression.value = expression.value + cmd;
      pointFlag.value = true;
    }
  } else {
    expression.value = expression.value + cmd;
  }
}

当连续输入多个运算符时,需要判断运算符是否合法,比如,连续输入+-时,应将上一步输入的+修改为-
一个数字应当只有一个小数点,即两个运算符之间最多有一个。相关边界条件的处理操作,均在input中实现。

响应式字体

通过JS查询文本父元素的宽高,动态赋值,并添加监听器,在每次窗口大小变化时,更新文字大小。

const updateFontSize = () => {
  const root = document.documentElement;
  const controlDom = document.getElementById("control");
  const buttons = controlDom.querySelector("button");
  const buttonsHeight = buttons.clientHeight * 0.3;
  root.style.setProperty("--font-size-button", buttonsHeight + 'px');
  const screenDom = document.getElementById("screen");
  const screenHeight = screenDom.clientHeight;
  const expressionHeight = screenHeight * 0.2;
  root.style.setProperty("--font-size-expression", expressionHeight + 'px');
  const resHeight = screenHeight * 0.4;
  root.style.setProperty("--font-size-res", resHeight + 'px');
}
window.addEventListener("resize", updateFontSize);

更新文字大小操作,通过对CSS变量赋值实现,在需要使用响应式字体的地方,将font-size取值为CSS变量。
image.png

表达式溢出滚动

用户可能输入高精度表达式,溢出窗格时,需允许横向滚动。浏览器默认的横向滚动是shift + wheel
通过CSSJS实现鼠标滚轮控制表达式位置。

onMounted(() => {
  updateFontSize();
  const expressions = document.querySelectorAll('.expression, .res');
  expressions.forEach(expression => {
    expression.addEventListener("wheel", (event: WheelEvent) => {
      if (event.deltaY) {
        event.preventDefault();
        expression.scrollLeft += Number(event.deltaY);
      }
    })
  })
})

改操作通过对表达式DOM添加监听器实现,需要在Vue生命周期在Mounted时执行,通过组合式的onMounted方法,添加回调函数。

CSS细节处理

在实现业务逻辑之外,还为按钮、自定义组件添加了动画效果,在hoveractive等状态下均有不同的显示效果,保证用户视觉体验。
image.png

Spring端详细设计

CalculateController

用于响应发送到/calculate下的GETPOST请求。根据请求内容,调用不同的Service对象:

  • QueryService:负责查询本地日志文件或数据库,通过@Autowired注解自动注入
  • CalculateService:负责表达式求值,由静态的工具方法实现。

对HTTP请求的参数进行封装:

  • @RequestBody用于映射请求体到对象CalculateRequestPojo
  • @ModelAttribute用于映射Query参数到QueryPojo中。@RequestParam会将Query参数逐个映射到同名的函数入参中,不能直接映射到QueryPojo中。
@RestController
@RequestMapping("/calculate")
public class CalculateController {
    @Autowired
    QueryService queryService;

    @LogAnnotation
    @PostMapping
    public ResultPojo<Double> calculate(@RequestBody CalculateRequestPojo calculateRequestPojo) {
        double res = CalculateService.calculate(calculateRequestPojo.getExpression());
        return ResultPojo.success(res);
    }

    @GetMapping
    public ResultPojo<List<CalculateLogPojo>> query(@ModelAttribute QueryPojo queryPojo) {
        if (queryPojo == null) return ResultPojo.success();
        return queryService.query(queryPojo);
    }
}

表达式求值

计算服务工具类

该工具类内实现了表达式求值的具体算法。
对外暴露calculate方法供调用,其他private私有变量和方法用于封装可重用的计算过程,简化代码设计,由类内成员函数调用。
synchronized关键字保证多线程环境下线程间数据的隔离。
使用Java自带的BigDecimal高精度类,简化高精度计算任务的代码设计。

public class CalculateService {
    private static final Map<Character, Integer> priority = new HashMap<>();
    private static StringBuilder stringBuilder = new StringBuilder();
    private static Stack<BigDecimal> num = new Stack<>();
    private static Stack<Character> op = new Stack<>();

    static {
        priority.put('(', 0);
        priority.put(')', 0);
        priority.put('+', 1);
        priority.put('*', 2);
        priority.put('/', 2);
        priority.put('-', 3);
    }

    private static void evaluate() {
        BigDecimal a = new BigDecimal(0);
        BigDecimal b = new BigDecimal(0);
        BigDecimal x = new BigDecimal(0);
        if (!num.empty()) a = num.pop();
        char c = op.pop();
        if (c == '-') {
            x = b.subtract(a);
        } else {
            if (!num.empty()) b = num.pop();
            if (c == '+') x = b.add(a);
            else if (c == '*') x = b.multiply(a);
            else if (c == '/') x = b.divide(a, 6, RoundingMode.HALF_UP);
        }
        num.push(x);
    }

    private static void flashStringBuilder() {
        if (stringBuilder == null || stringBuilder.isEmpty()) return;
        BigDecimal currentNum = BigDecimalParser.parse(stringBuilder.toString());
        num.push(currentNum);
        stringBuilder.setLength(0);
    }

    public static synchronized double calculate(String expression) {
        num.clear();
        op.clear();
        for (Character c : expression.toCharArray()) {
            if (Character.isDigit(c) || c == '.') {
                stringBuilder.append(c);
            } else {
                flashStringBuilder();
                if (c == '(') {
                    op.push(c);
                } else if (c == ')') {
                    while (op.peek() != '(') evaluate();
                    op.pop();
                } else {
                    while (!op.isEmpty() && priority.get(op.peek()) >= priority.get(c)) evaluate();
                    op.push(c);
                }
            }
        }
        if (!stringBuilder.isEmpty()) flashStringBuilder();
        while (!op.isEmpty()) evaluate();
        return num.pop().doubleValue();
    }
}

并发测试

由于精力有限,只设计了14条测试样例,涵盖:整数、浮点数、高精度、正负数、有括号等情况下的四则运算。
image.png

测试结果

image.png

本地日志读写

image.png

日志写入本地文件

写入本地日志操作,通过获取当前日期,创建并按时间命名日志文件,如果已经存在同日期的日志文件,那么在该文件后追加新的日志。
JSON格式写入文件,方便读出、方便在程序外通过文件阅读工具阅读。

@Override
public ResultPojo<String> save(Object object) {
    String date = calendar.get(Calendar.YEAR) + String.format("%02d%02d", calendar.get(Calendar.MONTH) + 1, calendar.get(Calendar.DAY_OF_MONTH));
    File file = new File("./calculator/" + date + ".log");
    try {
        if (!file.getParentFile().exists()) file.getParentFile().mkdirs();
        if (!file.exists()) file.createNewFile();
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
    try (FileOutputStream fileOutputStream = new FileOutputStream(file, true);) {
        String json = gson.toJson(object);
        fileOutputStream.write((json + "\n").getBytes());
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
    return ResultPojo.success();
}

image.png

从本地文件读出日志

日志按时间分文件存储,如果知道了具体的时间,那么可以通过读取以该日期格式化后命名的日志文件,获取当天的日志信息。
由于日志的追加方式是在文末,因此最新的日志记录是在文件末尾而非开头。
在接口定义上,允许求具体某一天的日志。
如果不指定日期,那么将合法的日期文件,按离当前时间的远近排序,递归地查询离当前时间较近的日志信息。
在接口文档中定义size参数,为查询的日志数。

  • 如果当前日志文件的条数不足size,那么就递归查询离改日志文件较近的日期的日志文件。直到累计的日志条数达到size,或者无日志文件可读。
  • 如果当前日志文件的条数超过size,那么可以通过滑动窗口的方式读取,滑动窗口的宽度为要查询的记录数。通过滑动窗口可以避免在内存受限条件下,直接读取大文件导致内存不足的潜在问题。
private ResultPojo<List<CalculateLogPojo>> queryFileByDate(QueryPojo queryPojo) {
    String filePath = "./calculator/" + queryPojo.getDate() + ".log";
    List<CalculateLogPojo> list = new LinkedList<>();
    File file = new File(filePath);
    if (!file.isFile()) return ResultPojo.success(list);
    int page = queryPojo.getPage() == null ? 0 : queryPojo.getPage();
    int size = queryPojo.getSize() == null || queryPojo.getSize() == 0 ? 10 : queryPojo.getSize();
    try (BufferedReader bufferedReader = new BufferedReader(new FileReader(filePath))) {
        String line = "";
        int sizeLimit = (page + 1) * size;
        for (int i = 0; (line = bufferedReader.readLine()) != null; i++) {
            CalculateLogPojo calculateLogPojo = gson.fromJson(line, CalculateLogPojo.class);
            list.add(calculateLogPojo);
            if (list.size() > sizeLimit) list.removeFirst();
        }
        list = list.subList(0, max(list.size() - page * size, 0));
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
    return ResultPojo.success(list.reversed());
}

@Override
public ResultPojo<List<CalculateLogPojo>> query(QueryPojo queryPojo) {
    if (queryPojo.getDate() != null) {
        return queryFileByDate(queryPojo);
    }
    File file = new File("./calculator/");
    if (!file.isDirectory()) return ResultPojo.success();
    List<Integer> logFilesName = new LinkedList<>();
    for (File logFile : Objects.requireNonNull(file.listFiles())) {
        if (Pattern.matches(regex, logFile.getName()) && logFile.isFile()) {
            String stringName = logFile.getName();
            stringName = stringName.substring(0, stringName.lastIndexOf("."));
            Integer name = Integer.parseInt(stringName);
            logFilesName.add(name);
        }
    }
    logFilesName.sort(Comparator.reverseOrder());
    List<CalculateLogPojo> list = new LinkedList<>();
    for (Integer i : logFilesName) {
        queryPojo.setDate(i);
        ResultPojo<List<CalculateLogPojo>> resultPojo = queryFileByDate(queryPojo);
        int need = queryPojo.getSize() - resultPojo.getData().size();
        list.addAll(resultPojo.getData());
        if (need > 0) {
            queryPojo.setSize(need);
        } else {
            break;
        }
    }
    return ResultPojo.success(list);
}

数据库日志读写

https://baomidou.com/getting-started/

数据库日志读写通过MyBatis-Plus实现,通过按照文档要求,定义LogMapperLogService实现插件预定义的CRUD增删查改操作。

日志写入数据库

LogService中,重载了save方法,在记录日志到数据库的同时记录到本地文件。
image.png

从数据库中读出日志

分页查询操作需要按MyBatis-Plus文档要求配置插件:https://baomidou.com/plugins/pagination/
查询操作的配置信息,在QueryService中实现:

@Service
public class QueryService {
    @Autowired
    LogMapper logMapper;
    @Autowired
    LocalLogMapper localLogMapper;

    public ResultPojo<List<CalculateLogPojo>> query(QueryPojo query) {
        String from = query.getFrom();
        if (from == null || from.equals("sql")) {
            QueryWrapper<CalculateLogPojo> queryWrapper = new QueryWrapper<>();
            queryWrapper.orderByDesc("date");
            if (query.getDate() != null) queryWrapper.eq("date", query.getDate());
            int queryPage = query.getPage() == null ? 0 : query.getPage();
            int querySize = query.getSize() == null ? 10 : query.getSize();
            Page<CalculateLogPojo> page = new Page<>(queryPage, querySize);
            logMapper.selectPage(page, queryWrapper);
            List<CalculateLogPojo> list = page.getRecords();
            return ResultPojo.success(list);
        }
        return localLogMapper.query(query);
    }
}

默认页数、页面大小可以写入配置文件,由于精力有限,在本项目中暂未实现。

AOP实现日志记录

创建LogAnnotation注解,将其添加到需要记录日志的地方。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface LogAnnotation {
}

整个LogAop类是一个切面。它被@Aspect注解标注,表示这是一个切面类。
定义切入点表达式@Around("@annotation(top.wushf.calculator.annotation.LogAnnotation)"),任何被@LogAnnotation注解标注的方法都会触发recordLog方法的执行。
recordLog是一个环绕通知Around advice,当匹配的切入点方法被调用时,这个方法会在目标方法执行前后运行,执行日志记录逻辑。

@Aspect
@Component
public class LogAop {
    @Autowired
    private LogService logService;

    @Around("@annotation(top.wushf.calculator.annotation.LogAnnotation)")
    public Object recordLog(ProceedingJoinPoint joinPoint) throws Throwable {
        Object[] args = joinPoint.getArgs();
        long beginTime = System.currentTimeMillis();
        ResultPojo resultPojo = (ResultPojo) joinPoint.proceed();
        long endTime = System.currentTimeMillis();
        long duration = endTime - beginTime;
        CalculateRequestPojo calculateRequestPojo = (CalculateRequestPojo) args[0];
        String expresstion = calculateRequestPojo.getExpression();
        double res = (double) resultPojo.getData();
        CalculateLogPojo calculateLogPojo = new CalculateLogPojo(null, new Timestamp(System.currentTimeMillis()), expresstion, res, duration);
        logService.save(calculateLogPojo);
        return resultPojo;
    }
}

在目标方法执行前后获取系统时间,做差求出计算用时。
通过joinPoint.getArgs()获得传入的参数,该方法的参数为DTO对象CalculateRequestPojo
通过接收joinPoint.proceed()的返回指得到运算结果,运算结果封装在全局的返回结果类ResultPojo<T>中。
执行LogServicesave方法,将信息写入日志。

跨域访问

https://developer.mozilla.org/zh-CN/docs/Web/HTTP/CORS

在本地Debug的过程中,发现在Apifox中可以正常响应请求,在Vue的浏览器环境下就报错。原因是出现了跨域访问问题。
由于源Origin的端口是Vue3默认的5137,请求的端口的spring默认的8080,端口不一致,浏览器会发送CORS预检请求。预检请求会向该URL发送OPTIONS请求,响应中会携带有关跨域访问的信息。
如果要允许跨域访问,可以尝试在Controller上添加@CrossOrigin注解。但亲测,只在GET方法上成功生效,POST仍然访问失败。
本项目使用的解决方案是,通过配置WebMvcConfigurer,实现跨域资源访问。

package top.wushf.calculator.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class CorsConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/calculate").allowedOriginPatterns("*").allowCredentials(true).allowedHeaders("*").allowedMethods("*").maxAge(3600);
    }
}

DevOps开发实践

持续交付CD

本项目的前后端均托管在Gitee仓库,通过部署流水线,实现持续集成、持续部署。
image.png

配置文件分离

由于Spring配置文件会指定数据库用户名密码等敏感信息,在本项目中选择将其排除在版本控制之外。
image.png
在部署脚本中,通过主函数参数指定外部配置文件。

PID=$(ps -ef | grep calculator-0.0.1-SNAPSHOT.jar | grep -v grep | awk '{print $2}')

# 如果找到进程,则杀死该进程
if [ -n "$PID" ]; then
  echo "找到进程 ID: $PID,正在杀死该进程..."
  kill -9 $PID
  echo "进程已被杀死。"
else
  echo "没有找到正在运行的calculator-0.0.1-SNAPSHOT.jar的进程。"
fi

# 重新启动程序
echo "正在重新启动calculator-0.0.1-SNAPSHOT.jar..."
nohup java -jar /home/admin/calculator-server/target/calculator-0.0.1-SNAPSHOT.jar --spring.config.location=/root/calculator-server.yml > /home/admin/calculator-server/calculator.log 2>&1 &
echo "程序已重新启动。"

软件设计模式在本项目中的应用

Command命令

尝试以对象来代表实际行动。命令对象可以把行动action及其参数封装起来

input = (cmd: string) =>{}

根据输入的字符,执行不同的动作,而不必为每一种输入定义单独的函数。
image.png

RESTful Api

REST中使用HTTP动词来表示对资源的操作,这与命令模式相似,通过不同的命令执行不同的操作。

REST表现层状态转换,该设计风格体现在本项目Api文档的定义中。
本项目围绕计算需求,对于URL/calculate

  • 求值请求通过POST方法推送表达式
  • 日志请求通过GET方法获取,并通过设置Query参数实现自定义查询

DAO数据访问对象

将数据访问逻辑从业务逻辑中分离出来,并将数据访问操作封装在一个专用的类中

为数据库表、HTTP请求等封装数据访问对象:
image.png

Proxy代理

代理者可以作任何东西的接口:网络连接、存储器中的大对象、文件或其它昂贵或无法复制的资源。

Spring AOP中,通过代理模式创建目标对象的代理对象。这个代理对象控制对目标对象的访问,允许在方法调用之前和之后插入额外的行为。
image.png

Strategy策略

将每个算法封装起来,使它们可以互换使用

Spring端持久层有本地文件实现和数据库实现。在读日志时,对于“有指定日期”和“无指定日期”、“从数据库”和“从本地文件”,分别封装具体的方法,在QueryService中,有策略类负责调度,根据QueryPojo,执行不同的策略。
image.png
由于本项目较为简单,并没有为每个算法定义单独的类,而是封装在具有相近语义的类下。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1835149.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

scratch3编程02-使用克隆来编写小游戏

目录 1&#xff0c;游戏效果 2&#xff0c;游戏代码块 1&#xff09;玩家 2&#xff09;障碍物 ​ 3&#xff09;箭头 ​ 4&#xff09;关卡图片 3&#xff0c;scratch文件 1&#xff0c;游戏效果 使用克隆 在这个游戏中&#xff1a; 程序开始&#xff1a;只要点击“…

和服务器建立联系——6.10山大软院项目实训1

下面介绍我如何在自己的项目中&#xff0c;根据aigc组的接口&#xff08;如下图&#xff09;&#xff0c;在Unity中和服务器建立联系并发出接受请求的&#xff1a; 这是一个通过HTTP POST方法调用的接口&#xff0c;需要发送JSON格式的数据。在Unity中实现这样的功能&#xff0…

[AI Google] TimesFM:AI预测股市价格,能否助我财务自由?

今天我偶然发现了一个名为TimesFM的模型&#xff0c;它能够预测时间序列数据。于是我心中冒出了一个大胆的想法&#xff1a;如果这个模型可以预测股票价格&#xff0c;那么我是否能借此成为股神呢&#xff1f; 介绍 TimesFM&#xff08;时间序列基础模型&#xff09;是由谷歌…

基于Spring Boot+VUE旅游管理系统

1管理员功能模块 管理员通过点击后台管理&#xff0c;进入页面可以输入用户名、密码、角色进行登录相对应操作&#xff0c;如图1所示。 图1管理员登录界面 管理员通过点击后台管理&#xff0c;进入页面可以填写首页、个人中心、用户管理、旅游方案管理、旅游购买管理、系统管…

华为---RIP路由协议的汇总

8.3 RIP路由协议的汇总 8.3.1 原理概述 当网络中路由器的路由条目非常多时&#xff0c;可以通过路由汇总(又称路由汇聚或路由聚合)来减少路由条目数&#xff0c;加快路由收敛时间和增强网络稳定性。路由汇总的原理是&#xff0c;同一个自然网段内的不同子网的路由在向外(其他…

【接口自动化测试】第一节.接口自动化测试基础和框架介绍

文章目录 前言一、接口自动化基础 1.1 接口自动化基础介绍 1.2 接口自动化测试流程 1.3 选取自动化测试用例 1.4 搭建自动化测试环境二、接口自动化测试框架 2.1 接口自动化框架设计思路 2.2 定义项目目录结构总结 前言 一、接口自动化基础 1.1…

企业中没有有效的PMC管理会是什么样子?

众所周知&#xff0c;有效的产品物料控制&#xff08;PMC&#xff09;管理对于企业的稳健发展至关重要。然而&#xff0c;如果企业忽视了PMC的重要性&#xff0c;或者其管理不到位&#xff0c;那么企业将面临一系列严重的问题&#xff0c;从生产线混乱到效益滑坡&#xff0c;甚…

16. 一个I/O项目:构建命令行程序(上)

标题 一、功能二 、接受命令行参数三、 读取文件四、重构改进模块性和错误处理4.1 二进制项目的关注分离4.2 提取参数解析器4.3 创建一个Config的构造函数4.4 传参错误处理4.5 从main中提取逻辑4.6 将代码拆分进crate 一、功能 grep 最简单的使用场景是在特定文件中搜索指定字…

大模型应用实战2——大模型知识体系、GLM4的function calling功能及agents开发

前面通过glm4作为引子&#xff0c;现在来谈大模型知识体系结构是怎么样的 两个研究方向&#xff1a;开发特定的agents/强化大模型在某个领域的能力&#xff0c;后一个的大致方法如下图 GLM4的function calling功能&#xff1a; 需要解决的问题&#xff1a;不能解决大数运算&a…

香港优才计划是坑人的吗?申请的人都在说……

大家好啊&#xff01;最近看到超多小伙伴私信我说申请香港优才被“坑”了&#xff0c;被割了一波韭菜&#x1f33f;心疼你们&#xff01;作为一个通过这个计划顺利移民香港的老司机&#x1f697;&#xff0c;我必须出来吐槽一下&#xff0c;并且把我的成功经验分享给大家&#…

天锐绿盾 | -公司电脑文件防泄密软件

天锐绿盾是一款专为企业设计的电脑文件防泄密系统&#xff0c;它结合了多种安全功能&#xff0c;旨在从源头上保障企业数据的安全。 www.drhchina.com 以下是关于天锐绿盾的详细介绍&#xff1a; 一、产品概述 天锐绿盾&#xff0c;又名绿盾信息安全管理软件&#xff0c;是一…

【达梦数据库】typeorm+node.js+达梦数据库返回自增列值

1.配置环境&#xff0c;下载依赖包 typeorm init --name test22 --database mysql typeorm-dm&#xff0c;uuid,typeorm2,修改连接信息 修改src/ data-source.ts 文件 连接dm&#xff0c;可参考刚刚安装typeorm-dm 模块中的 README.md 3.修改自增信息 /* 修改前*/PrimaryGen…

FileZilla证书过期,导致FileZilla Client联不上,或者老断开的处理

1、先到服务器上去重新生成一下证书&#xff0c;并且覆盖掉老的证书。edit--settings 输入信息&#xff0c;并且确认生成新的证书&#xff1a; 2、Client连接的时候&#xff0c;弹出证书信任&#xff0c;点击确认。 这样第一次连接&#xff0c;然后访问目录全都是好的&#xff…

解决此平台不支持虚拟化的 Intel VT-x/EPT故障问题

一、问题描述 当我们在VMware Workstation虚拟机上【启用】虚拟系统(如:Windows10)所在的虚【拟机设置】【处理器】【虚拟化引擎】下面的【虚拟化 Intel VT-x/EPT 或AMD-V/RVI(V)】内容后,在启动虚拟系统时,虚拟系统无法启动,且弹出【此平台不支持虚拟化的 Intel VT-x/EP…

算法课程笔记——单调栈单调队列

算法课程笔记——单调栈&单调队列

多协议接入/GB28181/GAT1400协议/安防综合管理系统EasyCVR报错version`GLIBCXX_3.4.19‘not found如何处理?

多协议接入/GB28181/GAT1400协议/安防综合管理系统EasyCVR视频汇聚平台能在复杂的网络环境中&#xff0c;将前端设备统一集中接入与汇聚管理。智慧安防/视频存储/视频监控/视频汇聚EasyCVR平台可以提供实时远程视频监控、视频录像、录像回放与存储、告警、语音对讲、云台控制、…

【自动驾驶】串口通信控制与反馈

文章目录 串口通信控制与反馈通讯协议上行数据帧解析下行数据帧解析串口通信控制与反馈 通讯协议 上行数据指的是机器人底盘向上位机发送的状态数据, 下行数据指的是上位机向机器人底盘发送的控制信息。 上行数据帧解析 机器人运动底盘通过串口发送的数据包格式,如下表所…

练习时长 1 年 2 个月的 Java 菜鸡练习生最近面经,期望25K

面经哥只做互联网社招面试经历分享&#xff0c;关注我&#xff0c;每日推送精选面经&#xff0c;面试前&#xff0c;先找面经哥 自我介绍&#xff1a;本人是练习时长 1 年 2 个月的 Java 后端菜鸡练习生。下面是我最近面试的面经&#xff1a; 百度 一面 约1h时间&#xff1a;2…

MJ绘画设计基础——如何玩转midjourney?

抽卡的时候经常有一个问题&#xff0c;就是整张图都还不错&#xff0c;但是某些地方有些小问题&#xff0c;比如说手很奇怪&#xff0c;比如下面这个图&#xff0c;哪都挺好看&#xff0c;就是左手有点问题。 这时候就可以局部重绘来拯救一下 第一次生成的图 点击图片下方的V…

黄仁勋加州理工毕业典礼演讲:人工智能是我们这个时代最重要的技术

英伟达公司首席执行官黄仁勋周五&#xff08;6月14日&#xff09;在加州理工学院&#xff08;Caltech&#xff09;毕业典礼上发表演讲&#xff0c;鼓励毕业生在逆境中努力&#xff0c;不断寻求新的机遇。 黄说&#xff0c;加州理工学院因其毕业生受人尊敬而闻名&#xff0c;如…