【项目总结】工程构建思路分析与分享
背景
写这篇文章的初衷是因为国产化项目临近结束,在做项目总结时,回想起了代码管理的相关问题。虽然工程搭建及管理是一个高级工程师必备的能力,但似乎每次一个新项目立项,都是从老的项目中拷贝工程框架,再进行定制修改。并没有认真了解其原理及整体工程管理和维护思想。
虽然大部分的软件工程,都具备一定的通用性,修修改改也能用起来,但是我觉得工程架构,应该要和软件架构相匹配,将软件架构思想贯彻到底。本文介绍我个人如何通过软件架构来构建工程架构的以及cmake
常用的接口。
如何构思工程框架
以T-BOX项目为例,其软件架构如下:
分析:
该软件架构采用分层结构,从上到下分为表现层、通讯层、业务层、抽象层、基础层。其中我们主要关心业务层、抽象层、基础层;
- 业务层。用于实现t-box自身业务,比如gnss定位、数据拨号、TSP平台、自升级、片间通信等。它内部又分为基础能力和扩展能力两类业务。
- 抽象层。该层的目的是进程底层硬件的依赖剥离,方便进行平台移植。
- 基础层。主要是一些基础的功能库。它内部又分为三类:
- 模组商SDK。比如数据拨号、gnss、低功耗等接口
- 开源库。数据库、日志库、MQTT等
- 系统库等
理论上业务层只能调用抽象层接口,而抽象层只能调用基础层接口。
问题一:开源库如何管理?集成库还是集成生成物?
这是大家可能常会考虑的问题,我一般倾向于集成开源库的生成物,原因有以下几点:
- 空间浪费。开源库应该是具有通用性的,也就是说不同的项目都可以使用,若集成源代码,则多个不同项目,则会在git仓库中有多份。
- 编译时间增长。比如
openssl
开源库的编译需要十几分钟。这就导致第一次编译时,时间非常长。 - 便于维护。有时候,开源库也存在一些bug,需要我们去修复,若是单独用仓库保存仅需要维护一处即可。再通知各个项目组,让他们主动更新。
当然,不同的角色,其考虑角度不一样,比如模组商应该更偏向于将开源库放在项目中。因为他们一般是需要将源码交付给客户的。
问题二: 如何剥离对外部或硬件依赖?
在之前的工作中,我们经常会面临的一个问题:软件已经具备联调条件,但是外部却迟迟不能配合,无法验证内部逻辑。比如模组的sdk或硬件未提供。于是我们的思路是:在x86环境下,可以进行部分业务自测。对于外部依赖的sdk,可以自己模拟接口,配置相关应答。
问题三:如何将我们的程序集成到模组中?
我们工程编译的最终产物一般是可执行程序、动态库、配置文件。如何让其在模组中运行就需要解决两点:
- 如何让我们的程序开机启动。解决方式:需要修改启动脚本,再生成文件系统。
- 如何将我们的程序集成到模组中。解决方式:模组商一般会提供一个单独的分区,需要将我们的程序生成单独的ubi文件,进行烧录。
大致流程:
- 将我们的生成物,按照模组商的提供的指导文档,生成对应的
oem_data.ubi
。 - 将修改的文件,替换到模组上的文件系统中,再生成文件系统
root.squashfs
。 - 将
oem_data.ubi
、root.squashfs
替换到整包中,进行烧录。
问题四: 模组商的SDK如何管理?
正如上面讨论的:foundation->oem->arm
目录、模组商的rootfs
、升级整包、交叉工具链等,都应该是模组上提供的SDK的部分生成物。如何管理模组商的SDK呢?我的建议是单独上库处理。原因有以下几点:
- 模组商的SDK太大,一般有1G多。且编译时间很长,首次编译一般需要几个小时。
- 后续迭代,方便跟踪,溯源。
现在,我们的项目的工程目录大致如下:
且有三个git
仓库:
如何使用cmake实现各个仓库框架
因为不同仓库源码来源不同,管理方式不同,因此cmake
也存在一些差异。
模组商SDK仓库
如上所述,该仓库的主要作用是记录后续模组商SDK变更记录,方便后续追溯。一般该SDK有自己的一套管理方式,不建议修改。我一般的做法是完整上库,不做修改它工程结构。
开源库仓库
该仓库的作用是将一些依赖的开源库进行编译,供上层使用。因此就可能存在多个开源依赖库。为了方便管理,我将所有的开源库,放到同一目录下。目录结构大致如下:
#build.sh
PROJECT_NAME=foundation
TOPDIR=$(pwd)
SRC_CODE_DIR=$TOPDIR#表示编译的平台,因为项目仓库需要支持arm环境和x86环境,因此底层依赖库需要提供两个版本。
export TYPES_ARM="arm"
export TYPES_X86="x86"
TYPES=($TYPES_ARM $TYPES_X86)# 默认编译arm平台
if [ -z "$1" ]
thenexport PROJECT_BUILD_TYPE=${TYPES_ARM}
elseexport PROJECT_BUILD_TYPE=$1
fi
echo "PROJECT_BUILD_TYPE:${PROJECT_BUILD_TYPE}"# 判断交叉编译链是否存在
if [[ "$1" == $TYPES_ARM ]]; thenif [ ! -d $SRC_CODE_DIR/toolchain ]; thenecho "目录 $SRC_CODE_DIR/toolchain 不存在"exitfi# source 环境变量source t108_compile_tool_env_init
fi
# 注:交叉编译链及环境变量,一般都是模组上提供的。编译时,我们需要放到指定目录下# 创建编译目录
rm -rf $SRC_CODE_DIR/build
mkdir $SRC_CODE_DIR/build
cd $SRC_CODE_DIR/build# 构建项目编译环境,并将编译平台传入
cmake -DENV_PLATFORM=${PROJECT_BUILD_TYPE} ..# 执行多线程编译
make -j8#若编译失败,则退出。若成功,则执行后续操作
if [ "$?" == "0" ]; thenecho $?echo "build foundation successful"
elseecho $?echo "build foundation error" exit 1
fi# 执行安装指令
make install#打包
cd ${TOPDIR}
rm -rf out
mkdir -p out
mkdir -p out/include
mkdir -p out/bin
mkdir -p out/etc
mkdir -p out/lib
cp -rf $SRC_CODE_DIR/build/deploy/include/* out/include/
cp -rf $SRC_CODE_DIR/build/deploy/sbin/mosquitto out/bin/
cp -rf $SRC_CODE_DIR/build/deploy/etc/* out/etc/
cp -rf $SRC_CODE_DIR/build/deploy/lib/*.a out/lib/
顶层CMakeList.txt
分析:
cmake_minimum_required(VERSION 3.10.0)
project(foundation C CXX)# 暂时屏蔽编译警告
# 对于开源库的编译警告,我的原则是不处理。而我们自己编写的代码是不可以有编译警告的。为了编译美观,因此忽略。
add_compile_options( -Wno-conversion -Wno-sign-conversion -Wno-parentheses -Wno-unused-variable -Wno-format -Wno-implicit-function-declaration -Wno-incompatible-pointer-types -Wno-int-conversion)# 设置安装指令的相关变量,便于统一管理
# CMAKE_INSTALL_PREFIX。指定了安装的目标路径前缀,即所有安装的目标文件都会被放置在这个路径下
# CMAKE_INSTALL_BINDIR。指定了可执行文件(二进制文件)的安装路径。若没有设置,默认的安装路径将是 ${CMAKE_INSTALL_PREFIX}/bin。
# CMAKE_INSTALL_LIBDIR。指定了库文件的安装路径,若没有设置,默认的安装路径通常是 ${CMAKE_INSTALL_PREFIX}/lib,但在64位系统上,可能会是 ${CMAKE_INSTALL_PREFIX}/lib64
#CMAKE_INSTALL_INCLUDEDIR。CMAKE_INSTALL_INCLUDEDIR,若没有设置,默认的安装路径是 ${CMAKE_INSTALL_PREFIX}/include。
set(CMAKE_INSTALL_PREFIX ${CMAKE_SOURCE_DIR}/build/deploy)
set(CMAKE_INSTALL_BINDIR ${CMAKE_INSTALL_PREFIX}/bin)
set(CMAKE_INSTALL_LIBDIR ${CMAKE_INSTALL_PREFIX}/lib)
set(CMAKE_INSTALL_INCLUDEDIR ${CMAKE_INSTALL_PREFIX}/include)# 设置foundation 版本信息
# 获取编译日志
EXECUTE_PROCESS(COMMAND date +%Y%m%d OUTPUT_VARIABLE COMPILE_DATE OUTPUT_STRIP_TRAILING_WHITESPACE)# 获取当前最新的commitID
EXECUTE_PROCESS(COMMAND git rev-parse --short HEAD OUTPUT_VARIABLE GIT_COMMITID OUTPUT_STRIP_TRAILING_WHITESPACE)# 定义版本名程: foundation_platform_data_commitID,并将该信息以ADM_FOUNDATION_SDK_VERSION宏传递给编译选项,这样源文件中就可以进行访问,并进行打印记录。
SET(PROJECT_VERSION ${PROJECT_NAME}_${ENV_PLATFORM}_${ADM_SDK_VERSION}_${COMPILE_DATE}_${GIT_COMMITID})
ADD_DEFINITIONS(-DADM_FOUNDATION_SDK_VERSION="${PROJECT_VERSION}" -D${PROJECT_VERSION})# 因为开源库mosquitto依赖openssl库,因此首先需要确保我们的编译环境中能够找到对应依赖。相当于环境编译检查。
find_package(OpenSSL REQUIRED)
if(OPENSSL_FOUND)include_directories(${OPENSSL_INCLUDE_DIR})
endif()# 将需要编译的开源库添加到构建过程中来
add_subdirectory(package/iniparser)
add_subdirectory(package/mosquitto)
add_subdirectory(package/log)
add_subdirectory(package/sqlite3)
add_subdirectory(package/cJSON)
在编译过程中,遇到了一个现象:编译x86环境时,可以正常编译。但是编译arm平台会报如下错误:
CMake Error at /home/xieyihua/.local/lib/python3.8/site-packages/cmake/data/share/cmake-3.29/Modules/FindPackageHandleStandardArgs.cmake:230 (message):Could NOT find OpenSSL, try to set the path to OpenSSL root folder in thesystem variable OPENSSL_ROOT_DIR (missing: OPENSSL_CRYPTO_LIBRARY) (foundversion "1.1.1f")
Call Stack (most recent call first):/home/xieyihua/.local/lib/python3.8/site-packages/cmake/data/share/cmake-3.29/Modules/FindPackageHandleStandardArgs.cmake:600 (_FPHSA_FAILURE_MESSAGE)/home/xieyihua/.local/lib/python3.8/site-packages/cmake/data/share/cmake-3.29/Modules/FindOpenSSL.cmake:686 (find_package_handle_standard_args)CMakeLists.txt:43 (find_package)
分析:其原因就是在做环境检查时,发现工程依赖openSSL
,但是却没有找到相关依赖。因为我的x86环境默认安装了openSSL
,且系统$PATH环境变量中添加了该路径,所以能找到相关依赖。但是可以提供的环境变量并没有相关设置,所以找不到。
# t108_compile_tool_env_init
export SDKPATH=$(cd $(dirname ${BASH_SOURCE[0]}) && pwd)
BUILD_TOOLCHAIN_DIR=$SDKPATH/toolchain
export CC=$BUILD_TOOLCHAIN_DIR/linux64/bin/arm-openwrt-linux-gcc
export CXX=$BUILD_TOOLCHAIN_DIR/linux64/bin/arm-openwrt-linux-g++
因此,需要在环境变量中,手动添加工具链中openSSL
的相关路径。export PATH=$BUILD_TOOLCHAIN_DIR/linux64/bin:$BUILD_TOOLCHAIN_DIR/linux64/usr/lib:$PATH
。
项目仓库
由上图可知,项目本身CMakeLists.txt
布局如下,并逐一分析:
build.sh
内容如下:
PROJECT_NAME=itu2.0
TOPDIR=$(pwd)# 若没有设置平台,则默认编译arm
#!/bin/bash
if [ -z "$1" ]; thenplatform=arm
elseplatform=$1
fiif [[ "$platform" == "arm" ]]; thenif [ ! -d $TOPDIR/toolchain ]; thenecho "目录 $TOPDIR/toolchain 不存在"exitfisource t108_compile_tool_env_init
fi# 创建目录和编译
rm -rf $TOPDIR/build
mkdir $TOPDIR/build# 编译,并传入平台
cd $TOPDIR/build
cmake -DENV_PLATFORM=$platform ..
make -j8if [ "$?" == "0" ]; thenecho $?echo "build $PROJECT_NAME successful"
elseecho $?echo "build $PROJECT_NAME error"exit 1
fi#回到顶层目录
cd ${TOPDIR}#将生成物拷贝到输出目录
OUT_DIR=$TOPDIR/out
EXTERNAL_OUTPUT_EXE=$OUT_DIR
EXTERNAL_OUTPUT_EXE_BIN=$OUT_DIR/bin
EXTERNAL_OUTPUT_EXE_ETC=$OUT_DIR/etc
EXTERNAL_OUTPUT_EXE_LIB=$OUT_DIR/librm -rf $EXTERNAL_OUTPUT_EXEmkdir -p $EXTERNAL_OUTPUT_EXE
mkdir -p $EXTERNAL_OUTPUT_EXE_BIN
mkdir -p $EXTERNAL_OUTPUT_EXE_ETC
mkdir -p $EXTERNAL_OUTPUT_EXE_LIBcp -rf $TOPDIR/build/common/libsaFoundation.so $EXTERNAL_OUTPUT_EXE_LIB
cp -rf $TOPDIR/build/abstract/libsaEngine.so $EXTERNAL_OUTPUT_EXE_LIBcp -rf $TOPDIR/build/application/appcore/basicApp/basicApp $EXTERNAL_OUTPUT_EXE_BIN
cp -rf $TOPDIR/build/application/appcore/extendApp/extendApp $EXTERNAL_OUTPUT_EXE_BIN
cp -rf $TOPDIR/application/appcore/daemon/shineAutoDaemon $EXTERNAL_OUTPUT_EXE_BIN
cp -rf $TOPDIR/common/3rd_libs/$platform/bin/mosquitto $EXTERNAL_OUTPUT_EXE_BINcp -rf $TOPDIR/rawdata/saTboxConfig.ini $EXTERNAL_OUTPUT_EXE_ETC
cp -rf $TOPDIR/common/3rd_libs/$platform/etc/mosquitto/mosquitto.conf.example $EXTERNAL_OUTPUT_EXE_ETC
CMakeLists.txt
内容如下:
cmake_minimum_required(VERSION 3.10.0)
project(ITU2.0 C CXX)# 设置编译参数
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -g -rdynamic -funwind-tables -ffunction-sections -std=gnu11")
#set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g -rdynamic -funwind-tables -ffunction-sections -std=c++14 ")
# 打开所有警告。组内要求,编译过程不可以有任何警告
add_compile_options(-Wall -Wextra)# 判断平台合法性
if((${ENV_PLATFORM} MATCHES "x86"))message(STATUS "ENV_PLATFORM = x86")
elseif(${ENV_PLATFORM} MATCHES "arm")message(STATUS "ENV_PLATFORM = arm")
else()message(FATAL_ERROR "ENV_PLATFORM ERROR x86 arm")
endif()find_package(OpenSSL REQUIRED)# 根据不同平台,设置库查找路径及依赖库
if(${ENV_PLATFORM} MATCHES "x86")# 设置x86平台 库查找路径set(FOUNDATION_LIBDIR ${CMAKE_SOURCE_DIR}/common/3rd_libs/${ENV_PLATFORM}/lib)# 添加依赖库set(FOUNDATION_LIBRARIESadmdebugcjsoneasyloggeriniparsermosquitto_staticsqlite3${FOUNDATION_LIBRARIES})
elseif(${ENV_PLATFORM} MATCHES "arm")# 设置arm平台 库查找路径set(FOUNDATION_LIBDIR ${CMAKE_SOURCE_DIR}/common/3rd_libs/${ENV_PLATFORM}/lib)# arm 平台需要添加模组商自身的库,以及跨平台的其它库list(APPEND FOUNDATION_LIBDIR ${CMAKE_SOURCE_DIR}/common/oem/libs-lyqr)list(APPEND FOUNDATION_LIBDIR ${CMAKE_SOURCE_DIR}/toolchain/linux64/usr/lib)# 配置编译链接路径set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wl,-rpath-link,${CMAKE_SOURCE_DIR}/toolchain/linux64/usr/lib")set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wl,-rpath-link,${CMAKE_SOURCE_DIR}/toolchain/linux64/lib")set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wl,-rpath-link,${CMAKE_SOURCE_DIR}/common/oem/libs-lyqr")# 添加arm 平台依赖库set(FOUNDATION_LIBRARIESadmdebugcjsoneasyloggeriniparsermosquitto_staticsqlite3#lyqrlynq_lib${FOUNDATION_LIBRARIES})
else()message(FATAL_ERROR "ENV_PLATFORM ERROR x86 arm")
endif()# 逐层编译
## 1. 编译基础库
add_subdirectory(foundation)## 2. 编译抽象层
add_subdirectory(abstract)## 3. 编译业务层代码
add_subdirectory(application)
foundation/CMakeLists.txt
内容如下。理论上foundation目录下都是已经配置文件、动态库、头文件。是不需要编译的。但是由于我们需要兼容x86环境,需要自定义模组商接口,用于调试。
cmake_minimum_required(VERSION 3.10.0)
project(lynq_lib C CXX)set(project_name lynq_lib)# x86环境下,依赖接口编译
if("${ENV_PLATFORM}" STREQUAL "x86" )file(GLOB engine_src"oem/x86/src/*")file(GLOB engine_inc"oem/x86/inc""oem/x86/src")
endif()add_library(${project_name} SHARED${engine_src}
)# 设置链接库路径,因为是底层foundation库,因此主要依赖交叉工具链
target_link_directories(${project_name} PRIVATE ${FOUNDATION_LIBDIR})
abstruct/CMakeLists.txt
内容如下:该层是对foundation的抽象,因此肯定会对foundation相关进行依赖。
cmake_minimum_required(VERSION 3.10.0)
project(engine C CXX)set(project_name saEngine)file(GLOB engine_src#rpc"rpc/src/*"#network"network/src/*"#gnss"gnss/src/*"#se"se/src/fdw/*"#lpm"lpm/src/*"#sms"sms/src/*"#voice_call"voice_call/src/*"#添加其它抽象层代码
)file(GLOB engine_inc"*/inc""*/src"
)message(STATUS "engine_inc = ${engine_inc}")
message(STATUS "engine_src = ${engine_src}")# 生成目标动态库 libsaEngine.so
add_library(${project_name} SHARED${engine_src})# 设置头文件查找路径。因为抽象层是对否foundation的抽象,理论上它应该只依赖foundation才对。
target_include_directories(${project_name}PRIVATE${engine_inc}${CMAKE_SOURCE_DIR}/foundation/3rd_libs/${ENV_PLATFORM}/include/${CMAKE_SOURCE_DIR}/foundation/libs/include/${CMAKE_SOURCE_DIR}/foundation/3rd_libs/${ENV_PLATFORM}/include/log/${CMAKE_SOURCE_DIR}/foundation/oem/inc/lynq/${CMAKE_SOURCE_DIR}/foundation/oem/inc/mbtk/
)# 添加依赖库libFoundation.so
target_link_libraries(${project_name}PRIVATEsaFoundation
)
bussniss/basic/CMakeLists.txt
内容如下:同理bussniss/extend/CMakeLists.txt
也类似。
cmake_minimum_required(VERSION 3.10.0)
project(basic C CXX)set(project_name basicApp)# 设置目标文件名称
add_executable(${project_name}main.csaInitBasicModule.c#其它服务源码
)#添加头文件查找路径。理论上若abstruct层做的足够优秀,业务层是不会直接调用foundation层接口的。
include_directories(${CMAKE_SOURCE_DIR}/abstract/inc/
)# 设置库依赖
target_link_libraries(${project_name}PRIVATEsaFoundationsaEngine)#设置库查找路径
target_link_directories(${project_name} PRIVATE ${FOUNDATION_LIBDIR})
编译成功之后,就是需要构建整包了。package.sh
内容如下:
#!/bin/bash
export PROJECT_ROOT=$(cd "$(dirname "$0")";pwd)# 创建输出目录
if [ ! -d $PROJECT_ROOT/package ];thenmkdir $PROJECT_ROOT/package
fi# 生成 oem_data.ubi。build.sh 脚本会将项目生成物保存到out目录下
mkdir $PROJECT_ROOT/tmp
$PROJECT_ROOT/tools/mkfs.ubifs -m 2048 -e 126976 -c 507 -F --space-fixup --compr=zlib -o $PROJECT_ROOT/tmp/oem_data.ubifs -d $PROJECT_ROOT/out/
cd $PROJECT_ROOT/tmp/
$PROJECT_ROOT/tools/ubinize -m 2048 -p 131072 -s 2048 -o $PROJECT_ROOT/package/openwrt-mmp-asr1806-oem_data.ubi $PROJECT_ROOT/tools/ubinize-oem.cfg
cd $PROJECT_ROOT/
rm -rf $PROJECT_ROOT/tmp# 生成 root.squashfs
mkdir $PROJECT_ROOT/tmp
cp -rf $PROJECT_ROOT/sdk-lynq/* $PROJECT_ROOT/tmp/
cp -rf $PROJECT_ROOT/sdk-shineauto/root-mmp/etc/init.d/mbtk_boot_last $PROJECT_ROOT/tmp/root-mmp/etc/init.d/mbtk_boot_last$PROJECT_ROOT/tools/mksquashfs4 $PROJECT_ROOT/tmp/root-mmp $PROJECT_ROOT/package/openwrt-mmp-asr1806-root.squashfs -nopad -noappend -root-owned -no-exports -comp xz -Xpreset 8 -Xe -Xlc 0 -Xlp 2 -Xpb 2 -Xbcj arm -b 64k -p '/dev d 755 0 0' -p '/dev/console c 600 0 0 5 1' -no-xattrs
rm -rf $PROJECT_ROOT/tmp# 生成整包
TIMESTAMP=$(date +'%Y%m%d')
GIT_COMMITID=$(git rev-parse --short HEAD)
echo $TIMESTAMP
echo $GIT_COMMITIDmkdir $PROJECT_ROOT/tmp
cp -rf $PROJECT_ROOT/wholePackage/* $PROJECT_ROOT/tmp
cp -rf $PROJECT_ROOT/package/openwrt-mmp-asr1806-oem_data.ubi $PROJECT_ROOT/tmp
cp -rf $PROJECT_ROOT/package/openwrt-mmp-asr1806-root.squashfs $PROJECT_ROOT/tmp# 构建fota包
$PROJECT_ROOT/tools/mkotafbf -f $PROJECT_ROOT/tmp/asr1806_p301_QSPINAND_Trusted_SDTIM_LPDDR2_OTA_BLF -o $PROJECT_ROOT/package/shineauto_$TIMESTAMP\_$GIT_COMMITID.bin -v "$TIMESTAMP\_$GIT_COMMITID"
$PROJECT_ROOT/tools/ota_update -f $PROJECT_ROOT/package/shineauto_$TIMESTAMP\_$GIT_COMMITID.bin -v $PROJECT_ROOT/tmp/
rm -rf $PROJECT_ROOT/tmp
总结
本文介绍了如何通过软件架构构建工程架构,并使用CMake进行项目管理。以T-BOX项目为例,详细分析了其软件架构,包括分层结构、业务层、抽象层和基础层。还探讨了如何管理开源库、剥离外部依赖、集成程序到模组中以及如何管理模组商的SDK。希望思路能够帮助到需要的朋友
若我的内容对您有所帮助,还请关注我的公众号。不定期分享干活,剖析案例,也可以一起讨论分享。
我的宗旨:
踩完您工作中的所有坑并分享给您,让你的工作无bug,人生尽是坦途
相关文章:

【项目总结】工程构建思路分析与分享
背景 写这篇文章的初衷是因为国产化项目临近结束,在做项目总结时,回想起了代码管理的相关问题。虽然工程搭建及管理是一个高级工程师必备的能力,但似乎每次一个新项目立项,都是从老的项目中拷贝工程框架,再进行定制修…...
3、AI测试辅助-测试计划编写(自动生成任务甘特图)
AI测试辅助-测试计划编写 一、创建测试计划1.1 使用ChatGpt生成测试计划1.1.1 测试计划细化-测试任务排期1.1.2 测试计划细化-利用plantuml输出甘特图(不推荐)1.1.3 测试计划细化-利用mermaid 生成甘特图(推荐) 一、创建测试计划 …...
MySQL:进阶巩固-触发器
目录 一、基本概述二、语法结构三、基本使用3.1 新增的触发器3.2 修改的触发器3.3 删除的触发器 一、基本概述 触发器是与表有关的数据对象,在INSERT/UPDATE/DELETE之前或者是之后,触发并执行触发器中定义的SQL语句的集合,触发器的这种特性可…...

python获取百度地图路径规划
自制语音助手想做一个查询交通路线的功能,于是就想到了百度地图。在百度地图的服务器端应用中,提供了“轻量级路线规划”API接口,正好可以满足需求。要用这个接口,需要注册百度地图账号并创建一个服务器端的应用。有关开通百度地图…...

Spring Boot 进阶- Spring Boot 自定义拦截器详解
上篇文章中我们创建了一个RestFul风格的接口,并且介绍了如何对其结果进行格式化的方式。这篇文章我们来讲在SpringMVC中常用到的另一个概念拦截器。 什么是拦截器? 在Spring MVC中拦截器有点类似于在Servlet中的过滤器的概念,主要是用来拦截用户求,并且对用户请求进行相应…...

ACM第三次考核题解
ACM第三次考核题解 题目序号难度题目编号题目考察知识点1签到题A这是一道很难的题!!!输出2迷之难度F神说要有光,于是有了手电筒贪心3简单BThis is a real English problem!思维 英语4简单C玩具简单排序5简单I“近义词…...
基于Vue3内置的lodash函数库实现防抖节流
前言 防抖节流是可以说是一种优化组件性能的技巧,可以有效减少组件中的渲染次数和计算量,从而提高组件的响应速度和用户体验。在Vue3中可以使用lodash库中的debounce和throttle函数来分别实现防抖和节流。当然也可以自行设计实现防抖节流函数࿰…...
ls -a有什么作用
ls -a 是一个在类Unix操作系统(如Linux、macOS)中使用的命令,用于列出目录中的所有文件和目录,包括那些以.开头的隐藏文件。 具体作用如下: ls: 这是一个列出目录内容的命令。 -a: 这是一个选项,表示"…...

LLM - 使用 RAG (检索增强生成) 多路召回 实现 精准知识问答 教程
欢迎关注我的CSDN:https://spike.blog.csdn.net/ 本文地址:https://spike.blog.csdn.net/article/details/142629289 免责声明:本文来源于个人知识与公开资料,仅用于学术交流,欢迎讨论,不支持转载。 RAG (R…...

编程语言图书创作要注意的事情有哪些?
编程语言图书的创作是一项复杂且具有挑战性的任务,需要作者深入理解技术、清晰表达,并考虑读者的学习体验。一本优秀的编程书籍不仅能够教授技术知识,更能引导读者逐步深入,激发他们的思考和实际应用能力。以下将详细探讨编程语言…...
主流高级编程语言的推出时间及年份
1.下表一些主流高级编程语言的推出时间及年份: 高级语言 推出时间 岁数 FORTRAN 1957 67 LISP 1959 65 COBOL 1961 63 BASIC 1964 60 Pascal 1970 54 C 1972 52 MATLAB 1978 46 SQL 1978 46 Objective-C 1983 41 C 1983 41 Perl …...
qt 模仿简易的软狗实现
我们在写软件的时候,希望我们的软件只在固定的机器上运行,其他机器上运行不了,那我们应该如何做呢? 1 首先我们需要得到运行机器的mac地址,这样可以简易的判断是否是我们授权的机器。 那我们首先定义一个授权mac机器…...

荣业食品销售费用每年上亿元:主要产品收入大降,电商占比过低
《港湾商业观察》黄懿 今年3月,广东荣业食品有限公司的控股公司Wing Yip Food Holdings Group Limited(下称“荣业食品”)向美国SEC递交了纳斯达克上市申请。 据悉,2023年11月,商务部宣布移除了一批共计55家因长期经…...

数据结构:并查集
数据结构:并查集 并查集原理实现框架初始化合并查询获取成员路径压缩其它 总代码 并查集 在生活中,经常会出现分组问题。比如一个班级分为多个小组,打篮球分为两方等等。在同一个组中的所有成员,就构成一个集合。对这种一个群体分…...
微信小程序实战教程:轻松实现列表批量选择功能
在许多场景下,用户需要对列表中的多项内容进行操作,如批量删除、批量下载等。为了满足这一需求,我们需要在微信小程序中实现列表批量选择功能。具体要求如下: 用户可以逐个选择列表项,也可通过全选按钮快速选择所有列表…...

企业微信:开启客户联系和配置
前言 客户联系是企业微信的一项非常实用且自定义化配置丰富的功能,使企业内的授权员工可以添加外部客户(企业微信联系人和微信联系人)进行工作沟通,并且还可以建立客户群,甚至发表内容到客户朋友圈! 由于功…...

Python发送邮件教程:如何实现自动化发信?
Python发送邮件有哪些方法?如何利用python发送邮件? 无论是工作汇报、客户通知还是个人提醒,邮件都能快速传递信息。Python发送邮件的自动化功能就显得尤为重要。AokSend将详细介绍如何使用Python发送邮件,实现自动化发信&#x…...

一周热门|苏姿丰:芯片行业不能只盯着 GPU;Gartner:GenAI 即将越过期望膨胀期
大模型周报将从【企业动态】【技术前瞻】【政策法规】【专家观点】四部分,带你快速跟进大模型行业热门动态。 01 企业动态 Open AI 计划从非营利组织向营利组织转型 日前,路透社报道称,OpenAI 正在制定一项计划,将其核心业务重…...
Failed to load WebView provider: No WebView installed
1、问题 使用webview加载网页,在应用运行时,报了如下错误:android.webkit.WebViewFactory$MissingWebViewPackageException: Failed to load WebView provider: No WebView installed2、分析 通过查看项目的修改记录,确实安装了We…...

java日志框架之Log4j
文章目录 一、Log4j简介二、Log4j组件介绍1、Loggers (日志记录器)2、Appenders(输出控制器)3、Layout(日志格式化器) 三、Log4j快速入门四、Log4j自定义配置文件输出日志1、输出到控制台2、输出到文件3、输出到数据库 五、Log4j自…...
conda相比python好处
Conda 作为 Python 的环境和包管理工具,相比原生 Python 生态(如 pip 虚拟环境)有许多独特优势,尤其在多项目管理、依赖处理和跨平台兼容性等方面表现更优。以下是 Conda 的核心好处: 一、一站式环境管理:…...

手游刚开服就被攻击怎么办?如何防御DDoS?
开服初期是手游最脆弱的阶段,极易成为DDoS攻击的目标。一旦遭遇攻击,可能导致服务器瘫痪、玩家流失,甚至造成巨大经济损失。本文为开发者提供一套简洁有效的应急与防御方案,帮助快速应对并构建长期防护体系。 一、遭遇攻击的紧急应…...

视频字幕质量评估的大规模细粒度基准
大家读完觉得有帮助记得关注和点赞!!! 摘要 视频字幕在文本到视频生成任务中起着至关重要的作用,因为它们的质量直接影响所生成视频的语义连贯性和视觉保真度。尽管大型视觉-语言模型(VLMs)在字幕生成方面…...
Spring Boot面试题精选汇总
🤟致敬读者 🟩感谢阅读🟦笑口常开🟪生日快乐⬛早点睡觉 📘博主相关 🟧博主信息🟨博客首页🟫专栏推荐🟥活动信息 文章目录 Spring Boot面试题精选汇总⚙️ **一、核心概…...

PL0语法,分析器实现!
简介 PL/0 是一种简单的编程语言,通常用于教学编译原理。它的语法结构清晰,功能包括常量定义、变量声明、过程(子程序)定义以及基本的控制结构(如条件语句和循环语句)。 PL/0 语法规范 PL/0 是一种教学用的小型编程语言,由 Niklaus Wirth 设计,用于展示编译原理的核…...

基于Java+VUE+MariaDB实现(Web)仿小米商城
仿小米商城 环境安装 nodejs maven JDK11 运行 mvn clean install -DskipTestscd adminmvn spring-boot:runcd ../webmvn spring-boot:runcd ../xiaomi-store-admin-vuenpm installnpm run servecd ../xiaomi-store-vuenpm installnpm run serve 注意:运行前…...

什么是VR全景技术
VR全景技术,全称为虚拟现实全景技术,是通过计算机图像模拟生成三维空间中的虚拟世界,使用户能够在该虚拟世界中进行全方位、无死角的观察和交互的技术。VR全景技术模拟人在真实空间中的视觉体验,结合图文、3D、音视频等多媒体元素…...
vue3 daterange正则踩坑
<el-form-item label"空置时间" prop"vacantTime"> <el-date-picker v-model"form.vacantTime" type"daterange" start-placeholder"开始日期" end-placeholder"结束日期" clearable :editable"fal…...
高防服务器价格高原因分析
高防服务器的价格较高,主要是由于其特殊的防御机制、硬件配置、运营维护等多方面的综合成本。以下从技术、资源和服务三个维度详细解析高防服务器昂贵的原因: 一、硬件与技术投入 大带宽需求 DDoS攻击通过占用大量带宽资源瘫痪目标服务器,因此…...
13.10 LangGraph多轮对话系统实战:Ollama私有部署+情感识别优化全解析
LangGraph多轮对话系统实战:Ollama私有部署+情感识别优化全解析 LanguageMentor 对话式训练系统架构与实现 关键词:多轮对话系统设计、场景化提示工程、情感识别优化、LangGraph 状态管理、Ollama 私有化部署 1. 对话训练系统技术架构 采用四层架构实现高扩展性的对话训练…...