当前位置: 首页 > article >正文

go实战案例:如何结合 Jenkin 完成持续化集成和自动化测试?

今天我们主要来介绍如何结合Jenkins完成持续化集成和自动化测试的案例。在微服务开发团队中一般会采用敏捷开发这类增长式的开发方式这能有效提高各个微服务的迭代效率。为了让完成的代码能够尽快得到反馈我们建议尽早将完成的代码提交到代码库中被集成部署每天一次甚至一天多次通过自动构建和自动化测试尽早检测出集成的错误从而确保错误被尽快发现和纠正。持续集成与Jenkins Pipeline在敏捷开发中持续集成ClContinuousIntegration是为了更快地发现和修复系统集成遇到的各类问题它建议开发人员一天最少提交一次或者多次代码到代码库中让自动化工具对提交的代码进行集成部署并使用自动化测试工具检验代码是否正常运行从而更快地发现代码中存在的问题并进行修复。一般来说业务系统经过微服务划分后每一个微服务都是由独立的小团队进行开发和维护在系统集成时考虑到微服务之间存在大量的互相调用这就要求我们不仅要验证微服务内模块的集成结果还需要验证微服务之间的集成结果。因此持续集成能够加快各个小团队之间的协作及早发现系统集成中遇到的问题进而提升整个项目的开发效率。Jenkins是常用的持续集成工具。它采用Java 开发提供Web界面简化操作并支持插件式扩展可以处理几乎任何类型的构建和持续集成。Jenkins 中提供多种方式进行构建工作其中 Pipeline 是最为常用的方式之一。Pipeline是一套运行在Jenkins上的工作框架。它能够将多个节点中的任务连接起来实现单个节点难以完成的复杂流程的编排和可视化工作。Pipeline以代码的形式实现它将一个流水线划分为多个Stage每个 Stage代表了一组操作比如构建、测试、部署等而 Stage 内部又由多个Step 组成每一个 Step 就是基本的操作命令比如打印日志echo等命令。在本文的后半部分我们将通过一个Pipeline完成user服务从GitHub中拉取代码到编译打包成镜像再到部署到Kubernetes 的流程。Go的单元测试在前面的CI简易流程图中我们可以看到服务在经过构建和部署之后会进行相应的测试来验证部署的代码是否合理。Go本身提供了一套轻量级的测试框架用于对Go程序进行单元测试和基准测试。g0test命令是一个按照一定的约定和组织来测试代码的程序它执行的文件都是以_test.go”作为后缀这部分文件不会包含在gobuild的代码构建中在测试文件中主要存在以下三种函数类型1.以Test作为函数名前缀的测试函数一般用作单元测试测试函数的逻辑行为是否正确2.以Benchmark作为函数名前缀的基准测试函数一般用来衡量函数的性能3.以Example作为函数名前缀的示例函数主要用于提供示例文档。接下来我们通过user_dao_test.go测试文件介绍如何编写Go的单元测试用例代码如下所示package dao import ( testing func TestUserDAoImpl_Save(t *testing.T) { userDAO : UserDAOImpl{} err : InitMysql(127.0.0.1, 3306, root, 123456, user) if err ! nil{ t.Error(err) t.FailNow() } user : UserEntity{ Username:aoho, Password:aoho, Email:aohomail.com, } err userDAo.Save(user) if err ! nil{ t.Error(err) t.FailNow() } t.Logf(new User ID is %d, user.ID) } func TestUserDAoImpl_SelectByEmail(t *testing.T) { userDAO : UserDAOImpl{} err : InitMysql(127.0.0.1, 3306, root, 123456, user) if err ! nil{ t.Error(err) t.FailNow() } user, err : userDAo.SelectByEmail(aohomail.com) if err ! nilf t.Error(err) t.FailNow() } t.Logf(result uesrname is %s, user.Username) }一般来说测试文件会以待测试文件名_test.go的方式命名比如user_dao_test.go说明是对user_dao.go文件的测试用例。类似的测试函数也是以“Test待测试函数的方式进行命名比如TestUserDAOlmpl_SelectByEmail 是对UserDAOlmpl 结构体的 SelectByEmail方法进行测试你也可以根据测试路径的不同添加额外的修饰语。测试文件需要导入testing 包测试函数中的*testing.T参数用于报告测试结果和附加的日志信息。我们可以通过go test命令运行测试用例在 user_dao_test.go所在目录下执行go test 命令即可执行user_dao_test.go内所有的测试函数并在命令行打印相应的执行结果。使用Pipeline构建部署服务在部署Pipeline 服务之前我们首先将 user服务依赖的 MySQL和 Redis 独立部署到Kubernetes上这里我们以Redis的yaml配置为例apiversion: apps/v1 kind: Deployment metadata: name: user-redis labels: name: user-redis spec: replicas: 1 strategy: type: RollingUpdate selector: matchLabels: name: user-redis template: metadata: labels: name: user-redis spec: containers: #定义Redis容器开放6379端口 - name: user-redis image: redis:5.0 ports: - containerPort: 6379 imagePullpolicy: IfNotPresentuser-redis.yaml 文件通过 Deployment Controller 管理 Pod当 Controller 中的 Pod 出现异常被重启时很可能导致 Pod 的IP 发生变化。如果此时 user服务通过固定IP 的方式访问 Redis很可能会访问失败。为了避免这种情况我们可以为 user-redis Pod 定义一个 Service配置文件描述如下:apiversion: v1 kind: Service metadata: name: user-redis-service spec: selector: name : user-redis ports: - protocol: TCP port: 6379 targetPort: 6379 name: user-redis-tcp在创建好 Pod 后再执行 kubectl create -f user-redis-service.yaml 命令即可为 user-redis Pod 生成一个Service。Service 定义了一组 Pod 的逻辑集合和一个用于访问它们的策略Kubernetes集群会为 Service 分配一个固定的Cluster IP用于集群内部的访问。我们可以通过以下命令查看Service的信息包括 Cluster IP 等信息:kubectl get services通过Cluster IP访问MySQL和Redis等服务我们就无须担心PodIP的变化。通过 Pipeline 部署服务到 Kubernetes 集群主要有以下步骤1.从 GitHub 中拉取代码;2.构建Docker 镜像3.上传 Docker 镜像到 Docker Hub;4.将应用部署Kubernetes;5.接口测试。在 Pipeline 中我们将上述步骤组织成相应的 Stage让Jenkins 为我们完成服务的持续集成和自动化测试接下来我们以user服务的部署作为例子。Pipeline 脚本是由Groovy语言实现支持Declarative声明式和 Scripted脚本式语法我们接下来的演示就基于脚本式语法进行介绍。第一步拉取代码。Stage 的声明如下stage(clone code from github) { echo first stage: clone code git url: https://github.com/longjoy/micro-go-course.git script { commit_id sh(returnstdout: true, script: git rev-parse --short HEAD).trim() } }我们通过giturl命令从 GitHub中获取user服务的代码并将本次提交记录的commit_id 提取出来作为变量使用。接下来是第二步使用user服务中的Dockfile定义构建相应的user 镜像。Stage声明如下stage(build image) { echo second stage: build docker image sh docker build -t aoho/user:${commit_id} sectionl1/user/ }为了方便在排查问题时可以根据对应的代码记录定位代码我们采用了GitHub的提交记录commit_id作为镜像的 tag。同时为了将 MySQL 和 Redis 的地址作为参数传入修改 user 服务的 Dockerfile 为如下:FROM golang:latest wORKDIR /root/micro-go-course/sectionl0/user CopY / /root/micro-go-course/sectionl0/user RUN go env -w GOPRoxYhttps://goproxy.cn,direct RUN go build -o user EXP0SE 10086 ENTRYPoINT ./user -mysql.addr $mysqlAddr -redis.addr $redisAddrmysqlAddr 和 redisAddr 将在 user.yaml 配置文件中以环境变量的方式指定 MySQL 和 Redis 的地址。第三步为了方便Kubernetes拉取服务的镜像我们将第二步构建好的Docker镜像推送到镜像仓库中。如下声明所示:stage(push image) { echo third stage: push docker image to registry sh docker login -u eoho -p xxxxxx sh docker push aoho/user:${commit_id} }Docker 中默认的镜像仓库为 Docker Hub上述声明中就将 user 镜像推送到 Docker Hub 中当然你也可以选择将镜像推送到私有仓库中。往DockerHub中推送镜像需要提交账号密码这需要我们预先注册申请一个 Docker Hub账户。然后在第四步中我们使用kubectl将user服务部署到Kubernetes 中。为了保证部署到正确版本的镜像我们需要将 commit_id 替换到 user.yaml 文件中以及将 mysqlAddr 和 redisAddr 作为环境变量输入user.yaml的配置如下:apiversion: apps/v1 kind: Deployment metadata: name: user-service labels: name: user-service spec: replicas: 1 strategy: type: RollingUpdate selector: matchLabels: name: user-service template: metadata: labels: name: user-service spec: containers: #定义User容器开放10086端口 - name: user image: aoho/user:COMMIT_ID_TAG ports: - containerPort: 10086 imagePullpolicy: IfNotPresent env: - name: mysqlAddr value: MYSQL_ADDR_TAG - name: redisAddr value: REDIS_ADDR_TAG在上述配置文件中我们使用DeploymentController 来管理Pod创建Pod的模板为第二步中构建的user 镜像。构建第四步的 Stage 声明如下:stage(deploy to Kubernetes) { echo forth stage: deploy to Kubernetes sh sed -i s/COMMIT_ID_TAG/${commit_id}/ user.yam] sh sed -i s/MYSQL_ADDR_TAG/${mysql_addr}/ user.yam] sh sed -i s/REDIS_ADDR_TAG/${redis_addr}/ user.yaml sh kubectl apply -f user.yaml }在上述声明中我们首先使用 sed 命令将yaml 文件中标识替换为对应的变量再通过 kubectl apply命令重新部署了user-servicePod。为了让user服务的接口在Kubernetes集群外也能够访问我们通过NodePort的方式将user服务的端口暴露到 Node 节点的相应端口定义 user-service.yaml 配置如下:apiversion: v1 kind: Service metadata: name: user-service-http spec: selector: name : user-service type: NodePort ports: - protocol: TCP port: 10086 targetPort: 10086 nodePort: 30036 name: user-service-tcp在上述配置中我们指定的Service的类型为NodePort并将user服务的接口通过Node 节点的30036暴露出去对此我们就可以在集群外部通过NodelP:NodePort的方式访问user服务了。最后一步我们通过gotest对user中的HTTP接口进行接口测试验证代码集成的效果。Stage声明如下:stage(http test) { echo fifth stage: http test sh cd sectionl1/user/transport go test -args ${user_addr} }上述 Stage 中 user_addr 变量即 NodelP:NodePortuser 服务通过 NodePort 暴露到 Kubernetes集群外的可访问端口。我们使用go test命令运行了transport包下的测试文件用于测试HTTP接口。到此我们通过Pipeline对user服务进行持续集成和测试的整个流程就已经完成了其完整的Pipeline脚本如下:node { script { mysql_addr 127.0.0.1 // service cluster ip redis_addr 127.0.0.1 // service cluster ip user_addr 127.0.0.1:30036 // nodeIp : port } stage(clone code from github) { echo first stage: clone code git url: https://github.com/longjoy/micro-go-course.git script { commit_id sh(returnstdout: true, script: git rev-parse --short HEAD).trim() } } stage(build image) { echo second stage: build docker image sh docker build -t aoho/user:${commit_id} sectionl1/user/ } stage(push image) { echo third stage: push docker image to registry sh docker login -u aoho -p xxxxxx sh docker push aoho/user:${commit_id} } stage(deploy to Kubernetes) { echo forth stage: deploy to Kubernetes sh sed -i s/COMMIT_ID_TAG/${commit_id}/ user-service.yaml sh sed -i s/MYSQL_ADDR_TAG/${mysql_addr}/ user-service.yaml sh sed -i s/REDIS_ADDR_TAG/${redis_addr}/ user-service.yaml sh kubectl apply -f user.yaml } stage(http test) { echo fifth stage: http test sh cd sectionl1/user/transport go test -args ${user_addr} } }我们可以在Jenkins 中创建一个 Pipeline任务将上述脚本复制到Script区域中保存后触发构建不过在这之前需要在Jenkins 中安装和配置好 Kubernetes Plugin 和 Docker Plugin。在实际的开发中我们可以将上述Pipeline脚本放入到Jenkinsfile中与代码一同提交到代码库将Pipeline任务的脚本配置类型修改为 Pipeline Script from SCM引I用代码库中 Pipeline 脚本进行构建。下图为在Pipeline 中构建user 服务的结果视图绿色表示该 Stage 执行成功。Pipeline中构建user服务的结果视图小结持续集成和自动化测试能够对开发代码进行快速校验和反馈帮助开发人员更早地发现代码中的集成Bug并进行修改有效提高团队的开发效率。在本文我们主要介绍了如何通过Jenkins对服务进行持续集成和自动化测试。我们借助了JenkinsPipeline的能力把user服务的代码从代码库拉取出来打包成user镜像并将镜像部署到Kubernetes集群最后还通过gotest对user服务中提供的HTTP接口进行测试其实除了手动触发构建外Jenkins中还支持多种触发器比如通过Webhook监听代码库中代码的变化在代码库发生提交或者合并时自动触发一次构建任务这能大大提升持续集成的效率。自动化测试也存在其他多种多样的方式比如借助JMeter和Jenkins对服务进行性能测试等。希望通过本文的学习能够帮助你了解持续集成和自动化测试的基本流程并掌握使用Jenkins进行持续集成的能力。

