Go-zero中分布式事务的实现(DTM分布式事务管理器,在一个APi中如何调用两个不同服务的rpc层,并保证两个不同服务之间的业务逻辑同时成功)

涉及到的相关技术

     1.DTM分布式事务管理器,解决跨数据库、跨服务、跨语言栈更新数据的一致性问题。

      2.SAGA事务模式,SAGA事务模式是DTM中常用的一种模式,简单易上手.(当然还有其它更多的事务模式,这里采用的SAGA只不过是其中一种较为简单的方法)

      3.Go-zero框架,ETCD服务注册...

更多内容移步至:go-zero 缩短从需求到上线的距离 和 介绍 | DTM开源项目文档

业务场景

        如果是在单体架构的业务当中,是不需要用到分布式事务的.单体架构中,涉及到需要保证多个事务同时成功的场景,只需要创建一个全局的事务对象 如:tx := db.Begin(),然后统一用这一个tx去管理接下来的业务逻辑即可.

        不清楚在一个api中如何调用其它服务rpc的可以看看我的另一篇博客中的一种解决办法:

go-zero标准的项目结构,以及如何使用docker-compose部署道linux服务器上-CSDN博客

        但是在go-zero框架的这种微服务中,比如说:我在一个用户服务的api中调用了用户服务rpc中注册的业务,并且同时还调用了标签服务的rpc层中的选择标签的业务. 那么,此时我就需要保证用户的注册和标签的选择这两个在不同服务下执行的业务逻辑同时成功.(总不能用户账号密码插入到的表中,但是突然断网了,导致标签没有选择上去吧,这个是不符合我的业务的).

DTM 环境搭建(Windows本地搭建)

        !!!!!!!!!!!!!! 这个环境请注意,是需要在你本地去搭建的,至于为什么,我会在后面解释,最重要的先把环境搭建起来吧! 我采用的是docker-compose去搭建.(如果不了解windows电脑如何配置docker环境,可以移步:)

Windows11电脑是如何搭建docker环境的-CSDN博客

        废话不多说,首先从搭建环境讲起.(我这里采用的是docker-compose搭建我需要的环境)

上图就是项目的结构

在dtm和etcd的目录下面各自新建一个Dockerfile文件,Dockerfile都不需要过多的配置,只需要用到最基础的镜像即可.在dtm的目录下还需要新建一个config.yml文件.

DTM下Dokcerfile以及config.yml的编写

FROM yedf/dtm:latest


LABEL maintainer="zyf021026 <shichuxin6@163.com>"
# 指定要存储trans状态的存储驱动
# Store:

### 默认存储驱动
#   Driver: 'boltdb'

### redis 存储驱动
#   Driver: 'redis'
#   Host: 'localhost'
#   User: ''
#   Password: ''
#   Port: 6379

### mysql 存储驱动
#   Driver: 'mysql'
#   Host: 'mysql'
#   User: 'root'
#   Password: '123456'
#   Port: 3306

### postgres 存储驱动
#   Driver: 'postgres'
#   Host: 'localhost'
#   User: 'postgres'
#   Password: 'mysecretpassword'
#   Port: '5432'

### 以下配置仅适用于 postgres/mysql 驱动
#   MaxOpenConns: 500
#   MaxIdleConns: 500
#   ConnMaxLifeTime: 5
#   TransGlobalTable: 'dtm.trans_global'
#   TransBranchOpTable: 'dtm.trans_branch_op'

### 以下配置仅适用于 redis/boltdb 驱动
#   DataExpire: 604800 # Trans 过期时间
#   RedisPrefix: '{}'  # Redis 存储前缀



MicroService:
  Driver: 'dtm-driver-gozero'           # 要处理注册/发现的驱动程序的名称
  Target: 'etcd://your-ip:2379/dtmservice' # 注册 dtm 服务的 etcd 地址
  EndPoint: 'your-ip:36790'

# 以下配置的单位为'秒'
# TransCronInterval: 3
# TimeoutToFail: 35
# RetryInterval: 10

# 日志等级
# LogLevel: 'info'

