SpringBoot 开发抖音开放平台获取用户的粉丝统计和短视频数据

image.png

最近有朋友问起我有没有做过抖音开放平台,让我有了些思考,其实之前做过的。虽然抖音APP很火,但是毕竟不像微信开放平台那样,已沉淀多年,基本上每个API只要肯用心查找,网上都有很多资料可以参考。而抖音开放平台则不然,刚面世不久,资料比较少。即使对于一个开发人员来说,接入第三方接口都大同小异,不会太难,但我还是想把这些记录下来,特别是遇到的坑,会列在下面,一起参考学习。限于水平有限,若有错误,不吝赐教哈。那么,我们就开始正文吧。

1、注册账号

抖音开放平台地址:https://open.douyin.com/platform

image.png

无独有偶,和其他第三方平台一样,进入开放平台注册账号后登录,平台会审核提交的信息,审核过了再创建应用(如果审核不过,是不让你创建应用的)。

2、创建应用

我们获取数据一般是用来做PC网站的,所以选择网站应用来创建,如实填写信息,等待审核。这里吐槽一下,和微信相比,抖音不管是小程序还是开放平台,审核的速度很慢,虽然在提交完信息后平台一般会提示三个工作日内审核,但是你可能还是要发邮件过去催。这里给大家看一下审核通过后的应用。


image.png

点击详情进入,看到如下内容,我们有了平台颁发的Client Key和Client Secret就可以开始撸代码了。


image.png

3、实现思路

也没什么特别的思路啦,就是引导用户扫描我们接入的二维码,用户在抖音APP端扫码确认或账号密码授权登录后,会重定向到我们的回调接口,并且附带授权临时票据(code),我们拿着code,以及ClientKey和ClientSecret等参数,通过API换取access_token,然后就可以通过access_token进行接口调用,获取用户基本信息及其他操作等。大致的流程就是这样子,接下来我们就来看一下实现的一些细节。

4、开发细节

4.1、选择资源中心 -> Open Api -> 账号授权及绑定 查看接口文档,


image.png

4.2、用户扫码授权,回调我们的接口,拿到code,再调用获取access_token的接口,也可以拿到用户对应的open_id,因为access_token是有时效性的,所以我们要做缓存,要在过期前先用refresh_token刷新延长access_token的有效期,又过期后只能让用户重新授权。

授权相关的service

private static final Logger logger = LoggerFactory.getLogger(OauthServiceImpl.class);
    private static final String OAUTH_STATE_SESSION_KEY = "OAUTH_STATE_SESSION_KEY";
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Value("${redis.key.douyinTokenKeyPrefix}")
    private String douyinTokenKeyPrefix;
    @Value("${redis.key.douyinRefreshTokenKeyPrefix}")
    private String douyinRefreshTokenKeyPrefix;
    @Value("${redis.key.douyinClientTokenKey}")
    private String douyinClientTokenKey;

     /**
     * 获取授权码(code)
     * @param clientKey
     * @param redirectUri
     * @param state
     * @return
     */
    @Override
    public String qrcodeAuth(String clientKey,String redirectUri,String state) {
        String requestUrl = Urls.BASE_URL+String.format(Urls.PERSON_CONNECT_URL,clientKey,redirectUri,state);    
        ShiroUtils.setSessionAttribute(OAUTH_STATE_SESSION_KEY,state);
        logger.info("qrConnect requestUrl=" + requestUrl);
        return requestUrl;
    }

    /**
     * 获取access_token
     * @param request
     * @param clientKey
     * @param clientSecret
     * @return
     */
    @Override
    public TokenResult accessToken(HttpServletRequest request,String clientKey,String clientSecret) {
        String code = request.getParameter("code");
        String state = request.getParameter("state");
        Object sessionState = SecurityUtils.getSubject().getSession().getAttribute(OAUTH_STATE_SESSION_KEY);
        TokenResult token = new TokenResult();
        //校验state
        if (sessionState != null && state.equalsIgnoreCase(sessionState.toString())) {
            SecurityUtils.getSubject().getSession().removeAttribute(OAUTH_STATE_SESSION_KEY);
            String requestUrl = Urls.BASE_URL+String.format(Urls.ACCESS_TOKEN_URL,clientKey,clientSecret,code);
            JSONObject response = (CommonUtil.httpsRequestJson(requestUrl, "GET", null));
            JSONObject object = response.getJSONObject("data");
            logger.info("accessToken result=" + response);
            int errorCode = object.getInteger("error_code");
            String description = object.getString("description");
            if (errorCode == 0) {
                token.setErrorCode(0);
                token.setAccessToken(object.getString("access_token"));
                token.setExpiresIn(object.getInteger("expires_in"));
                token.setRefreshToken(object.getString("refresh_token"));
                token.setOpenId(object.getString("open_id"));
                token.setScope(object.getString("scope"));
            } else {
                token.setErrorCode(errorCode);
                token.setDescription(description);
            }
        } else {
            token.setErrorCode(500);
            token.setDescription("state校验失败");
        }
        return token;
    }

    /**
     * 刷新access_token
     * @param clientKey
     * @param refreshToken
     * @return
     */
    @Override
    public TokenResult refreshToken(String clientKey,String refreshToken) {
        String requestUrl = Urls.BASE_URL+String.format(Urls.REFRESH_TOKEN_URL,clientKey,refreshToken);
        JSONObject response = (CommonUtil.httpsRequestJson(requestUrl, "GET", null));
        JSONObject object = response.getJSONObject("data");
        logger.info("refreshToken result=" + response);
        int errorCode = object.getInteger("error_code");
        String description = object.getString("description");
        TokenResult token = new TokenResult();
        if (errorCode == 0) {
            token.setErrorCode(0);
            token.setAccessToken(object.getString("access_token"));
            token.setExpiresIn(object.getInteger("expires_in"));
            token.setRefreshToken(object.getString("refresh_token"));
            token.setOpenId(object.getString("open_id"));
            token.setScope(object.getString("scope"));
        } else {
            token.setErrorCode(errorCode);
            token.setDescription(description);
        }
        return token;
    }