相关文章:

go实战案例:如何结合 Jenkin 完成持续化集成和自动化测试?

今天我们主要来介绍如何结合Jenkins完成持续化集成和自动化测试的案例。在微服务开发团队中,一般会采用敏捷开发这类增长式的开发方式,这能有效提高各个微服务的迭代效率。为了让完成的代码能够尽快得到反馈,我们建议尽早将完成的代码提交到代…...

SI4735开源库实战指南:从零构建多模式无线电接收系统

SI4735开源库实战指南:从零构建多模式无线电接收系统 【免费下载链接】SI4735 SI473X Library for Arduino 项目地址: https://gitcode.com/gh_mirrors/si/SI4735 SI4735库是一款专为Silicon Labs SI473X系列调谐器芯片设计的开源Arduino库,提供从…...

从同人图到商品图:我是如何用Nano Banana零成本为我的小众手办拍“宣传大片”的

从同人图到商品图:我是如何用Nano Banana零成本为我的小众手办拍“宣传大片”的 作为一名独立手办设计师,我常常面临一个尴尬的困境:精心设计的原创角色,却因为缺乏专业摄影资源而难以展现其魅力。直到我发现Nano Banana这个AI工具…...

HVV 红队攻击入门到精通|一文全覆盖,零基础也能轻松上手,收藏备用