ETCD的Dockerfile文件编写

FROM bitnami/etcd:latest

LABEL maintainer="zyf021026 <shichuxin6@163.com>"

使用docker-compose 构建镜像,启动容器


version: '3'

networks:
  backend:
    driver: bridge


######## 项目依赖的环境,启动项目之前要先启动此环境 #######
services:
  etcd:
    build:
      context: etcd
    environment:
      - TZ=Asia/Shanghai
      - ALLOW_NONE_AUTHENTICATION=yes
    ports: # 设置端口映射
      - "2379:2379"
    networks:
      - backend
    restart: always
  dtm:
    build:
      context: ./dtm
    environment:
      - TZ=Asia/Shanghai
    entrypoint:
      - "/app/dtm/dtm"
      - "-c=/app/dtm/configs/config.yaml"
    privileged: true
    volumes:
      - ./dtm/config.yml:/app/dtm/configs/config.yaml # 将 dtm 配置文件挂载到容器里
    ports:
      - "36789:36789"
      - "36790:36790"
    networks:
      - backend
    restart: always
    depends_on:
      - etcd

   在根目录下面执行docker-compose up -d 将需要的环境搭建起来

执行如下图中的命令:

        新建子事务屏障的数据库(库名和表名请不要修改) 可以作为独立的一个数据库使用,没有必要把自己的项目数据库名称改为dtm_barrier

