【原创系列文章】微信小程序短视频去水印落地全攻略——4.7、知行—代码浅析

1、引言
2、缘起
3、物相
4、知行
4.1、知行—运营策略
4.2、知行—产品设计
4.3、知行—运行环境搭建
4.4、知行—技术后端系分
4.5、知行—Java日志与使用
4.6、知行—踩坑与经验
4.7、知行—代码浅析
5、结语


整体代码框架

本文以抖音短视频的解析功能为例,分析讲解下代码:

前端

代码目录

前端开发参考:https://developers.weixin.qq.com/miniprogram/dev/reference/

1、images:放自己的静态图片文件

2、pages:基于页面分文件,首页、我的页面、历史页

3、utils:工具类

4、app:小程序的启动处理、整体小程序配置相关

整体前端围绕四个文件开发:

文件后缀功能
js核心处理逻辑
json页面全局配置
wxml页面细节布局
wxss自定义样式
细节分析

如首页去水印功能:

1、json

json里:

定义了页面标题、背景色、文字样式

2、wxss

局部细节的样式,如字体大小、颜色、对齐风格等

3、wxml

1、定义视频链接输入框,支持清空

2、用户去水印按钮,点击触发 js 里的 submit 方法

4、js

这里会先判断用户是否已登陆(即已获取到用户的openId),然后调用服务端http接口来去解析,成功会跳转到我的解析记录页面:

后端

代码目录

整体按照标准的 spring 架构分层设计:

画板

LayerBundle功能
bizcontrollerweb请求处理
scheduler定时任务调度
message监听消息处理
serviceimpl门面方法的实现层,涉及TR、RPC接口形式
coremodel领域模型
service核心服务
commondal数据库操作处理
utils工具方法
serviceintegration调用外部系统方法
servicefacade提供门面方法给外部调用
细节分析

画板

1、trace切面

这里用拦截器实现将 TRACE_ID 放入 MDC 的上下文中,方便 logback 日志打印出来

public class TraceInterceptor implements HandlerInterceptor {
    /**
     * 前处理
     *
     * @param request  请求
     * @param response 响应
     * @param handler  处理
     * @return boolean
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        MDC.put(LoggerConstants.TRACE_ID, UUID.randomUUID().toString()
            .replace(CharsetConstants.DASH, StringUtils.EMPTY).toLowerCase());
        return true;
    }

    /**
     * 完成后
     *
     * @param request  请求
     * @param response 响应
     * @param handler  处理程序
     * @param ex       异常
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
                                @Nullable Exception ex) {
        MDC.remove(LoggerConstants.TRACE_ID);
    }
}
2、流程模版

这里提供模版方法供各接口使用,方便统一异常、日志处理;同时模版处理沉淀在core层,方便后续提供TR/RPC给其他系统调用时,不需要更改打印,代码层级等处理

public class ServiceTemplate {
    /**
     * 摘要日志记录器
     */
    private static final Logger DIGEST_LOGGER = LoggerFactory.getLogger(LoggerConstants.CORE_SERVICE_DIGEST_LOGGER);

    /**
     * 日志记录器
     */
    private static final Logger LOGGER = LoggerFactory.getLogger(ServiceTemplate.class);

    /**
     * 执行
     *
     * @param result   结果
     * @param callback 回调
     */
    public static void execute(BaseResult result, ServiceCallback callback) {
        // 获取调用方法名
        String declareMethod = MethodUtil.getPureCallMethodName(ServiceTemplate.class);

        // 计时器
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();

        try {
            // 执行服务校验参数
            callback.checkParameter();

            // 执行服务处理
            callback.process();

            result.setSuccess(true);
        } catch (BizException e) {
            LOGGER.warn(String.format("BizException, (method=%s, errorCode=%s, message=%s)", declareMethod, e.getResultCode(), e.getMessage()), e);
            BaseResult.fillFailureResult(result, ErrorCodeEnum.getByCode(e.getResultCode()), e.getMessage());
        } catch (Throwable e) {
            LOGGER.error(String.format("Throwable, (method=%s, message=%s)", declareMethod, e.getMessage()), e);
            BaseResult.fillFailureResult(result, ErrorCodeEnum.UNKNOWN_EXCEPTION);
        } finally {
            // 执行服务打印
            callback.finalLog();

            // 摘要日志处理
            stopWatch.stop();
            String extraDigestStr = StringUtils.EMPTY;
            List<String> extraDigestLogItemList = callback.extraDigestLogItemList();
            if (!CollectionUtils.isEmpty(extraDigestLogItemList)) {
                extraDigestStr = String.join(CharsetConstants.COMMA, extraDigestLogItemList);
            }
            // 摘要打印:方法名、耗时、是否成功、是否可重试、结果码、额外摘要
            DIGEST_LOGGER.info(String.format("%s|%s|%s|%s|%s|%s", declareMethod, stopWatch.getTotalTimeMillis(),
                result.isSuccess(), result.getErrorCode() == null ? StringUtils.EMPTY : result.isRetryFail(),
                result.getErrorCode() == null ? StringUtils.EMPTY : result.getErrorCode(), extraDigestStr));
        }
    }
}
3、解析视频处理:

1、parserMap 自动注入相关的解析器,key为对应的beanId,value为bean实例

2、Transactional 事务注解,表明两个db的操作是需要同时成功的

@Service
public class VideoServiceImpl implements VideoService {
    private static final Logger LOGGER = LoggerFactory.getLogger(VideoServiceImpl.class);

    /**
     * 解析器map
     */
    @Autowired
    private Map<String, Parser> parserMap;

    /**
     * 用户信息dao
     */
    @Resource
    private UserInfoDAO userInfoDAO;

    /**
     * 解析视频记录dao
     */
    @Resource
    private ParseVideoRecordDAO parseVideoRecordDAO;

    /**
     * 解析视频
     *
     * @param url    url
     * @param userId 用户id
     * @return {@link BaseResult}<{@link Boolean}>
     */
    @Override
    public BaseResult parseVideo(String url, String userId) {
        BaseResult baseResult = new BaseResult<>();
        ServiceTemplate.execute(baseResult, new ServiceCallback() {
            @Override
            public void checkParameter() {
                AssertUtil.assertNotBlank(url, "视频url不能为空");
                AssertUtil.assertNotNull(VideoSourceEnum.getByUrl(url), String.format("此链接目前不支持解析,目前仅支持:%s",
                    Arrays.stream(VideoSourceEnum.values()).map(VideoSourceEnum::getDesc).collect(Collectors.toList())));
                AssertUtil.assertNotBlank(userId, "userId不能为空");
            }

            @Override
            public void process() {
                doParseVideo(url, userId);
            }

            @Override
            public void finalLog() {
                LOGGER.debug(String.format("解析视频,入参[url:%s, userId:%s], 出参[baseResult:%s]", url, userId, baseResult));
            }

            @Override
            public List<String> extraDigestLogItemList() {
                List<String> itemList = new ArrayList<>();
                itemList.add(url);
                itemList.add(userId);
                return itemList;
            }
        });
        return baseResult;
    }

    /**
     * 解析视频
     *
     * @param url    url
     * @param userId 用户id
     */
    private void doParseVideo(String url, String userId) {
        // 1、查询用户数据
        QueryWrapper<UserInfoDO> queryWrapper = new QueryWrapper<>();
        queryWrapper.lambda().eq(UserInfoDO::getUserId, userId);
        UserInfoDO userInfoDO = userInfoDAO.selectOne(queryWrapper);
        AssertUtil.assertNotNull(userInfoDO, ErrorCodeEnum.USER_INFO_NULL);
        AssertUtil.assertTrue(userInfoDO.getAvailableNumber() > 0, ErrorCodeEnum.NONE_AVAILABLE_PARSE_COUNT);

        // 2、视频解析,videoSourceEnum外层保护过了,这里不需要判null
        VideoSourceEnum videoSourceEnum = VideoSourceEnum.getByUrl(url);
        VideoDTO videoDTO = parserMap.get(videoSourceEnum.getParserBeanId()).parseVideo(url);
        AssertUtil.assertNotNull(videoDTO, ErrorCodeEnum.WEB_API_CALL_FAIL);

        // 3、数据事务操作
        userInfoDO.setAvailableNumber(userInfoDO.getAvailableNumber() - 1);
        userInfoDO.setParsedNumber(userInfoDO.getParsedNumber() + 1);

        ParseVideoRecordDO parseVideoRecordDO = new ParseVideoRecordDO();
        parseVideoRecordDO.setUserId(userInfoDO.getUserId());
        BeanUtils.copyProperties(videoDTO, parseVideoRecordDO);

        databaseTransaction(userInfoDO, parseVideoRecordDO);
    }