0x00 什么是红队 红队,一般是指网络实战攻防演习中的攻击一方。 红队一般会针对目标系统、人员、软件、硬件和设备同时执行的多角度、混合、对抗性的模拟攻击;通过实现系统提权、控制业务、获取数据等目标,来发现系统、技术、人员和基础架构中存在的网…...

小熊派BearPi-Pico H3863(二)Wi-Fi6与星闪SLE实战开发

1. Wi-Fi6开发实战:从零搭建物联网连接 第一次拿到BearPi-Pico H3863开发板时,最让我惊喜的就是它内置的Wi-Fi6模块。相比传统Wi-Fi4,Wi-Fi6的传输效率提升了近3倍,实测在智能家居多设备场景下延迟能控制在20ms以内。下面分享几个…...

pymavlink实战:从串口通信到UDP消息解析

1. 环境准备与工具安装 第一次接触pymavlink时,我被它强大的硬件通信能力惊艳到了。这个Python库可以让你用几行代码就实现与飞控设备(如Pixhawk)的深度交互。不过在开始实战前,我们需要先准备好开发环境。 我推荐使用Python 3.8版…...

别再为点云数据发愁了!用这个免费GIS工具箱,5分钟把LAS文件变成Web可用的3DTiles

零代码实战:5分钟将无人机点云数据转化为Web三维模型的完整指南 你是否刚拿到无人机航测的LAS数据包,却被领导要求在下午的汇报中展示网页版三维模型?作为测绘或工程行业的从业者,我们常面临这样的紧急需求——无需编程基础&#…...