/*
 Navicat Premium Data Transfer

 Source Server         : Link
 Source Server Type    : MySQL
 Source Server Version : 50743
 Source Host           : 39.101.77.206:3306
 Source Schema         : dtm_barrier

 Target Server Type    : MySQL
 Target Server Version : 50743
 File Encoding         : 65001

 Date: 03/03/2024 13:57:48
*/

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for barrier
-- ----------------------------
DROP TABLE IF EXISTS `barrier`;
CREATE TABLE `barrier`  (
  `id` bigint(22) NOT NULL AUTO_INCREMENT,
  `trans_type` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '',
  `gid` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '',
  `branch_id` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '',
  `op` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '',
  `barrier_id` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '',
  `reason` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT 'the branch type who insert this record',
  `create_time` datetime NULL DEFAULT CURRENT_TIMESTAMP,
  `update_time` datetime NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE INDEX `gid`(`gid`, `branch_id`, `op`, `barrier_id`) USING BTREE,
  INDEX `create_time`(`create_time`) USING BTREE,
  INDEX `update_time`(`update_time`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1482 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

SET FOREIGN_KEY_CHECKS = 1;

SAGA事务模式的使用

        简单说明一下,SAGA分布式事务模式,是没有办法携带返回值的,因此尽量此处要避免需要有返回值的业务场景.

        直接用代码来展示SAGA事务模式的使用方法吧!

    用户注册服务PRC的编写以及事务失败补偿机制的编写

这里不再演示proto文件是如何编写的

用户注册服务的rpc

func (l *UserCreateLogic) UserCreate(in *user.UserCreateRequest) (pd *user.UserCreateResponse, endErr error) {
	// 获取 RawDB
	// 注册
	db, err := sqlx.NewMysql(l.svcCtx.Config.Mysql.DataSource).RawDB()
	// 获取子事务屏障对象
	barrier, err := dtmgrpc.BarrierFromGrpc(l.ctx)
	if err != nil {
		return nil, status.Error(500, err.Error())
	}
	// 开启子事务屏障
	err = barrier.CallWithDB(db, func(tx *sql.Tx) error {
		// 加密密码
		pwd, _ := bcrypt.GetPwd(in.Password)
		// 插入用户数据
		_, err = tx.Exec("INSERT INTO users (id , created_at, updated_at, username, password, avatar, phone) VALUES (?,?, ?, ?, ?, ?, ?)", in.Id, time.Now(), time.Now(), in.Username, pwd, in.Avatar, in.Phone)
		//返回子事务执行失败
		if err != nil {
			return err
		}
		return nil
	})

	if err != nil {
		return nil, status.Error(codes.Aborted, dtmcli.ResultFailure) //如果失败,不再重试,直接回滚
	}
	return &user.UserCreateResponse{}, endErr
}

用户注册服务rpc的失败补偿 (如果注册服务的rpc失败,就会执行相应的补偿方法)

func (l *UserCreateRevertLoginLogic) UserCreateRevertLogin(in *user.UserCreateRequest) (pd *user.UserCreateResponse, err error) {
	fmt.Println("用户标签回滚开始--->")
	// 获取 RawDB
	db, err := sqlx.NewMysql(l.svcCtx.Config.Mysql.DataSource).RawDB()
	// 获取子事务屏障对象
	barrier, err := dtmgrpc.BarrierFromGrpc(l.ctx)
	if err != nil {
		return nil, status.Error(500, err.Error())
	}
	// 开启子事务屏障
	err = barrier.CallWithDB(db, func(tx *sql.Tx) error {
		fmt.Println("注册事务走入了补偿")
		//删除插入的标签数据 和 用户数据
		_, err = tx.Exec("DELETE FROM tb_user_tag where user_id = ?", in.Id)
		_, err = tx.Exec("DELETE FROM users where id = ?", in.Id)
		//返回子事务执行失败
		if err != nil {
			return err
		}
		return nil
	})
	if err != nil {
		fmt.Println("failed---->", err)
		return nil, err
	}
	fmt.Println("删除成功")
	fmt.Println("用户标签回滚结束--->")
	return &user.UserCreateResponse{}, nil
}

        标签服务Rpc的编写以及事务失败补偿机制的编写

标签服务的rpc

func (l *SignUserChooseTagLogic) SignUserChooseTag(in *tag.UserChooseTagRequest) (*tag.UserChooseTagRequest, error) {
	// 获取 RawDB
	// 注册账号时,选择标签
	db, err := sqlx.NewMysql(l.svcCtx.Config.Mysql.DataSource).RawDB()
	if err != nil {
		return nil, status.Error(500, err.Error())
	}
	// 获取子事务屏障对象
	barrier, err := dtmgrpc.BarrierFromGrpc(l.ctx)
	// 开启子事务屏障
	err = barrier.CallWithDB(db, func(tx *sql.Tx) (err error) {
		// 用户注册时选择标签
		var exists bool
		err = tx.QueryRow("SELECT EXISTS(SELECT 1 FROM tb_user_tag WHERE tag_id = ? and user_id = ?)", in.TagId, in.UserId).Scan(&exists)
		if err != nil {
			return err
		}
		if exists {
			return fmt.Errorf("标签重复选择")
		}
		fmt.Println("开始插入标签")
		_, err = tx.Exec("INSERT INTO tb_user_tag (tb_user_tag.created_at , tb_user_tag.updated_at , tag_id, user_id) VALUES (?,?,?, ?)", time.Now(), time.Now(), in.TagId, in.UserId)
		if err != nil {
			return fmt.Errorf("标签选择失败")
		}
		return nil
	})
	if err != nil {
		return nil, status.Error(codes.Aborted, dtmcli.ResultFailure) //事务失败不再重试,直接回滚
	}
	return &tag.UserChooseTagRequest{}, nil
}

标签选择失败补偿的rpc

func (l *SignUserChooseTagRevertLogic) SignUserChooseTagRevert(in *tag.UserChooseTagRequest) (*tag.UserChooseTagRequest, error) {

	fmt.Println("用户标签SignUserChooseTagRevert--->开始")
	// 获取 RawDB
	db, err := sqlx.NewMysql(l.svcCtx.Config.Mysql.DataSource).RawDB()
	if err != nil {
		return nil, status.Error(500, err.Error())
	}
	// 获取子事务屏障对象
	barrier, err := dtmgrpc.BarrierFromGrpc(l.ctx)
	// 开启子事务屏障
	err = barrier.CallWithDB(db, func(tx *sql.Tx) (err error) {
		fmt.Println("注册时选择标签进入了补偿")
		logc.Info(l.ctx)
		//删除记录
		_, err = tx.Exec("DELETE FROM tb_user_tag where tag_id = ? and user_id = ?", in.TagId, in.UserId)
		return err
	})
	if err != nil {
		return nil, err
	}
	fmt.Println("用户标签SignUserChooseTagRevert--->结束")

	return &tag.UserChooseTagRequest{}, nil
}

至此rpc层的业务逻辑全部编写完毕,但请一定要注意每一个rpc的返回值,一定要按照 如&tag.UserChooseTagRequest{}返回,不能简单的返回一个nil值.否则会导致事务一直无法提交

        API层的编写


func (l *SignUpLogic) SignUp(req *types.UserCreateRequest) (resp *types.UserCreateResponse, err error) {
	//首先判断用户是否存在
	_, err = l.svcCtx.UserRpc.UserIsExists(l.ctx, &user.UserCreateRequest{
		Phone: req.Phone,
	})
	if err != nil {
		return nil, err
	}
	// 获取UserRpc 的BuildTarget
	userRpcBuildServer, err := l.svcCtx.Config.UserRpc.BuildTarget()
	if err != nil {
		return nil, status.Error(100, "用户注册异常")
	}
	// 获取TagRpc 的BuildTarget
	tagRpcBuildServer, err := l.svcCtx.Config.TagRpc.BuildTarget()
	if err != nil {
		return nil, status.Error(100, "标签选择异常")
	}
	empty := user.Empty{}
	//dtm服务的etcd注册地址
	var dtmServer = l.svcCtx.Config.Dtm
	//dtmServer := "etcd://etcd:2379/dtmservice"
	fmt.Println(dtmServer)
	// 创建一个gid
	gid := dtmgrpc.MustGenGid(dtmServer)
	//创建一个自增id
	if _, err := l.svcCtx.UserRpc.AddUserId(l.ctx, &empty); err != nil {
		return nil, fmt.Errorf("CREATE user id error:%v", err)
	}
	userID, _ := l.svcCtx.UserRpc.NextUserID(l.ctx, &empty)

	saga := dtmgrpc.NewSagaGrpc(dtmServer, gid).Add(tagRpcBuildServer+"/tag.TagSign/SignUserChooseTag", tagRpcBuildServer+"/tag.TagSign/SignUserChooseTagRevert", &tag.UserChooseTagRequest{
		UserId: userID.NextUserId,
		TagId:  req.StartTagId,
	}).Add(userRpcBuildServer+"/user.UserService/UserCreate", userRpcBuildServer+"/user.UserService/UserCreateRevertLogin", &user.UserCreateRequest{
		Username: req.Username,
		Password: req.Password,
		Avatar:   req.Avatar,
		Phone:    req.Phone,
		Id:       userID.NextUserId,
	})
	//事务提交
	if err := saga.Submit(); err != nil {
		//自增主键减少1
		if _, err := l.svcCtx.UserRpc.DecUserID(l.ctx, &empty); err != nil {
			logx.Error(err)
		}
		logx.Error(err)
		return nil, fmt.Errorf("saga submit error:%v", err)
	}
	return &types.UserCreateResponse{}, nil
}

上面代码的逻辑,相信如果各位接触到微服务,一定是可以理解的,由于saga的事务模式没有返回值,所以我通过redis生成一个自增id来使用,而不再采用mysql的自增主键id.

上面代码中的地址可以在rpc生成的pb.go中找到

自己的理解

        经历了长度一周多对分布式事务的研究,写一点自己的简单理解吧!(比较浅显)

        saga事务模式需要自己写事务的补偿方法,子事务屏障内的事务执行失败之后,就会执行对应的事务补偿方法!即回滚事务.补偿方法内写的便是对这一次执行的插入,修改语句的相反操作.比如我增加某一条数据,补偿内就写上对删除的操作.

        感觉和MySQL的Undo log 回滚日志很相似啊!Undo log日志会记录更新前的数据到日志中,是在一个事务下执行过程中,在还没有提交之前,如果发生意外,就可以通过这个日志回滚到事务执行之前的数据了.

        

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

虾皮、lazada店铺运营攻略,如何搭建高效、稳定的自养号测评系统

随着电子商务的蓬勃发展&#xff0c;越来越多的人选择在虾皮这样的电商平台上开设店铺&#xff0c;以实现创业梦想。但如何在众多店铺中脱颖而出&#xff0c;成为消费者的首选?本文将为您详细解答“怎么样做好虾皮店铺”&#xff0c;并提供一些实用的运营建议。 一、怎么样做…

Java工作需求后端代码--实现树形结构

加油&#xff0c;新时代打工人&#xff01; 前端页面 带树形结构的表格 最近在新项目上加班加点&#xff0c;下面是个实现树形结构的数据表格。 需求&#xff1a; 在前端页面表格中展示成树形结构的数据。 技术&#xff1a; 后端&#xff1a;Java、Mybatis-Plus、HuTool树形的…

b站小土堆pytorch学习记录—— P18-P22 神经网络+小实战

文章目录 一、卷积层 P181.卷积操作2.代码 二、池化层 P191.池化层简单介绍2.代码&#xff08;1&#xff09;池化操作中数字的变化&#xff08;2&#xff09;池化操作对图片的影响 三、非线性激活 P201.简要介绍2.代码 四、线性层及其他层介绍 P211.线性层2.代码 五、搭建小实战…

docker安装和使用kafka

1. 启动zookeeper Kafka依赖zookeeper, 首先安装zookeeper -p&#xff1a;设置映射端口&#xff08;默认2181&#xff09; docker run --name zookeeper \--network app-tier \-e ALLOW_ANONYMOUS_LOGINyes \--restartalways \-d bitnami/zookeeper:latest2. 启动kafka docker…

实验七 综合实验

一&#xff0e; 下载并成功运行Anaconda,jupyter book ,spyder 输入检验(print (“hello”)) 二&#xff0e; 在jupyter prompt中安装库&#xff1a; 找到anaconda 的Scripts库&#xff0c;并复制路径以备后面安装命令 D:\Program Files\anaconda3\Scripts 进入prompt命令…

网络原理初识

一、IP地址 概念 IP 地址主要用于标识网络主机、其他网络设备&#xff08;如路由器&#xff09;的网络地址。简单说&#xff0c; IP 地址用于定位主机 的网络地址 。 就像我们发送快递一样&#xff0c;需要知道对方的收货地址&#xff0c;快递员才能将包裹送到目的地。 二、…

搭建的svn 1.14.1,拉取代码时候没输入账户密码就报错 auth failed

这边在ubuntu里面搭的svn server&#xff0c;但是拉代码的是否一直报错 auth faield&#xff0c;一开始以为是有auth cache&#xff0c;去设置里面清楚了&#xff0c;windows 里面也清楚了&#xff0c;但是还是报错 问题原因 一直排查才发现&#xff0c;我新增用户的时候&…

vue+uniapp实现图形验证码功能-插件(附源码)

一、需求背景 vueuniapp实现图形验证码功能-插件&#xff08;附源码&#xff09; 在登录系统时&#xff0c;除了密码登录&#xff0c;还需要提供验证码登录。 涉及验证码&#xff0c;为了安全&#xff0c;一般会加入图形验证码&#xff0c;然后再发短信验证码。 如图&#xff1…

为国产信创服务器提供LDAP统一身份认证方案

金融信创作为 8 大行业信创之首&#xff0c;早已成为其他行业信创建设的参考。金融行业有着极为复杂的业务场景&#xff0c;对系统有着极高的稳定可靠需求&#xff0c;因此&#xff0c;在寻找微软 AD 国产化替代方案时&#xff0c;常会涉及到更深层次的场景。例如&#xff0c;最…

计算机大数据毕业设计-基于Flask的旅游推荐可视化系统的设计与实现

基于Flask的旅游推荐可视化系统的设计与实现 编程语言&#xff1a;Python3.10 涉及技术&#xff1a;FlaskMySQL8.0Echarts 开发工具&#xff1a;PyCharm 摘要&#xff1a;以Pycharm为旅游推荐系统开发工具&#xff0c;采用B/S结构&#xff0c;使用Python语言开发旅游景点推…

电网数字孪生的开发框架

电网数字孪生的开发框架通常会综合利用多种技术和工具&#xff0c;包括数据处理、模型建立、仿真与优化等方面的工具和平台。以下是一些常用的开发框架&#xff0c;希望对大家有所帮助。北京木奇移动技术有限公司&#xff0c;专业的软件外包开发公司&#xff0c;欢迎交流合作。…

网络安全: Kali Linux 进行 SSH 渗透与防御

目录 一、实验 1.环境 2.nmap扫描目标主机 3.Kali Linux 进行 SSH 渗透 3.Kali Linux 进行 SSH 防御 二、问题 1.SSH有哪些安全配置 一、实验 1.环境 &#xff08;1&#xff09;主机 表1 主机 系统版本IP备注Kali Linux2022.4 192.168.204.154&#xff08;动态&…

【JavaScript 漫游】【031】window 对象总结

文章简介 本篇文章为【JavaScript 漫游】专栏的第 030 篇文章&#xff0c;记录了浏览器模型中 window 对象的相关知识点。 window 对象概述 浏览器里面&#xff0c;window 对象&#xff08;注意&#xff0c;w 为小写&#xff09;指当前的浏览器窗口。它也是当前页面的顶层对…

Java中线程操作的相关方法

当涉及到在Java中操作线程时&#xff0c;有许多内置的类和方法可供使用。下面是关于Java中线程操作的主要方法和技术的简要教程&#xff1a; 1. 创建线程 在Java中&#xff0c;有两种主要的方式来创建线程&#xff1a; - 继承 Thread 类并重写其 run() 方法。 - 实现 Runna…

深入探讨javascript的流程控制与分支结构,以及js的函数

✨✨ 欢迎大家来到景天科技苑✨✨ &#x1f388;&#x1f388; 养成好习惯&#xff0c;先赞后看哦~&#x1f388;&#x1f388; 所属的专栏&#xff1a;前端泛海 景天的主页&#xff1a;景天科技苑 文章目录 1.流程控制与分支结构1.if分支结构2.switch case 分支结构3.循环结…

C++消息队列处理提高性能的方法

1 消息队列特点 在当前多数软件系统中,处理传递消息多用消息队列机制,他具有以下优点: 1. 异步通信:消息队列支持异步通信,发送者和接收者之间的解耦程度较高。发送者将消息放入队列后即可继续执行,而不需要等待接收者的响应。这样可以提高系统的吞吐量和处理能力。 2.…

删除指定的数

删除指定的数 题目描述&#xff1a;解法思路&#xff1a;解法代码&#xff1a;运行结果&#xff1a; 题目描述&#xff1a; 先输入10个整数存放在数组中&#xff0c;再输入⼀个整数n&#xff0c;删除数组中所有等于n的数字&#xff0c;数组中剩余的数组保证数组的最前面&#…

电脑键盘快捷键,掌握这些,快速提高效率!

“我是一名电脑新手&#xff0c;在使用电脑时还有很多不懂的。想问问大家平常有什么比较好用的电脑键盘快捷键可以推荐吗&#xff1f;” 在数字化时代&#xff0c;电脑已成为我们生活与工作中不可或缺的工具。掌握一些常用的电脑键盘快捷键&#xff0c;不仅能提高我们的工作效率…

[备赛笔记]——5G大唐杯(5G考试等级考考试基础试题)

个人名片&#xff1a; &#x1f981;作者简介&#xff1a;学生 &#x1f42f;个人主页&#xff1a;妄北y &#x1f427;个人QQ&#xff1a;2061314755 &#x1f43b;个人邮箱&#xff1a;2061314755qq.com &#x1f989;个人WeChat&#xff1a;Vir2021GKBS &#x1f43c;本文由…

学生信息管理展示-h5版(uniapp+springboot+vue)

记录一下做的第一个完整的h5业务。 一、登录 二、个人中心 三、首页&#xff08;管理员&#xff09; 四、首页&#xff08;学生&#xff09; 五、视频展示 学生信息管理展示&#xff08;h5&#xff09;完整版