SpringBoot 仿抖音短视频小程序开发(一):项目的简介( https://segmentfault.com/a/11...
SpringBoot 仿抖音短视频小程序开发(二):项目功能分析与具体实现( https://segmentfault.com/a/11...
源代码: SpringBoot 仿抖音短视频小程序开发 全栈式实战项目( https://gitee.com/scau_zns/sh... )
短视频后台管理系统:( https://gitee.com/scau_zns/sh...
涉及的技术栈:Bootstrap + jQuery + jGrid + SSM框架 + zookeeper
前端代码:
<div class="usersList_wrapper"> <!-- 用户列表展示的表格 --> <table id="usersList"></table> <!-- 底部的分页条 --> <div id="usersListPager"></div> </div>
jGrid发送请求获取数据封装好展示到页面
// 用户列表 var handleList = function() { // 上下文对象路径 var hdnContextPath = $("#hdnContextPath").val(); var apiServer = $("#apiServer").val(); var jqGrid = $("#usersList"); jqGrid.jqGrid({ caption: "短视频用户列表", url: hdnContextPath + "/users/list.action", mtype: "post", styleUI: 'Bootstrap',//设置jqgrid的全局样式为bootstrap样式 datatype: "json", colNames: ['ID', '头像', '用户名', '昵称', '粉丝数', '关注数', '获赞数'], colModel: [ { name: 'id', index: 'id', width: 30, sortable: false, hidden: false }, { name: 'faceImage', index: 'username', width: 50, sortable: false, formatter:function(cellvalue, options, rowObject) { <!-- 配置的虚拟目录apiServer = http://192.168.199.150:8080 --> var src = apiServer + cellvalue; var img = "<img src='" + src + "' width='120'></img>" return img; } }, { name: 'username', index: 'password', width: 30, sortable: false }, { name: 'nickname', index: 'nickname', width: 30, sortable: false }, { name: 'fansCounts', index: 'age', width: 20, sortable: false }, { name: 'followCounts', index: 'sexValue', width: 20, sortable: false }, { name: 'receiveLikeCounts', index: 'province', width: 20, sortable: false, hidden: false } ], viewrecords: true, // 定义是否要显示总记录数 rowNum: 10, // 在grid上显示记录条数,这个参数是要被传递到后台 rownumbers: true, // 如果为ture则会在表格左边新增一列,显示行顺序号,从1开始递增。此列名为'rn' autowidth: true, // 如果为ture时,则当表格在首次被创建时会根据父元素比例重新调整表格宽度。如果父元素宽度改变,为了使表格宽度能够自动调整则需要实现函数:setGridWidth height: 500, // 表格高度,可以是数字,像素值或者百分比 rownumWidth: 36, // 如果rownumbers为true,则可以设置行号 的宽度 pager: "#usersListPager", // 分页控件的id subGrid: false // 是否启用子表格 }).navGrid('#usersListPager', { edit: false, add: false, del: false, search: false }); // 随着窗口的变化,设置jqgrid的宽度 $(window).bind('resize', function () { var width = $('.usersList_wrapper').width()*0.99; jqGrid.setGridWidth(width); }); // 不显示水平滚动条 jqGrid.closest(".ui-jqgrid-bdiv").css({ "overflow-x" : "hidden" }); // 条件查询所有用户列表 $("#searchUserListButton").click(function(){ var searchUsersListForm = $("#searchUserListForm"); jqGrid.jqGrid().setGridParam({ page: 1, url: hdnContextPath + "/users/list.action?" + searchUsersListForm.serialize(), }).trigger("reloadGrid"); }); }
后端获取用户列表分页数据的接口:
@PostMapping("/list") @ResponseBody public PagedResult list(Users user , Integer page) { PagedResult result = usersService.queryUsers(user, page == null ? 1 : page, 10); return result; }
搜索功能的实现:
<!-- 搜索内容 --> <div class="col-md-12"> <br/> <form id="searchUserListForm" class="form-inline" method="post" role="form"> <div class="form-group"> <label class="sr-only" for="username">用户名:</label> <input id="username" name="username" type="text" class="form-control" placeholder="用户名" /> </div> <div class="form-group"> <label class="sr-only" for="nickname">昵称:</label> <input id="nickname" name="nickname" type="text" class="form-control" placeholder="昵称" /> </div> <button id="searchUserListButton" class="btn yellow-casablanca" type="button">搜 索</button> </form> </div>
使用jGrid发送请求给后台
// 条件查询所有用户列表 $("#searchUserListButton").click(function(){ var searchUsersListForm = $("#searchUserListForm"); jqGrid.jqGrid().setGridParam({ page: 1, url: hdnContextPath + "/users/list.action?" + searchUsersListForm.serialize(), }).trigger("reloadGrid"); });
$("#file").fileupload({ pasteZone: "#bgmContent", dataType: "json", done: function(e, data) { console.log(data); if (data.result.status != '200') { alert("长传失败..."); } else { var bgmServer = $("#bgmServer").val(); var url = bgmServer + data.result.data; $("#bgmContent").html("<a href='" + url + "' target='_blank'>点我播放</a>"); $("#path").attr("value", data.result.data); } } });
后台接口保存BGM的方法参考上传头像的方法
参考用户列表信息的分页查询多少
var deleteBgm = function(bgmId) { var flag = window.confirm("是否确认删除???"); if (!flag) { return; } $.ajax({ url: $("#hdnContextPath").val() + '/video/delBgm.action?bgmId=' + bgmId, type: "POST", success: function(data) { if (data.status == 200 && data.msg == 'OK') { alert('删除成功~~'); var jqGrid = $("#bgmList"); jqGrid.jqGrid().trigger("reloadGrid"); } } }) }
var forbidVideo = function(videoId) { var flag = window.confirm("是否禁播"); if (!flag) { return; } $.ajax({ url: $("#hdnContextPath").val() + "/video/forbidVideo.action?videoId=" + videoId, type: "POST", async: false, success: function(data) { if(data.status == 200 && data.msg == "OK") { alert("操作成功"); var jqGrid = $("#usersReportsList"); //reloadGrid是重新加载表格 jqGrid.jqGrid().trigger("reloadGrid"); } else { console.log(JSON.stringify(data)); } } }) }
import org.apache.curator.framework.CuratorFramework; import org.apache.zookeeper.CreateMode; import org.apache.zookeeper.ZooDefs.Ids; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class ZKCurator { //zk客户端 private CuratorFramework client = null; final static Logger log = LoggerFactory.getLogger(ZKCurator.class); public ZKCurator(CuratorFramework client) { this.client = client; } public void init() { client = client.usingNamespace("admin"); try { //判断在admin命名空间下是否有bgm节点 /admin/bgm if( client.checkExists().forPath("/bgm") == null ) { //对于zk来讲,有两种类型的节点,一种是持久节点(永久存在,除非手动删除),另一种是临时节点(会话断开,自动删除) client.create().creatingParentContainersIfNeeded() .withMode(CreateMode.PERSISTENT) //持久节点 .withACL(Ids.OPEN_ACL_UNSAFE) //匿名权限 .forPath("/bgm"); log.info("zookeeper客户端连接初始化成功"); log.info("zookeeper服务端状态:{}",client.isStarted()); } } catch (Exception e) { log.error("zookeeper客户端连接初始化失败"); e.printStackTrace(); } } /** * 增加或者删除Bgm,向zk-server创建子节点,供小程序后端监听 * @param bgmId * @param operType */ public void sendBgmOperator(String bgmId, String operObject) { try { client.create().creatingParentContainersIfNeeded() .withMode(CreateMode.PERSISTENT) //持久节点 .withACL(Ids.OPEN_ACL_UNSAFE) //匿名权限 .forPath("/bgm/" + bgmId, operObject.getBytes()); } catch (Exception e) { e.printStackTrace(); } } }
<!-- 创建重连策列 --> <bean id="retryPolicy" class="org.apache.curator.retry.ExponentialBackoffRetry"> <!-- 每次重试连接的等待时间 --> <constructor-arg index="0" value="1000"></constructor-arg> <!-- 设置最大的重连次数 --> <constructor-arg index="1" value="5"></constructor-arg> </bean> <!-- 创建zookeeper客户端 --> <bean id="client" class="org.apache.curator.framework.CuratorFrameworkFactory" factory-method="newClient" init-method="start"> <constructor-arg index="0" value="120.79.18.35:2181"></constructor-arg> <constructor-arg index="1" value="10000"></constructor-arg> <constructor-arg index="2" value="10000"></constructor-arg> <constructor-arg index="3" ref="retryPolicy"></constructor-arg> </bean> <!-- 调用init方法启动 --> <bean id="ZKCurator" class="com.imooc.web.util.ZKCurator" init-method="init"> <constructor-arg index="0" ref="client"></constructor-arg> </bean>
@Autowired private ZKCurator zKCurator; @Override public void addBgm(Bgm bgm) { String id = sid.nextShort(); bgm.setId(id); bgmMapper.insert(bgm); Map<String, String> map = new HashMap<>(); map.put("operType", BGMOperatorTypeEnum.ADD.type); map.put("path", bgm.getPath()); zKCurator.sendBgmOperator(id, JSONUtils.toJSONString(map)); } @Override public void deleteBgm(String id) { Bgm bgm = bgmMapper.selectByPrimaryKey(id); bgmMapper.deleteByPrimaryKey(id); Map<String, String> map = new HashMap<>(); map.put("operType", BGMOperatorTypeEnum.DELETE.type); map.put("path", bgm.getPath()); zKCurator.sendBgmOperator(id, JSONUtils.toJSONString(map)); }
private CuratorFramework client = null; final static Logger log = LoggerFactory.getLogger(ZKCuratorClient.class); // public static final String ZOOKEEPER_SERVER = "120.79.18.36:2181"; public void init() { if(client != null) { return; } //重试策略 RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 5); //创建zk客户端 120.79.18.36:2181 client = CuratorFrameworkFactory.builder().connectString(resourceConfig.getZookeeperServer()).sessionTimeoutMs(10000) .retryPolicy(retryPolicy).namespace("admin").build(); //启动客户端 client.start(); try { addChildWatch("/bgm"); } catch (Exception e) { e.printStackTrace(); } }
public void addChildWatch(String nodePath) throws Exception { final PathChildrenCache cache = new PathChildrenCache(client, nodePath, true); cache.start(); cache.getListenable().addListener(new PathChildrenCacheListener() { @Override public void childEvent(CuratorFramework client, PathChildrenCacheEvent event) throws Exception { if(event.getType().equals(PathChildrenCacheEvent.Type.CHILD_ADDED)){ log.info("监听到事件CHILD_ADDED"); //1. 从数据库查询bgm对象,获取路径Path String path = event.getData().getPath(); String operatorObjStr = new String(event.getData().getData()); Map<String, String> map = JsonUtils.jsonToPojo(operatorObjStr, Map.class); String operatorType = map.get("operType"); String songPath = map.get("path"); // String[] arr = path.split("/"); // String bgmId = arr[arr.length-1]; // Bgm bgm = bgmService.queryBgmById(bgmId); // if(bgm == null){ // return; // } //1.1 bgm所在的相对路径 // String songPath = bgm.getPath(); //2. 定义保存到本地的bgm路径 // String filePath = "E://imooc_videos_dev" + songPath; String filePath = resourceConfig.getFileSpace() + songPath; //3. 定义下载的路径(播放url) String[] arrPath = songPath.split("////"); //windows // String[] arrPath = songPath.split("/"); //linux String finalPath = ""; //3.1 处理url的斜杠以及编码 for(int i=0; i<arrPath.length;i++){ if(StringUtils.isNotBlank(arrPath[i])) { finalPath += "/"; finalPath += URLEncoder.encode(arrPath[i], "UTF-8"); } } // String bgmUrl = "http://192.168.199.150:8080/mvc" + finalPath; String bgmUrl = resourceConfig.getBgmServer() + finalPath; if(operatorType.equals("1")){ //下载bgm到springboot服务器 URL url = new URL(bgmUrl); File file = new File(filePath); FileUtils.copyURLToFile(url, file); client.delete().forPath(path); }else if(operatorType.equals("2")){ File file = new File(filePath); FileUtils.forceDelete(file); client.delete().forPath(path); } } } }); }