Chord野生动物监测:濒危物种识别系统

Chord野生动物监测:濒危物种识别系统 1. 引言 在茂密的东北森林深处,一套智能监测系统正在静静守护着珍稀野生动物的安全。当夜幕降临,红外相机捕捉到东北虎矫健的身影;当晨曦微露,系统自动识别出豹猫独特的斑纹。这…...

STM32CubeMX新手避坑指南:从安装到第一个LED闪烁项目

STM32CubeMX新手避坑指南:从安装到第一个LED闪烁项目 第一次接触STM32开发时,面对复杂的外设配置和底层寄存器操作,很多初学者都会感到无从下手。ST公司推出的STM32CubeMX工具正是为了解决这一痛点而生——它通过图形化界面简化了芯片配置流程…...

告别繁琐搬运!4个实用技巧教你将多个文件夹中的图片集中到一个文件夹,新手也能秒会

在日常工作和生活中,我们常常会遇到需要整理分散在多个文件夹中的图片的情况,比如整理旅行照片、项目设计素材或家庭照片等。手动逐个移动图片不仅耗时耗力,还容易遗漏或重复操作。本文将介绍4种简单高效的方法,帮助你快速将多个文…...

2026年3月24日技术资讯洞察:边缘AI商业化,Java26正式发布与开源大模型成本革命

