审核平台前端新老仓库迁移

背景

审核平台接入50+业务,提供在线审核及离线质检、新人培训等核心能力,同时提供数据报表、资源追踪、知识库等工具。随着平台的飞速发展,越来越多的新业务正在或即将接入审核平台,日均页面浏览量为百万级别。如今审核平台已是公司内容生产链路上的关键一环,是保障内容安全的重要防线,因此稳定性至关重要。

过去一年我们曾对前端项目进行框架升级,考虑风险与成本最小化,选择了渐进式升级,利用微前端实现Vue2和Vue3共存,新接业务在Vue3仓库中开发。经过一年的迭代,Vue3项目趋于稳定,沉淀了大部分通用能力。为了降低多仓库维护心智,同时解决核心模块的技术债务,考虑将剩余活跃代码进行重构并迁移至新仓库。

面向迁移的重构 - 整洁架构在前端的应用

案例选择

参考前端埋点报表,选择老仓库中页面维度访问量最高的路由,对线上使用情况进行摸排。日常业务现状是点直融合,直播业务配置化接入需求较多,因为业务形态的差异,定制需求多,现有配置能力无法满足,需扩充。开发现状是通用配置化代码改动频繁,逻辑复杂,开发门槛较高,影响范围大,牵一发而动全身。因此选择配置化详情页作为优先重构并迁移的对象。

配置化详情页采用的是业务定制化的低代码方案,包含schema渲染器和任务流两部分。当前已沉淀近百份json schema,托管在内部其他低代码平台上。页面覆盖40多个业务,占据平台约20%访问量和35%独立访客。

图片

图片

如果将页面看做一个黑盒子,依据唯一标识(路由path和query等)从node服务、平台服务以及外部业务方服务获取数据,基于页面内部规则渲染页面。审核员浏览并进行通过、驳回等操作,提交后将对视频、弹幕等业务资源产生影响。

图片

schema渲染器基于json schema和接口数据,在平台内生成路由信息与页面内容,负责各种模式的页面分发、物料分发,并提供敏感词、快照、洗数等通用平台能力。

代码现状是数据获取、提交操作和页面复杂逻辑分散在vue文件和store中,业务逻辑和UI框架耦合严重,不利于集成自动化测试和框架升级。待办、任务、资源等边界划分不清晰,平铺在“巨石store“中,维护成本极高且代码改动风险大。渲染器和任务流逻辑不够内聚,耦合严重,无法做到关注点分离。因此需要寻找一种合适的架构进行重构,减弱业务逻辑对UI框架的依赖,增强可测试性。

整洁架构

整洁架构由Robert C. Martin在2012年提出,核心思想是将软件系统拆分为独立的层次,以实现高内聚、低耦合、可测试和可维护。

图片

一共分为四个层级,环与环之间,存在一个依赖关系原则:源代码中的依赖关系,必须只指向同心圆的内层,即由低层机制指向高级策略。

  • 实体层:包含业务领域的核心概念和业务逻辑。

  • 用例层:实现特定的业务用例,将实体层的业务逻辑与具体的应用场景结合起来。

  • 接口适配器层:负责处理与外部系统的交互比如用户界面、数据库、web服务等。将外部系统的请求和数据格式转化为用例层和实体层能够理解和处理的对象。

  • 框架和驱动层:包含具体的框架和工具,比如web框架、数据库驱动等。

优点是可以在没有UI、数据库、web服务器或其他外部基础设施的情况下测试业务逻辑;降低对UI框架的依赖,比如跨端开发时,业务逻辑可以复用,只需要做UI层的适配。相应的,缺点也很明显,过于复杂,数据需要经过多层处理。学习成本较高,容易过度设计,增加复杂性,灵活性较低。适用于大型复杂项目,对于需要长期维护和持续开发的项目,清晰层次结构和明确依赖关系有助于减少代码腐化,更容易适应需求变化。针对我们选择的模块,审核前端配置化页面,比较适合。

重构

图片

图片

实体层

图片

主要可拆分成待办、任务、资源等实体。待办实体,主要提供待办的基础信息、获取配置等属性和方法。任务实体提供任务的状态、任务耗时、调度配置、任务数据,计时和拉取任务流程等。资源实体提供资源的详情数据、获取详情及数据清洗方法等。待办实体包含了配置详情页所需的核心数据,任务实体高度抽象了核心任务流。在新业务接入过程中,实体层一般不变动,通过依赖倒置划分架构边界。