授权相关的controller

@Value("${redis.key.douyinTokenKeyPrefix}")
    private String douyinTokenKeyPrefix;
    @Value("${redis.key.douyinRefreshTokenKeyPrefix}")
    private String douyinRefreshTokenKeyPrefix;
    @Value("${open.douyin.clientKey}")
    private String clientKey;
    @Value("${open.douyin.clientSecret}")
    private String clientSecret;

     /**
     * 抖音授权登录
     * @param anchorUuid
     * @param response
     * @throws IOException
     */
    @RequestMapping(value = "qrcodeAuth")
    public void qrcodeAuth(String anchorUuid, HttpServletResponse response) throws IOException {
        String redirectUrl = parameter.getSERVER_PATH() + "/mobile/douyin/authCallback";
        String state = UuidUtils.randomUUID() + "::" + anchorUuid;
        String requestUrl = oauthService.qrcodeAuth(clientKey, URLEncoder.encode(redirectUrl, "UTF-8"), state);
        response.sendRedirect(requestUrl);
    }
    
    /**
     * 抖音授权回调
     * @param request
     * @return
     */
    @RequestMapping(value = "authCallback")
    public void authCallback(HttpServletRequest request) {
        String state = request.getParameter("state");
        String anchorUuid = state.split("::")[1];
        TokenResult result = oauthService.accessToken(request, clientKey, clientSecret);
        if (result.getErrorCode() == 0) {
            String openId = result.getOpenId();
            String accessToken = result.getAccessToken();
            //保存accessToken等信息到缓存
            stringRedisTemplate.opsForValue().set(douyinTokenKeyPrefix + anchorUuid,
                    accessToken, 14, TimeUnit.DAYS);
            stringRedisTemplate.opsForValue().set(douyinRefreshTokenKeyPrefix + anchorUuid,
                    result.getRefreshToken(), 29, TimeUnit.DAYS);
            logger.info("accessToken===" + accessToken);
            anchorService.saveDouyin(accessToken, anchorUuid, openId);
        }
    }

4.3、根据access_token和open_id就可以获取到该用户的基本信息和粉丝统计数据

/**
     * 获取用户信息
     * @param accessToken
     * @param openId
     * @return
     */
    @Override
    public JSONObject userInfo(String accessToken,String openId) {
        String requestUrl = Urls.BASE_URL+String.format(Urls.USERINFO_URL,accessToken,openId);
        JSONObject response = (CommonUtil.httpsRequestJson(requestUrl, "GET", null));
        JSONObject object = response.getJSONObject("data");
        logger.info("userInfo result=" + response);
        return object;
    }

     /**
     * 获取用户粉丝数据
     * @param accessToken
     * @param openId
     * @return
     */
    @Override
    public JSONObject fansData(String accessToken,String openId) {
        String requestUrl = Urls.BASE_URL+String.format(Urls.FANS_DATA_URL,accessToken,openId);
        JSONObject response = (CommonUtil.httpsRequestJson(requestUrl, "GET", null));
        JSONObject object = response.getJSONObject("data");
        logger.info("fansData result=" + response);
        return object;
    }