每日精选全网最值得关注的5条技术动态,结合9年Python后端开发经验,为你提供深度解读与实战思考。今日核心要点MWC 2026边缘AI商业化加速:运营商从“卖带宽”转向“卖AI计算能力”,AT&T、T-Mobile等推出AIGrids服务Java 26正式…...

RMBG-2.0镜像免配置亮点:内置Nginx静态资源服务,UI与API同端口暴露

RMBG-2.0镜像免配置亮点:内置Nginx静态资源服务,UI与API同端口暴露 今天要聊的这个工具,绝对能让搞图像处理的朋友眼前一亮。想象一下,你拿到一个功能强大的AI抠图模型,不用再折腾复杂的Web服务器配置,不用…...

一个 MCP,让浏览器变成“自动打工人”

一、背景:为什么需要浏览器自动化? 在日常工作中,我们经常需要做大量重复操作: 登录后台系统填写表单导出数据执行测试流程 传统方案(如 Puppeteer / Playwright)虽然可以解决,但存在明显问题…...

Win11Debloat:让Windows 11重获新生的系统优化方案

Win11Debloat:让Windows 11重获新生的系统优化方案 【免费下载链接】Win11Debloat 一个简单的PowerShell脚本,用于从Windows中移除预装的无用软件,禁用遥测,从Windows搜索中移除Bing,以及执行各种其他更改以简化和改善…...

Umi-OCR Rapid引擎HTTP服务参数配置故障深度解析

Umi-OCR Rapid引擎HTTP服务参数配置故障深度解析 【免费下载链接】Umi-OCR Umi-OCR: 这是一个免费、开源、可批量处理的离线OCR软件,适用于Windows系统,支持截图OCR、批量OCR、二维码识别等功能。 项目地址: https://gitcode.com/GitHub_Trending/um/U…...

MedGemma-X效果实测:看AI如何精准描述肺部结节与解剖结构

MedGemma-X效果实测:看AI如何精准描述肺部结节与解剖结构 1. 引言:当AI学会"看"胸片 在放射科医生的日常工作中,胸片解读是一项基础却至关重要的技能。一张看似简单的X光片,需要医生在几分钟内识别出肺部结节、判断解剖…...

Linux编译报错:libnetcdf.so.19找不到?3种方法快速定位并修复共享库问题

Linux编译报错:libnetcdf.so.19找不到?3种方法快速定位并修复共享库问题 每次在Linux系统上编译完程序,满心欢喜准备运行时,突然跳出一个libnetcdf.so.19: cannot open shared object file: No such file or directory的报错&…...

高德地图MassMarks技术解析:如何高效渲染海量点数据

1. 为什么传统Marker标点会卡顿? 第一次用高德地图JS API开发地图应用时,我兴冲冲地在地图上标注了几百个点位。结果页面直接卡成了PPT,鼠标移动都一顿一顿的。相信很多开发者都遇到过类似问题——当标点数量超过1000个时,页面性能…...

使用Linux系统部署灵毓秀-牧神-造相Z-Turbo的完整指南

使用Linux系统部署灵毓秀-牧神-造相Z-Turbo的完整指南 本文详细讲解如何在Linux服务器上一步步部署灵毓秀-牧神-造相Z-Turbo,从环境准备到最终运行,让你快速上手这个专业的文生图工具。 1. 开始之前:了解你要部署的工具 灵毓秀-牧神-造相Z-T…...

群晖NAS Intel 2.5G网卡驱动终极解决方案:告别网络瓶颈的完整指南