// entities/todo.js
export default class Todo {todoIdbusinessIdtodoConfig...constructor() {}async getTodoConfig() {// 获取配置}
}// entities/task.js
export default class Task {dispatch_conflistDatatimeCount...constructor() {}async getTaskDetail({ getTask, taskFormat, afterGetTask}) {// 抽象封装核心任务流程}// 计时逻辑startTimer() {}clearTimers() {}
}// entities/resource.js
export default class Resource {detaildataReadyconstructor() {}async getResourceDetail({ getResource, resourceFormat, afterGetResource }) {// 抽象封装资源模式核心流程}
}
 

用例层

用例层针对洗数、提交等复杂场景,通过调用实体层来实现特定的业务逻辑,是适配器层与实体层的中介。例如封装了基于配置的洗数中间件,列表模式的单个和批量提交,卡片模式的单个和批量提交,快照上报、自动化质检的复杂逻辑。用例层包含了系统的复杂业务逻辑,为单元测试提供了便利,可以独立于UI和外部系统进行测试。

// usecase/use-single-submit
import { get } from 'lodash-es' // 三方工具库
import { ANNOTATION_SINGLE_OPER_PASS_NAME } from '@/constants' // 常量
import { workbenchApi } from '@/api'
import { setLogData } from '@/utils/xx' // 工具函数
export function getSingleSubmitParams({ data, state.xxx }) {//... 逻辑处理return params
}
export function submitAuditSingle({ data, afterTaskSubmit }) {const params = ...workbenchApi.submit(params).then((res) => {if(res.code = xxx){afterTaskSubmit() // 调用钩子函数}})
}

适配器层

适配器层包含store和UI,调用用例层的代码,主要负责将依赖UI、外部服务、设备等的数据处理为用例层可以使用的“干净数据”。适配器层一般不包括复杂的业务逻辑,因此在框架迁移时仅需关注基本的框架差异,适合自动化代码转换。

// store/todoConfigDetail
import { getTodoInfo } from '@/struct/TodoConfigDetailStruct/usecase/use-todo'
import { getTaskInfo, getTask, taskDispatchListFormat } from '@/struct/TodoConfigDetailStruct/usecase/use-task'
import { getSingleSubmitParams, submitAuditSingle } from '@/struct/TodoConfigDetailStruct/usecase/use-single-submit'
const todo = ref({})
async function init({ $route }) {const query = $route.queryconst todoId = +query.todo_id...set(todo, getTodoInfo({ todoId, ... }))await get(todo).getTodoConfig()...
}
function getTaskDetail() {get(task).getTaskDetail({getTask: async ({ noSeize, drillTaskIds }) => await getTask({ todo: get(todo), noSeize, drillTaskIds }),taskFormat: async (data) => await taskDispatchListFormat({ data, schema: get(todo).schema })afterGetTask: (res) => { ... }})
}
async function submit(data) {if (single) {const params = getSingleSubmitParams({ data, todo: get(todo) })submitAuditSingle({ params, afterTaskSubmit: () => { ... } })} else {...}
}
// Audit.vue
const todoConfigDetailStore = useTodoConfigDetailStore()
const { todo, task, multipleSelection } = storeToRefs(todoConfigDetailStore)
const { getTaskDetail, submit } = todoConfigDetailStore

新业务接入一般对实体层和用例层无改动,仅需适配器层增加相应的展示物料,尽可能避免“牵一发动全身”。新架构会有一定的初学成本,但结合审核平台复杂的项目现状和持续接入新业务的节奏,长远来看对于系统稳定性、可测试性有一定帮助,同时降低UI框架依赖性。

基于新的架构,可分层进行自动化测试和自动代码转换。

完善用例层的单元测试

用例层为纯函数,不依赖框架、设备、三方服务等。单元测试的技术栈为jest和vue-test-utils,从审核员的基本工作模式入手,针对任务领取、数据清洗与展示、稿件处理三个环节完善测试用例。因为是新老仓库迁移,所以可以将线上环境视为基准进行用例采集。根据业务重要性、线上访问情况,按优先级执行测试。单测能有效降低回归成本,在新业务持续接入的背景下,保障系统稳定性。

适配器层的自动代码转换 - 基于gogocode将vue2升级为vue3 setup语法

调研与分析

在寻求升级方案的过程中,我们对比了两款工具:Gogocode 和 vue2-to-composition-api。以下是它们的简要对比:

功能

Gogocode

vue2-to-composition-api

缺点默认不支持转换成 Vue3 setup 语法不支持 template 转换
转换规则覆盖转换规则列表转换效果
特性像 jQuery 一样修改AST将 Vue2 代码转换为 Vue 3 的 Composition API 格式
2. jQuery-like API 简化了 AST 修改成本2. 支持在线转换
优点1. 支持自定义插件1. 支持转换成 Vue3 setup 语法

经过调研,gogocode 是基于 AST 封装的库可扩展空间大,但是默认 gogocode-plugin-vue 不支持转换成 Composition API

vue2-to-composition-api 倒是支持 Composition API,但是不支持 template 部分的转换。

考虑到我们的项目中有许多自定义的转换逻辑,如 UI 库替换、store 替换等,我们最终决定使用 gogocode 作为主要工具,并结合其他手段来实现 vue2 到 vue3 的全面升级。

实施与探索

基础:升级语法

升级语法是借助 gogocode 的 replace 方法实现的,通过 $$$ 匹配符保留所需要的代码块,将 Vue2 的语法快速替换成 Vue3 的语法。

// 替换datascriptAst.replace("data() {return {$$$};}", `const $data = reactive({$$$})`);
// 替换propsscriptAst.replace("props:{$$$}", "const props = defineProps({$$$})");​​​​​​​
// 替换生命周期scriptAst.replace("created(){$$$}", "onBeforeMount(()=>{$$$})")  .replace("mounted(){$$$}", "onMounted(()=>{$$$})")  .replace("async mounted(){$$$}", "onMounted(async ()=>{$$$})")  .replace("beforeUnmount(){$$$}", "onBeforeUnmount(()=>{$$$})")  .replace("unmounted(){$$$}", "onUnmounted(()=>{$$$})")  .replace("beforeDestroy(){$$$}", "onBeforeUnmount(()=>{$$$})")  .replace("destoryed(){$$$}", "onUnmounted(()=>{$$$})");

效果如下,通过 replace 替换的方式适合大部分场景,比如 methods、filters、watch 等

图片

进阶:处理模板中的变量、函数中的this变量

template绑定的变量可能是data,可能是props,还可能是 methods

想要替换 template 中的变量,需要先收集 data、props、methods 中的 keys

​​​​​​​

getDataKeys() {  const keys = new Set();  // 只需要第一层的key,所以deep设为1  this.scriptAst.find('data() {$$$}').find('$_$:$_$', { deep: 1 }).each(node => {    if (node.match[0] && node.match[0][0].node.type === 'Identifier') {      keys.add(node.match[0][0].value);    }  });  return Array.from(keys)}getPropsKeys() {  const keys = new Set();  this.scriptAst.find('props: {$$$1}', { deep: 1 }).each((node) => {    if (node.match['$$$1']) {      node.match['$$$1'].forEach((item) => {        if (item.key && item.key.type === 'Identifier') {          keys.add(item.key.name)        }      })    }  });
  return Array.from(keys)}// methods 有点小复杂,需要考虑异步函数,普通函数和键值对的写法getMethodsKeys() {  const methodsAst = this.scriptAst.find('methods:{$$$}');  const methods = methodsAst.find('$_$() {$$$1}');  const asyncmMethods = methodsAst.find('async $_$(){$$$1}');  const mapKeys = methodsAst.find('$_$:$$$1', { deep: 1 });  const methodNames = [];  methods.each(node => {    if (node.match[0] && node.match[0][0]) {      methodNames.push(node.match[0][0].value);    }  });  asyncmMethods.each(node => {    if (node.match[0] && node.match[0][0]) {      methodNames.push(node.match[0][0].value);    }  });  mapKeys.each(node => {    if (node.match[0] && node.match[0][0]) {      methodNames.push(node.match[0][0].value);    }  });  return methodNames;}

收集完成后可以开始遍历 template 中的 attr,并替换所绑定的变量了。

​​​​​​​

handlTemplate() {    // 替换attr, 例如 <div :value="value"></div>    this.ast.find("<template></template>").find(`<$_$ ="$$$0" >$$$1</$_$>`).each((node) => {      node.match['$$$0'].forEach(attr => {        if (attr && attr.value) {          this.dataKeys.some(keyName => {            const reg = new RegExp(`${keyName}\\b`, 'g')            const macth = reg.test(attr.value.content);            attr.value.content = attr.value.content.replace(reg, `$data.${keyName}`)            if (macth) {              return true;            }          })          this.methodsKeys.some(keyName => {            const reg = new RegExp(`\\b${keyName}\\b`, 'g')            const macth = reg.test(attr.value.content);            attr.value.content = attr.value.content.replace(reg, `methods.${keyName}`)            if (macth) {              return true;            }          })
          this.propsKeys.some(keyName => {            const reg = new RegExp(`\\b${keyName}\\b`, 'g')            const macth = reg.test(attr.value.content);            attr.value.content = attr.value.content.replace(reg, `props.${keyName}`)            if (macth) {              return true;            }          })        }      })      // 替换content,例如:<div>{{value}}<div>      node.match['$$$1'].forEach(node => {        if (node.content && node.content.value) {          // 省略:与上面类似        }      })    })  }

说到 this 替换也是一个繁琐的问题,this.xx, xx 可以是 data,可以是props,可以是 function,还可以是私有属性等。

所以我们需要先把组件中的 data、props、methods、mapGetter 中的 keys 都收集一遍,然后再替换 script 中的 this 变量。​​​​​​​

// 正则替换更方便,所以需要放在最后一步替换handlThis(code) {  code = code.replace(/this\.([_$0-9a-zA-Z]+)/g, (match, $1) => {    // 替换function body 中的 data引用    if (this.dataKeys.includes($1)) {      return `$data.${$1}`;    }    // 替换function body 中的 methods调用    else if (this.methodsKeys.includes($1)) {      return `methods.${$1}`;    }    // 替换 vm 私有属性    else if ($1 && $1[0] === '$') {      return `$vm.${$1}`;    } else if (this.computedKeys.includes($1)) {      return $1    } else if (this.propsKeys.includes($1)) {      return `props.${$1}`    }    return `$vm.${$1}`  })  // 替换function body 中的 动态methods调用  code = code.replace(/this\[(.+)\]/g, (match, $1) => {    return `methods[${$1}]`;  })  return code}

进阶:动态调用this.xxx该如何解决

​​​​​​​

async parseOpenDialog(payload) {  const { schema, data } = payload  await this[schema.method](    parseSchema,    data,    dialogParams,    dialogType  )}

按 vue3 新的写法,一般是展开的​​​​​​​

const parseOpenDialog = async (payload) => {}

但是按这个习惯来转换,就无法做到动态调用this.xxx,我们可以尝试把方法都放在methods对象中,有点类似 vue2​​​​​​​

const methods = {  parseOpenDialog: async (payload) => {  },  xxx: async() => {  }}

在替换 this 时,将 this 替换成 methods 变成 methods[schema.method]()。

其他技巧

1 . 原来 vue2 中肯很多属性挂在组件实例上,比如 $route, $router, $emit 甚至自定义的属性等等。下面是 vue3 中获取组件实例的方法。​​​​​​​

import { getCurrentInstance } from 'vue'
const { proxy: $vm } = getCurrentInstance()$vm.xxx = '自定义属性'
 

2 . 原来使用的 vuex,现在使用的是 pinia。我们需要先收集 ...mapState($_$, [$$$1]),然后使用 storeToRefs 代替​​​​​​​

// 获取store 使用storeToRefsif (this.storeType === 'pinia') {  this.scriptAst  .find("computed:{}")  .before(`const {${stateNames.join(',')}} = storeToRefs(${this.getPiniaStoreName(key)})`)  .replace("computed:{$$$}", "$$$")}

最终将上述转换能力封装成一个库,通过 npm 安装来实现组件批量升级。

迁移前后的E2E测试 - 视觉辅助UI自动化测试

端到端测试是确保新旧系统平稳过渡的关键步骤。本次迁移依旧遵循渐进式升级的原则,新增v3路由,线上新老路由共存。共分为功能测试、UI测试、性能测试三个部分。功能测试为单元测试的补充,主要验证新老路由下核心操作路径及提交参数的一致性,拦截请求避免对线上造成影响。性能由通用监控大盘进行保障。UI对比测试是本次的重点。

新老仓库分别基于Element UI和Element Plus,Element Plus重新设计了组件以适应Vue3,组件尺寸体系调整为更自然的大中小选项。间距优化为更通用的4px体系,主要涉及 padding 和 margin 属性修改、 font-size 等字体和图标大小修改等。因此,虽然大部分组件在外观上保持相似,视觉和布局上可能有一些差异。由于业务组件具备一定复杂性,手写测试用例工作量繁琐,新旧页面组件可能存在差异无法完全复用,方案也不具备通用性。因此考虑使用计算机视觉技术来识别和验证用户界面的元素。

基于公司内部的自动化测试平台,测试框架为Playwright,测试语言选择Python以更好的利用丰富的图像处理库。指定CSS选择器,随机选择页面上的元素进行截图和对比,设定阈值进行判断。图像相似度对比可分为传统的基于像素差的方法和基于图像特征的方法。图像特征有SIFT、ORB等特征提取方法和深度学习方法。分别选择一种代表性的算法进行对比和测试。

  • SSIM(结构相似性指数),同时考虑图片亮度、对比度与结构信息。用于检测两张相同尺寸的图像的相似性。对于图像的亮度和对比度变化具有较好的鲁棒性。

  • SIFT(尺度不变特征变换),用于在图像中检测和描述局部特征,这些特征对图像的缩放、旋转和部分亮度变化具有不变性。能够检测和匹配不同尺寸下的特征,对图像的仿射变换、噪声和部分遮挡具有较好的鲁棒性。

  • LPIPS是一种深度学习特征,用于量化图像之间的感知相似性,通过比较图像在深度神经网络中的特征表示来工作,这些特征能够捕捉到人类视觉所关注的图像细节。基于一个大规模的人类感知相似性判断数据集,包含484K个人类的判断,涵盖了多种图像变换和失真类型。使用深度特征(例如VGG网络中的特征)来衡量图像相似性,比传统的度量方法更有效。

v2v3SSIMSIFTLPIPS

图片

图片

0.6560.8550.909

图片

图片

0.9160.8740.961

图片

图片

0.6580.8570.881

图片

图片

0.8770.8550.926

图片

图片

0.5510.8360.947

图片

图片

0.9290.8840.942

实验表明,基于深度学习特征的相似度对比结果更接近用户感知,针对Vue2升级Vue3 UI组件库导致的间距、字体、尺寸等细微差异判断更准确。因此采用LPIPS作为对比算法。​​​​​​​

def compare_images(url1, url2):  loss_fn = lpips.LPIPS(net = 'alex')  img1 = cv2.imread(url1)  img2 = cv2.imread(url2)  if img1 is not None and img2 is not None and img1.size > 0 and img2.size > 0:      img2 = cv2.resize(img2, (img1.shape[1], img1.shape[0]))      cv2.imwrite(url2, img2)      combined_image = cv2.hconcat([img1, img2])      ex_img1 = lpips.im2tensor(lpips.load_image(url1))      ex_img2 = lpips.im2tensor(lpips.load_image(url2))      d = loss_fn.forward(ex_img1, ex_img2)      if d is not None:          cv2.putText(combined_image,'score: %.3f'%(1 - d.mean()), (20, 20), cv2.FONT_ITALIC, 0.4, (255, 0, 255))      return d, combined_image  else:      return None

依据业务流程,分别访问新老路由,对页面指定元素进行截图、对比和拼接,输出测试截图和完整的测试报告。

图片

图片

采用视觉辅助UI自动化测试,更接近用户真实感知,大大降低测试用例复杂度,提升测试效率,无需关注繁杂的DOM元素层级。

上线

以页面配置的形式按待办进行灰度,跳转至新路由。单元测试已集成至项目流水线中,MR和发布前触发。灰度过程中手动执行E2E用例,以自定义环境变量的形式指定页面路径、元素选择器、相似度阈值等。测试通过后修改页面配置,引流至新路由。通过用户故障群和页面反馈入口响应用户反馈,结合前端埋点报表观察线上使用情况和确定灰度策略。通过完善单元测试、E2E测试和制定合理的灰度策略,针对特定模板的迁移已顺利完成,期间未收到线上故障反馈。

后续迁移策略将以老仓库中改动较频繁文件优先入手,测试用例先行,借助自动代码转换工具快速平稳迁移,线上埋点数据做辅助。

收益

活跃代码陆续迁移,结束多仓库并行,减少维护心智。之前我们的常态是新老仓库并行,开发一个完整的业务功能时要在ts和js,选项式API和setup语法之间频繁切换,心智负担较重。活跃代码陆续迁移至vue3新仓库,结合新的框架特性和实用工具,能够更专注于业务逻辑本身。

图片

核心模块重构,“巨石store”轻量化,提高可维护性和可演进性,分层结构保障核心逻辑稳定性。原本配置能力扩充时需要在复杂的数据流中“走迷宫”,耦合严重,通用代码影响范围大,常常出现A业务需求上线导致B业务不可用的情况。利用整洁架构进行分层设计后,新增一种审核模式仅需在适配器层新增对应物料和action,用例层新增用例。无需修改实体层和其他业务相关的用例、通用页面、物料等。

图片

图片

业务逻辑的重新梳理,弥补测试用例空缺。领域提取是对业务逻辑的重新梳理,前端能加深业务理解。稳定性至上的模块很长时间缺少测试用例,造成对开发人员的经验、能力依赖极大。完善核心链路的测试用例能有效降低回归成本,保障系统稳定性。

工具沉淀,组内复用。自动代码转换工具和基于AI能力的前端E2E测试方案为后续组内其他项目的框架升级和迁移提供了便利。

总结与展望

在2023年开始渐进式升级Vue3后,我们经历了很长一段时间的多仓库并行。在新业务不断接入、开发新成员加入的背景下,这样的模式无疑提升了开发门槛和维护心智。本次活跃代码的陆续迁移结束了多仓库并行的现状,同时在整个实践过程中我们为审核平台这个大型复杂项目前端引入了整洁架构的思想,为后续的开发维护提供了一种新的思路。沉淀了一套自动化vue2代码转vue3 setup的工具,可为后台项目的框架升级提供便利。同时借助AI能力提升前端E2E测试的效率,利用计算机视觉辅助前端UI自动化测试。有几点心得:

完美重构、敏捷重构、系统稳定性难以平衡。

图片

既然下定决心对年久失修的代码进行重构,我们一定是追求极致优化和整洁的。但是需求现状是不断有新特性进来,战线拉的太长必将导致抹平差异的成本增加,因此敏捷性也很重要。同时底线是关注系统的可靠性和稳定性。这三者一定程度上存在矛盾,需平衡:

  • 任务拆分,基于埋点数据选择最重要或最紧急的链路进行重构,而非追求大而全,先落地架构,后扩充功能

  • 测试用例先行,重构必将引入风险,用例先行能最大程度保障核心功能的稳定迁移

  • 制定合理的灰度策略,新增路由,以页面配置形式进行灰度,优先uv较低的页面验证,并保证及时的反馈渠道

  • 持续优化,重构应成为一种思想,在迭代中持续优化

整洁架构非银弹,容易过度设计,学习门槛较高。

  • 按模块重构,而非项目

  • 针对新架构,制定长期维护计划,组织团队培训,降低学习成本

多仓库迁移路线:数据为支撑,测试用例先行,借助自动代码转换工具和视觉辅助UI自动化测试,制定合理的灰度策略并建立及时的故障反馈和响应渠道。对于大型复杂项目或模块,先进行面向迁移的重构,也能起到事半功倍的作用。

对我们而言,迁移的结束只是起点,基于更整洁的架构和更先进的前端框架,未来仍有很多发力点:

  • 对于迁移过程中沉淀下来的新架构,需不断优化和改进,以适应复杂的业务场景。

  • 在更多的业务场景下评估迁移后的性能改进,确保用户体验得到提升。

  • 持续审查并解决在迁移过程中发现的技术债务。

  • 持续建设自动代码转换工具,赋能团队内其他项目。

  • 视觉辅助UI自动化测试的方案,进一步抽象,给到不熟悉自动化测试的团队成员“开箱即用”。

  • ...

-End-

作者丨伍月、莫小谦

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

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

相关文章

代码提交错分支了怎么办?

你有么有遇到过正在开发的代码&#xff0c;提交到生产环境的分支去&#xff0c;遇到这种情况怎么办&#xff1f; 问题重现&#xff1a; 这段注释// AAAAAAAAAAA 本来应该写在dev分支的&#xff0c;现在提交并push到master分支了 现在第一步&#xff0c;撤回提交 第二步&…

MySQL的Geometry数据处理之WKB方案

MySQL的Geometry数据处理之WKT方案&#xff1a;https://blog.csdn.net/qq_42402854/article/details/140134357 MySQL的Geometry数据处理之WKT方案中&#xff0c;介绍WTK方案的优点&#xff0c;也感受到它的繁琐和缺陷。比如&#xff1a; 需要借助 ST_GeomFromText和 ST_AsTex…

qt结合vs2022安装

进入清华大学开源软件&#xff1a; 清华大学开源软件镜像站 | Tsinghua Open Source Mirror 下载完成后&#xff0c;双击进行安装&#xff1a; 进入邮箱进行验证&#xff1a; 可能是因为网络问题&#xff0c;无法安装。 重新安装5.12.12版本。 安装后启动失败&#xff0c;重新…

横截面交易策略:概念与示例

数量技术宅团队在CSDN学院推出了量化投资系列课程 欢迎有兴趣系统学习量化投资的同学&#xff0c;点击下方链接报名&#xff1a; 量化投资速成营&#xff08;入门课程&#xff09; Python股票量化投资 Python期货量化投资 Python数字货币量化投资 C语言CTP期货交易系统开…

数据结构--单链表实现

欢迎光顾我的homepage 前言 链表和顺序表都是线性表的一种&#xff0c;但是顺序表在物理结构和逻辑结构上都是连续的&#xff0c;但链表在逻辑结构上是连续的&#xff0c;而在物理结构上不一定连续&#xff1b;来看以下图片来认识链表与顺序表的差别 这里以动态顺序表…

WGAN(Wassertein GAN)

WGAN E x ∼ P g [ log ⁡ ( 1 − D ( x ) ) ] E x ∼ P g [ − log ⁡ D ( x ) ] \begin{aligned} & \mathbb{E}_{x \sim P_g}[\log (1-D(x))] \\ & \mathbb{E}_{x \sim P_g}[-\log D(x)] \end{aligned} ​Ex∼Pg​​[log(1−D(x))]Ex∼Pg​​[−logD(x)]​ 原始 GAN …

springboot基于Java的超市进销存系统+ LW+ PPT+源码+讲解

第三章系统分析与设计 3.1 可行性分析 一个完整的系统&#xff0c;可行性分析是必须要有的&#xff0c;因为他关系到系统生存问题&#xff0c;对开发的意义进行分析&#xff0c;能否通过本网站来补充线下超市进销存管理模式中的缺限&#xff0c;去解决其中的不足等&#xff0c…

6域名系统DNS

《计算机网络》第7版&#xff0c;谢希仁 每次记不清楚的知识点&#xff0c;通过上网查找&#xff0c;总是只能看到很零碎的答案。最后还是最喜欢看这个版本的书&#xff0c;一看就回忆起来了&#xff0c;逻辑严谨&#xff0c;循循善诱&#xff0c;知识讲解的全面又清晰&#xf…

架构师应该在团队中发挥怎样的作用?

架构师分为5种&#xff1a; 1.企业架构师EA(Enterprise Architect) EA的职责是决定整个公司的技术路线和技术发展方向。 2.基础结构架构师IA(Infrastructure Architect) IA的工作就是提炼和优化技术方面积累和沉淀形成的基础性的、公共的、可复用的框架和组件&#xff0c;这…

Qt 基础组件速学 鼠标和键盘事件

学习目标&#xff1a; 鼠标事件和键盘事件应用 前置环境 运行环境:qt creator 4.12 学习内容和效果演示&#xff1a; 1.鼠标事件 根据鼠标的坐标位置&#xff0c;做出对应的事件。 2.键盘事件 根据键盘的输入做出对应操作 详细主要代码 1.鼠标事件 #include "main…

一文读懂轻量日志收集系统Loki工作原理

Loki 是由 Grafana Labs 开发的日志聚合系统&#xff0c;设计目标是提供一种高效、低成本的日志收集和查询解决方案。与传统的日志系统&#xff08;如 ELK Stack&#xff09;不同&#xff0c;Loki 不会对日志内容进行索引&#xff0c;而是仅对日志的元数据进行索引&#xff0c;…

FTP、http 、tcp

HTTP VS FTP HTTP &#xff1a;HyperText Transfer Protocol 超文本传输协议&#xff0c;是基于TCP协议 FTP&#xff1a; File Transfer Protocol 文件传输协议&#xff0c; 基于TCP协议&#xff0c; 基于UDP协议的FTP 叫做 TFTP HTTP 协议 通过一个SOCKET连接传输依次会话数…

FIND_IN_SET使用案例--[sql语句根据多ids筛选出对应数据]

一 FIND_IN_SET select id,system_ids from intellect_client_info where FIND_IN_SET(5, system_ids) > 0;

Spring Boot 中的监视器是什么?有什么作用?

前言&#xff1a; 监听器相信熟悉 Spring、Spring Boot 的都知道&#xff0c;但是监视器又是什么&#xff1f;估计很多人一脸懵的状态&#xff0c;本篇分享一下 Spring Boot 的监视器。 Spring Boot 系列文章传送门 Spring Boot 启动流程源码分析&#xff08;2&#xff09; …

Apache DolphinScheduler 与 AWS 的 EMR/Redshift 集成实践分享

引言 这篇文章将给大家讲解关于DolphinScheduler与AWS的EMR和Redshift的集成实践&#xff0c;通过本文希望大家能更深入地了解AWS智能湖仓架构&#xff0c;以及DolphinScheduler在实际应用中的重要性。 AWS智能湖仓架构 首先&#xff0c;我们来看一下AWS经典的智能湖仓架构图…

高考选专业,兴趣与就业前景该如何平衡?

从高考结束的那一刻开始&#xff0c;有些家长和学生就已经变得焦虑了&#xff0c;因为他们不知道成绩出来的时候学生应该如何填报志愿&#xff0c;也不知道选择什么样的专业&#xff0c;毕竟大学里面的专业丰富多彩&#xff0c;如何选择确实是一门学问&#xff0c;而对于学生们…

乐清网站建设规划书

乐清是位于浙江省温州市的一个县级市&#xff0c;拥有悠久的历史和丰富的文化底蕴。随着互联网的快速发展&#xff0c;网站建设成为推动乐清经济和文化发展的重要手段。因此&#xff0c;我们认为有必要制定一个全面的乐清网站建设规划书&#xff0c;以促进乐清的经济繁荣和文化…

东芝 TB5128FTG 强大性能的步进电机驱动器

TB5128FTG它以高精度和高效能为设计理念&#xff0c;采用 PWM 斩波方法&#xff0c;并内置时钟解码器。通过先进的 BiCD 工艺制造&#xff0c;这款驱动器提供高达 50V 和 5.0A 的输出额定值&#xff0c;成为广泛应用场景中的强劲解决方案。 主要特性 TB5128FTG 拥有众多确保高…

SAP PS学习笔记01 - PS概述,创建Project和WBS

本章开始学习PS&#xff08;Project System&#xff09;。 1&#xff0c;PS的概述 PS&#xff08;Project System&#xff09;是SAP企业资源规划系统中的一个关键模块&#xff0c;主要用于项目管理。 它提供了一个全面的框架来规划、控制和执行项目&#xff0c;涵盖了从项目启…

竞赛选题 卷积神经网络手写字符识别 - 深度学习

文章目录 0 前言1 简介2 LeNet-5 模型的介绍2.1 结构解析2.2 C1层2.3 S2层S2层和C3层连接 2.4 F6与C5层 3 写数字识别算法模型的构建3.1 输入层设计3.2 激活函数的选取3.3 卷积层设计3.4 降采样层3.5 输出层设计 4 网络模型的总体结构5 部分实现代码6 在线手写识别7 最后 0 前言…