用户信息接口没有返回该用户的粉丝数,倒是在粉丝统计数据接口那边返回来粉丝数,可以在这边拿到粉丝数存到用户表,结合前端开发,把数据传给前端就可以显示出来了。这边叫了一个漂亮的小姐姐授权了,下面有短视频截图,你们就说好不好看吧。

用户基本信息


image.png

粉丝年龄分布、区域分布和性别分布


image.png

粉丝活跃分布
image.png

粉丝设备分布


image.png

粉丝兴趣分布
image.png

4.4、根据access_token和open_id就可以获取到该用户所有的抖音短视频数据

/**
     * 该接口用于分页获取用户所有视频的数据。返回的数据是实时的。
     * 列出已发布的视频
     * @param accessToken
     * @param openId
     * @param cursor
     * @param count
     * @return
     */
    @Override
    public JSONObject videoList(String accessToken,String openId,Long cursor,Integer count) {
        String requestUrl = Urls.BASE_URL+String.format(Urls.VIDEO_LIST_URL,accessToken,openId,cursor,count);
        JSONObject response = (CommonUtil.httpsRequestJson(requestUrl, "GET", null));
        logger.info("videoList result=" + response);
        return response;
    }

和粉丝数一样,开放平台没有提供接口直接获取用户的作品数、点赞数、总评论数、总分享数、平均点赞数、平均评论数、平均分享数,所以我们在获取到所有视频的时候要根据每条视频返回来的相应字段计算出这些数据再存到数据库,结合前端开发,把数据传给前端就可以显示出来了。

image.png

这里不得不吐槽一下,像粉丝数、作品数、点赞数、总评论数、总分享数等这些和用户相关的字段应该统计出来在用户信息那个接口就要返回来的,这样能给开发者省了很多时间,而且更符合常理,不知道抖音是怎么想的。

5、总结


image.png

5.1、看完这些代码之后,其实也不难,和对接其他第三方接口一样,只要照着文档写,总能调出结果来。现在看到的抖音开放平台文档是更新过的,看上去会比之前要好些,不管是版面、注意点还是参数注释都有改进,虽然还是没提供demo下载,但是增加了几种语言的接口调用样例虽然没有什么实际的作用,但是手心手背都是肉,还是知足些吧。


image.png

5.2、第一次对接新的第三方接口基本都有坑,大部分时候我们都寄希望于踩过坑的前人能够填好这些坑,给后来的人一些参考,少走弯路,节省时间,提高效率。起初抖音官方在飞书建了开放平台技术讨论群,可以在里面问问题,但是没多久上线了工单平台,要开发者有问题就提工单,就关掉了飞书群。有过对接第三方开发经验的应该都有感触,提交工单的途径来问问题的效率有多慢。下面就列出一些在开发过程中遇到的坑,小伙伴们感受一波。

问题:当时对接的时候,修改回调域名需要重新审核,不知道现在平台改过来没有。

解决:所以为了保险起见,还是一开始就填正确,这点微信开放平台修改回调域名不需要审核。

问题:在做OAuth 2.0授权时,scope传入多个,像这样scope=aweme.share,hotsearch,enterprise.data,user_info,fans.list,following.list,fans.data,video.create,video.delete,video.data,video.list,video.comment,总是报“权限非法”,我去掉一个就可以,我试了多次后猜想应该是scope太长了,最后一个权限被抖音截掉了(比如video.comment被截断变成了video.com,而video.com确实不是完整的权限,所以就报权限非法的错误)。


image.png

解决:然后向平台反应了,果然这是他们的一个bug,现在已经修复了。

问题:接口不稳定,有时候可以,有时候不可以。

解决:所有的接口路径,最后面都要加上“/”,比如/fans/data/,这个不知道为什么,是问了抖音工作人员给出的解决方案。

问题:调用授权二维码的时候,如果因为自身业务需要在用户扫码确认授权后回调我们的接口那边携带自己的参数,要注意,不能在回调接口的路径上拼接参数,因为回调那边获取不到,比如回调接口路径是/mobile/douyin/authCallback,不能这样携带参数/mobile/douyin/authCallback?userId=36781631,这应该也是被抖音限制了吧,但是做微信扫码授权就可以这样传参。

解决:扫码的时候为了安全需要传入一个随机数state,可以在state后面拼接我们的业务参数,然后在回调那边获取到state后截取。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容