    /**
     * 数据库事务
     *
     * @param userInfoDO         用户信息
     * @param parseVideoRecordDO 解析视频记录
     */
    @Transactional(timeout = 10)
    public void databaseTransaction(UserInfoDO userInfoDO, ParseVideoRecordDO parseVideoRecordDO) {
        userInfoDAO.updateById(userInfoDO);
        parseVideoRecordDAO.insert(parseVideoRecordDO);
    }
}
4、抖音解析器

1、基于原始链接正则提取出需要的真实链接,如原始链接:“2.89 复制打开抖音,看看【舌尖的作品】羊肉的奶香味与韭花酱的咸香碰撞融合,这一口内蒙古手… https://v.douyin.com/CeiJLLMMS/ sRK:/ 05/27 H@V.yt”,提取出:“https://v.douyin.com/CeiJLLMMS/”

2、headForHeaders 来获取链接请求后的 Location 内容,这是链接对应的 HRAD 请求后资源的重定向地址,可以提取出视频id

3、拼接后 GET 请求:“https://www.iesdouyin.com/share/video/” + videoId,可以获取页面的信息,里面是各种页面 Element 元素,找到需要的视频数据,根据对应路径 json 解析即可,然后视频链接里吧 “playwm” 替换成 “play” 就是无水印的原始视频

@Service
public class DouYinParser extends AbstractParser {
    /**
     * 数据key
     */
    private static final String DATA_KEY = "window._ROUTER_DATA = ";

    /**
     * 数据路径
     */
    private static final String DATA_PATH = "$.loaderData.['video_(id)/page'].videoInfoRes.item_list[0].";

    /**
     * 解析视频
     *
     * @param originalUrl 原始地址
     */
    @Override
    public VideoDTO parseVideo(String originalUrl) {
        // 1、去除掉无用视频前后缀
        String url = fetchTargetUrl(originalUrl, "(https?://v.douyin.com/[\\S]*)");

        // 2、获取重定向后的地址,来获取视频id
        url = restTemplateService.headForHeaders(url).getLocation().toString();
        Matcher matcher = Pattern.compile("/share/video/([\\d]*)[/|?]").matcher(url);
        if (!matcher.find()) {
            throw new BizException(ErrorCodeEnum.ILLEGAL_VIDEO_URL);
        }
        String videoId = matcher.group(1);
        if (StringUtils.isBlank(videoId)) {
            throw new BizException(ErrorCodeEnum.ILLEGAL_VIDEO_URL);
        }

        // 3、http调用api获取结果
        String dyWebApi = "https://www.iesdouyin.com/share/video/" + videoId;
        HttpHeaders httpHeaders = fetchHttpHeaders(dyWebApi, null);
        String content = restTemplateService.fetchHttpEntity(dyWebApi, httpHeaders, HttpMethod.GET, null, String.class);
        if (StringUtils.isBlank(content)) {
            throw new BizException(ErrorCodeEnum.WEB_API_CALL_FAIL);
        }

        // 4、解析出html标签,找到DATA_KEY对应的元素
        Document document = Jsoup.parse(content);
        for (Element element : document.getAllElements()) {
            if (element == null) {
                continue;
            }
            int index = element.data().indexOf(DATA_KEY);
            if (index < 0) {
                continue;
            }
            // 解码
            String decodeContent = element.data().substring(index + DATA_KEY.length());
            // 5、提取出对象
            DocumentContext context = JsonPath.parse(decodeContent);
            VideoDTO videoDTO = new VideoDTO();
            videoDTO.setVideoId(videoId);
            videoDTO.setVideoSource(VideoSourceEnum.DOU_YIN.getCode());
            videoDTO.setVideoOriginalUrl(originalUrl);
            videoDTO.setVideoTitle(context.read(DATA_PATH + "desc"));
            videoDTO.setVideoCover(context.read(DATA_PATH + "video.cover.url_list[0]"));
            videoDTO.setVideoParsedUrl(((String) context.read(DATA_PATH + "video.play_addr.url_list[0]"))
                .replace("playwm", "play"));
            return videoDTO;
        }
        return null;
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值