群晖NAS Intel 2.5G网卡驱动终极解决方案:告别网络瓶颈的完整指南 【免费下载链接】synology-igc Intel I225/I226 igc driver for Synology Kernel 4.4.180 项目地址: https://gitcode.com/gh_mirrors/sy/synology-igc 还在为群晖NAS无法识别Intel I225/I22…...

保姆级教程:用SNAP处理Sentinel-1 SAR数据,手把手教你从下载到水稻识别

保姆级教程:用SNAP处理Sentinel-1 SAR数据,从下载到水稻识别的全流程实战 南方多雨地区的农业监测常受云层干扰,而合成孔径雷达(SAR)技术能穿透云雾实现全天候观测。本教程将手把手带您完成Sentinel-1数据的获取、处理…...

【排雷心法】别在 while(1) 里等死了!撕开 HardFault 遮羞布,用 ARM 汇编与堆栈回溯手撕“野指针”真凶

摘要:当 STM32 发生 HardFault 时,系统进入了物理学的“植物人”状态。默认的库函数只提供了一个死循环,掩盖了犯罪现场。本文将带你反思“试错式 Debug”的低效与愚蠢。我们将直视 Cortex-M 内核的异常处理架构,教你如何编写裸汇…...

WindowsCleaner:智能化解救C盘空间危机的全维度解决方案

WindowsCleaner:智能化解救C盘空间危机的全维度解决方案 【免费下载链接】WindowsCleaner Windows Cleaner——专治C盘爆红及各种不服! 项目地址: https://gitcode.com/gh_mirrors/wi/WindowsCleaner 一、空间危机预警:如何识别系统存…...

ServoLight:面向MSP430的超轻量舵机控制库

1. ServoLight 库概述:面向超低资源 MCU 的极简舵机控制方案ServoLight 是一款专为 TI MSP430 系列微控制器(特别是 LaunchPad 开发平台)设计的轻量级舵机(Servo)驱动库,其核心设计哲学是“功能最小化、资源…...

清音刻墨效果惊艳:Qwen3-ForcedAligner在中文四六级口语评分中应用

清音刻墨效果惊艳:Qwen3-ForcedAligner在中文四六级口语评分中应用 1. 引言:口语评分的新突破 中文四六级口语考试一直是很多学生的痛点,传统评分方式主要依赖人工听评,不仅效率低下,还容易受到主观因素影响。想象一…...

ROS2服务通信避坑指南:为什么你的AddTwoInts服务总是连接失败?

ROS2服务通信深度排障:AddTwoInts连接失败的7个关键陷阱与解决方案 在ROS2开发中,服务通信作为核心的请求-响应机制,其稳定性直接影响系统可靠性。但许多开发者在实现类似AddTwoInts的基础服务时,常陷入各种连接失败的困境。本文将…...

ChatTTS插件全解析:如何实现高效自然语音合成与交互

在语音交互应用开发中,我们常常会遇到一个两难的局面:要么追求语音合成的自然度,牺牲响应速度,导致交互体验卡顿;要么为了实时性,使用生硬、机械的合成语音,让用户体验大打折扣。尤其是在客服机…...

基于全阶磁链观测器的无感异步电机矢量控制探索

基于全阶磁链观测器的无感异步电机矢量控制/FFO-FOC/异步电机无感矢量控制/转速辨识 全阶磁链观测器属于一种闭环磁链观测器,根据MRAS进行误差反馈修正估计值,动态和稳态性能有所提高。 全阶磁链观测器的重点在于离散化方法和反馈矩阵的设计,…...

ST7789 IPS屏幕驱动优化与性能提升实战

1. ST7789 IPS屏幕驱动基础解析 ST7789是一款广泛应用于嵌入式系统的IPS液晶屏驱动芯片,支持240x240或240x320分辨率。初次接触这块屏幕时,我发现它虽然引脚众多,但实际需要控制的信号线只有5根:BLK(背光控制&#xff…...

NDK交叉编译工具链实战指南:从配置到运行

1. 为什么需要NDK交叉编译工具链 第一次接触NDK开发时,我完全被交叉编译这个概念搞懵了。为什么不能直接用电脑上的gcc编译代码?后来在实际项目中踩过几次坑才明白,这就像让一个只会说中文的人去教英国人学汉语——必须找个既懂中文又懂英语的…...