# douyin-open-api-sdk **Repository Path**: 75270093/douyin-open-api-sdk ## Basic Information - **Project Name**: douyin-open-api-sdk - **Description**: 抖音开放平台sdk java版(个人项目) forked from https://github.com/gadfly3173/douyin-open-api-sdk - **Primary Language**: Java - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 19 - **Created**: 2023-08-10 - **Last Updated**: 2023-08-22 ## Categories & Tags **Categories**: Uncategorized **Tags**: 抖音SDK, 抖音小程序SDK ## README # douyin-open-api-sdk 快速接入 几行代码实现抖音接入 开发中,暂不可直接使用 参考项目: - [https://gitee.com/hudan870614/wptai-douyin-api](https://gitee.com/hudan870614/wptai-douyin-api) - [https://github.com/yydzxz/ByteDanceOpen](https://github.com/yydzxz/ByteDanceOpen) - [https://github.com/Wechat-Group/WxJava](https://github.com/Wechat-Group/WxJava) - [抖音网站应用能力概述-API集合列表](https://developer.open-douyin.com/docs/resource/zh-CN/dop/overview/capabilities) - [抖音小程序能力概述-API集合列表](https://developer.open-douyin.com/docs/resource/zh-CN/mini-app/introduction/capabilities) - [抖音网站应用能力概述-API集合列表](https://developer.open-douyin.com/docs/resource/zh-CN/dop/overview/capabilities) - [抖音生活服务商家应用能力概述-API集合列表](https://partner.open-douyin.com/docs/resource/zh-CN/local-life/guide/guide) - ## 架构 本SDK应当以**单例**或**依赖注入**的形式被调用。 整体架构与WxJava项目接近,提供了一个默认实现`DefaultTtOpServiceImpl.java`。其中的各api接口实现在各个子service中。 使用时需要修改子service的实现的话就去实现其对应的interface,通过`ITtOpBaseService.setXXXService(yourSubService)`来实现。 而如果需要修改`DefaultTtOpServiceImpl.java`中的实现,也就是覆盖`AbstractTtOpApiBase.java`的实现时, 请不要直接实现(implements) `ITtOpBaseService`,而是继承(extends) `AbstractTtOpApiBase`。 * 自动更新用户的 access/refresh token 功能未实现。 ## 项目介绍 为抖音开发者提供快速接入方案、未依赖任何第三方mvc框架,支持各类 java web 框架接入 ## 安装教程 ### Maven引用 > 暂未在maven仓库发布,请自行打包后使用 > > 由于准备发布至maven仓库,配置了gpg插件。自行打包时可能需要删除 ```xml vip.gadfly douyin-open-api-sdk 0.0.3 ``` maven引用时可能出现依赖的okhttp3版本变为3.14.9等低版本的情况,如果在使用自带的okhttp实现时遇到NoSuchMethod等异常时, 可以另外声明依赖的okhttp版本或换成自带的joddhttp/rest template的实现,这两个实现的依赖需要另外声明。 ```xml com.squareup.okhttp3 okhttp 4.9.1 org.jodd jodd-http 5.1.5 org.springframework spring-web 5.2.7.RELEASE ``` ### 自行打包 参阅:[编译](#编译) ## 规范说明 - 类 相关: * Service 类: 接口调用及处理类 * Request 类: 接口入参实实体类 * Result 类:接口出参实体类 ## 使用说明 - 默认的json序列化/反序列化包:`gson` - 默认的http client:`okhttp 4` - 项目给出了RedisTemplate的实现,但是`spring-data-redis`是provided scope的 ### 加载配置文件及指定配置参数(Spring Boot 2.x) 1.在 `application.yml` 中添加如下配置,clientKey:网站应用,mini:指小程序 *以下配置仅供参考* ```yaml tt: op: useRedis: true configs: - clientKey: awxxxxxxxxx clientSecret: 87111aaaaaaa1111xxxx11 miniClientKey: tt5e252898xxxxxx miniClientSecret: 48a5xxxxxxxxxe49138d5d49f ``` 2. 加载及初始化 ```java @Configuration @RequiredArgsConstructor @EnableConfigurationProperties(TtOpConfiguration.TtOpProperties.class) public class TtOpConfiguration { private final TtOpProperties properties; private final RedisTemplate redisTemplate; @Bean public ITtOpBaseService ttBaseService() { final List configs = this.properties.getConfigs(); if (CollectionUtils.isEmpty(configs)) { throw new RuntimeException("抖音配置无效,请检查!"); } RedisTemplateTtOpRedisOps redisOps = new RedisTemplateTtOpRedisOps(redisTemplate); DefaultTtOpServiceImpl service = new DefaultTtOpServiceImpl(); service.setMultiConfigStorages(configs.stream().map(a -> { TtOpDefaultConfigImpl configStorage; if (this.properties.isUseRedis()) { configStorage = new TtOpRedisConfigImpl(redisOps, "tiktok_open"); } else { configStorage = new TtOpDefaultConfigImpl(); } configStorage.setClientKey(a.getClientKey()); configStorage.setClientSecret(a.getClientSecret()); return configStorage; }).collect(Collectors.toMap(TtOpDefaultConfigImpl::getClientKey, a -> a, (o, n) -> o))); // 设置http client,okhttp是默认值,可以不设置 service.setTiktokOpenHttpClient(new OkHttpTtOpHttpClient()); return service; } @Data @ConfigurationProperties(prefix = "tt.op") public static class TtOpProperties { /** * 是否使用redis存储access token */ private boolean useRedis; /** * 多个抖音开放应用配置信息 */ private List configs; @Data public static class TtOpConfig { /** * 设置抖音开放应用的clientKey */ private String clientKey; /** * 设置抖音开放应用的app secret */ private String clientSecret; } } } ``` ### Webhook消息路由 项目提供了 Webhook 消息的路由器,可以针对不同的消息/事件类型配置不同的处理。 配置文件 ```java @Configuration @RequiredArgsConstructor public class TtOpConfiguration { // 这些Handler都需要自己编写,自行针对不同的场景进行配置即可。 private final LogHandler logHandler; private final NullHandler nullHandler; private final VerifyWebhookHandler verifyWebhookHandler; @Bean public TtOpWebhookMessageRouter messageRouter() { RedisTemplateTtOpRedisOps redisOps = new RedisTemplateTtOpRedisOps(redisTemplate); final TtOpWebhookMessageRouter messageRouter = new TtOpWebhookMessageRouter(new TtOpWebhookRedisDuplicateChecker(redisOps)); // 默认async是true,也就是异步执行,可以在这些异步处理里加上专用的日志记录等 messageRouter.rule().addHandler(this.logHandler).next(); // 可以指定event来让这个事件的情况都进入某个handler。对于私信类事件还可以指定msgType // msgType根据收到消息中的content.messageType字段来区分。event 和 msgType 的判断都忽略大小写。 // 如果给非私信事件配置msgType,会因为取不到消息中的content.messageType而认为不符合路由 // 因此对于非私信事件,必须将msgType置为null或不设置 // // 这里使用verify webhook仅为示例,实际开发中这个事件直接在controller里处理更简单 messageRouter.rule().async(false).event(WebhookEventType.VERIFY_WEBHOOK).addHandler(this.verifyWebhookHandler).end(); // 不指定event则这个handler处理所有的事件。路由规则的设置依赖ArrayList,因此需要注意顺序,这个兜底的路由需要放在最后。 messageRouter.rule().async(false).addHandler(this.nullHandler).end(); return messageRouter; } } ``` Handler ```java public abstract class AbstractHandler implements ITtOpWebhookMessageHandler { protected static final Logger log = org.slf4j.LoggerFactory.getLogger(AbstractHandler.class); } @Service public class LogHandler extends AbstractHandler { @Override public TtOpWebhookMessageHandleResult handle(TtOpWebhookMessage ttOpWebhookMessage, Map map) { log.info("接收到抖音webhook请求消息,内容:{}", JSONObject.toJSONString(ttOpWebhookMessage)); return null; } } @Service public class VerifyWebhookHandler extends AbstractHandler { @Override public TtOpWebhookMessageHandleResult handle(TtOpWebhookMessage message, Map map) { log.info("VerifyWebhookChallenge为:{}", message.getContent().getChallenge()); TtOpWebhookMessageHandleResult result = new TtOpWebhookMessageHandleResult(); result.setHandleResult(message.getContent()); return result; } } @Service public class NullHandler extends AbstractHandler { @Override public TtOpWebhookMessageHandleResult handle(TtOpWebhookMessage ttOpWebhookMessage, Map map) { log.info("进入了默认处理"); return null; } } ``` 在controller中使用 ```java @RestController @Slf4j public class CallbackController { @Autowired private ITtOpBaseService ttOpBaseService; @Autowired private TtOpWebhookMessageRouter messageRouter; @ApiOperation("抖音token授权-1-小程序-tt.showDouyinOpenAuth-返回openid和accesstoken") @GetMapping("/getOpenAuthToken") public AjaxResult getOpenAuthToken(@RequestParam String code) { TtMiniUserInfoService ttMiniUserInfoService = ttOpBaseService.getTtMiniUserInfoService(); TtMiniAccessTokenResult accessTokenByAuthorizationCode = ttMiniUserInfoService.getAccessTokenByAuthorizationCode(code); log.info("result:{}", accessTokenByAuthorizationCode); return AjaxResult.success(accessTokenByAuthorizationCode); } @ApiOperation("抖音token授权后登录-tt.login-只返回openid") @GetMapping("/getAuthLogin") public AjaxResult getAuthLogin(@RequestParam String openId) { TtMiniUserInfoService ttMiniUserInfoService = ttOpBaseService.getTtMiniUserInfoService(); TtMiniAccessTokenResult miniUserLogin = ttMiniUserInfoService.getMiniUserLogin(openId); log.info("result:{}", miniUserLogin); return AjaxResult.success(miniUserLogin); } /** * @param openId * @param dateType 天 * @return */ @ApiOperation("抖音用户统计视频-2-by-open_id") @GetMapping("/getVideoExternalList") public AjaxResult getVideoExternalList(@RequestParam String openId, Integer dateType) { TtMiniUserInfoService ttMiniUserInfoService = ttOpBaseService.getTtMiniUserInfoService(); TtMiniVideoListResult externalList = ttMiniUserInfoService.getExternalList(openId, dateType); log.info("result:{}", externalList); return AjaxResult.success(externalList); } @ApiOperation("抖音用户统计粉丝-2-by-open_id") @GetMapping("/getFancsExternalList") public AjaxResult getFancsExternalList(@RequestParam String openId, Integer dateType) { TtMiniUserInfoService ttMiniUserInfoService = ttOpBaseService.getTtMiniUserInfoService(); TtMiniFansListResult fansList = ttMiniUserInfoService.getFansList(openId, dateType); log.info("result:{}", fansList); return AjaxResult.success(fansList); } } ``` 网站应用样例 ```java import com.pingjl.domain.AjaxResult; import com.pingjl.tasks.DouyinAsyncUploadVideo; import com.pingjl.util.MinioUtil; import io.swagger.annotations.ApiOperation; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.apache.commons.io.FileUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpHeaders; import org.springframework.web.bind.annotation.*; import vip.gadfly.tiktok.core.enums.TtOpConst; import vip.gadfly.tiktok.mini.api.TtMiniUserInfoService; import vip.gadfly.tiktok.mini.bean.oauth2.TtMiniAccessTokenResult; import vip.gadfly.tiktok.open.api.TtOpOAuth2Service; import vip.gadfly.tiktok.open.api.TtOpUserInfoService; import vip.gadfly.tiktok.open.api.TtOpVideoService; import vip.gadfly.tiktok.open.bean.message.TtOpWebhookMessage; import vip.gadfly.tiktok.open.bean.oauth2.TtOpAccessTokenResult; import vip.gadfly.tiktok.open.bean.userinfo.TtOpFansListResult; import vip.gadfly.tiktok.open.bean.userinfo.TtOpUserInfoResult; import vip.gadfly.tiktok.open.bean.video.*; import vip.gadfly.tiktok.open.common.ITtOpBaseService; import vip.gadfly.tiktok.open.message.TtOpWebhookMessageResult; import vip.gadfly.tiktok.open.message.TtOpWebhookMessageRouter; import javax.servlet.http.HttpServletRequest; import java.io.File; import java.io.InputStream; import java.util.ArrayList; import java.util.List; import java.util.Map; @RestController @Slf4j @RequestMapping("/douyinWeb") public class DouyinWebController { @Autowired private MinioUtil minioUtil; @Autowired private ITtOpBaseService ttOpBaseService; @Autowired private TtOpWebhookMessageRouter messageRouter; /** * 回调消息能力() * @param body * @param headers * @param clientKey * @return */ @PostMapping("/tiktokOpenWebhook/{clientKey}") public Object handlePostTtOpWebhook(@RequestBody String body, @RequestHeader HttpHeaders headers, @PathVariable String clientKey) { if (!ttOpBaseService.switchover(clientKey)) { throw new IllegalArgumentException(String.format("未找到对应clientKey=[%s]的配置,请核实!", clientKey)); } if (!ttOpBaseService.checkWebhookSignature(headers.getFirst("X-Douyin-Signature"), body)) { throw new IllegalArgumentException("非法请求,可能属于伪造的请求!"); } TtOpWebhookMessage message = TtOpWebhookMessage.fromJson(body); // 抖音webhook的消息id放在了请求头中,因此sdk不能直接读取,需要自行传入 message.setMsgId(headers.getFirst("Msg-Id")); // 抖音的webhook验证要求返回的内容是一个包含challenge的json,相比于走路由器,直接处理消息后做个if直接return更简单 if (message.getEvent().equalsIgnoreCase(TtOpConst.WebhookEventType.VERIFY_WEBHOOK)) { return message.getContent(); } TtOpWebhookMessageResult result = messageRouter.route(message); log.info("result:{}", result); return result.getDefaultResult(); } @ApiOperation("抖音token授权后登录-tt.login-只返回openid") @GetMapping("/getJscode2session") public AjaxResult getJscode2session(@RequestParam String code) { log.info("getJscode2session code:{}", code); TtMiniUserInfoService ttMiniUserInfoService = ttOpBaseService.getTtMiniUserInfoService(); TtMiniAccessTokenResult miniUserLogin = ttMiniUserInfoService.getMiniUserLogin(code); log.info("result:{}", miniUserLogin); return AjaxResult.success(miniUserLogin); } @ApiOperation("抖音网站应用==取token与openid") @GetMapping("/getWebToken") public AjaxResult douyinGetToken(HttpServletRequest request) { String code = request.getParameter("code"); log.info("douyinGetToken code:{}", code); TtOpOAuth2Service ttOpUserInfoService = ttOpBaseService.getTtOpOAuth2Service(); TtOpAccessTokenResult accessTokenByAuthorizationCode = ttOpUserInfoService.getAccessTokenByAuthorizationCode(code); log.info("result:{}", accessTokenByAuthorizationCode); return AjaxResult.success(accessTokenByAuthorizationCode); } @ApiOperation("抖音网站应用==取用户信息头像昵称") @GetMapping("/getWebUserInfo") public AjaxResult getWebUserInfo(@RequestParam String openId) { TtOpUserInfoService ttOpUserInfoService = ttOpBaseService.getTtOpUserInfoService(); TtOpUserInfoResult userInfo = ttOpUserInfoService.getUserInfo(openId); log.info("result:{}", userInfo); return AjaxResult.success(userInfo); } /** * 获取粉丝列表 * * @param openId openid * @param cursor 游标 * @param count 数量 * @return 结果 */ @ApiOperation("抖音网站应用==获取粉丝列表") @GetMapping("/getFansList") public AjaxResult getFansList(@RequestParam String openId) { //粉丝 Long cursor = 0L; Integer count = 100; TtOpUserInfoService ttOpUserInfoService = ttOpBaseService.getTtOpUserInfoService(); TtOpFansListResult fansList = ttOpUserInfoService.getFansList(openId, cursor, count); log.info("result:{}", fansList); return AjaxResult.success(fansList); } /** * 上传视频() * * @param openId openid * @return 结果 video_id * "data": { * "error_code": 0, * "description": "", * "cursor": null, * "has_more": false, * "total": null, * "video": { * "height": 640, * "video_id": "@9VwHjuSCRcs7LSOuMo4+T87z1mHpNfCAPJF3oWXgLwUTbqer1nDhelpF7GjFkA5nD2bFzmsDdoFT1MWBTRfTOMO5m8mpStxiUCFxjpB9uiQ=", * "width": 368 * } * } */ @ApiOperation("抖音网站应用==上传视频V2(minio视频文件)") @GetMapping("/uploadVideoV2") public AjaxResult uploadVideoV2(@RequestParam String openId, String filePath) { if(filePath==null){ return AjaxResult.error("文件必传"); } File tempFile = null; String fileName = filePath.substring(filePath.lastIndexOf("."),filePath.length()); if(filePath.startsWith("http")|| filePath.startsWith("https")){ filePath = filePath.substring(filePath.indexOf("douyin/"),filePath.length()); } log.info("uploadVideoV2 filePath:{}", filePath); InputStream stream = null; try { stream = minioUtil.download("pjl", filePath); tempFile = File.createTempFile("temp", fileName); FileUtils.copyInputStreamToFile(stream, tempFile); } catch (Exception e) { e.printStackTrace(); return AjaxResult.error("文件下载失败:"+ filePath); } TtOpTiktokVideoUploadRequest request = new TtOpTiktokVideoUploadRequest(); request.setVideo(tempFile); TtOpVideoService ttOpUserInfoService = ttOpBaseService.getTtOpVideoService(); TtOpTiktokVideoUploadResult fansList = ttOpUserInfoService.uploadTiktokVideoV2(openId, request); log.info("result:{}", fansList); return AjaxResult.success(fansList); } /** * 创建视频 * * @param openId openid * @return 结果 item_id * { * "msg": "操作成功", * "code": 0, * "data": { * "error_code": 0, * "description": "", * "cursor": null, * "has_more": false, * "total": null, * "item_id": "@9VwHjuSCRcs7LSOuMo4+T87912PgNP2DPZZzqwKhLVYRbPT960zdRmYqig357zEBQ2IqKB/xyjGoSnv4th07Vw==" * } * } */ @ApiOperation("抖音网站应用==创建视频V2") @GetMapping("/createVideoV2") public AjaxResult createVideoV2(@RequestParam String openId, String videoId) { TtOpVideoService ttOpUserInfoService = ttOpBaseService.getTtOpVideoService(); TtOpTiktokVideoCreateRequest request = new TtOpTiktokVideoCreateRequest(); request.setVideoId(videoId); TtOpTiktokVideoCreateResult tiktokVideoV2 = ttOpUserInfoService.createTiktokVideoV2(openId, request); log.info("result:{}", tiktokVideoV2); return AjaxResult.success(tiktokVideoV2); } /** * 获取视频列表 * * @param openId openid * @return 结果 */ @ApiOperation("抖音网站应用==查询抖音指定视频数据") @GetMapping("/getVideoData") public AjaxResult getVideoData(@RequestParam String openId) { TtOpVideoService ttOpUserInfoService = ttOpBaseService.getTtOpVideoService(); TtOpTiktokVideoDataRequest request = new TtOpTiktokVideoDataRequest(); List itemIds = new ArrayList<>(); request.setItemIds(itemIds); TtOpTiktokVideoDataResult fansList = ttOpUserInfoService.getTiktokSpecificVideoData(openId, request); log.info("result:{}", fansList); return AjaxResult.success(fansList); } @ApiOperation("重取PC==token") @SneakyThrows @GetMapping("/getPcTokenCode") public AjaxResult getPcTokenCode(String code) { String clientKey = "awc8wpybkr4o26co"; String clientSecret = "7cb2f0ccca6240b005a0f198320ea439"; Map shareresult = DouyinAsyncUploadVideo.getDouYinAcessToken(code, clientKey, clientSecret); log.info("douyinLogin result:"+shareresult); return AjaxResult.success(shareresult); } } ``` ## 编译 项目使用了lombok来简化代码,请在你的IDE中安装对应的插件。 由于lombok是在编译过程中增加字节码的形式来增加语句,因此不做配置时使用 `maven-source-plugin` 打包出的源码包会在IDE中提示字节码与源码不同。为了解决这个问题,项目中引入了 `lombok-maven-plugin` 来进行 delombok。打包时需要指定一下profile id。例如: ```bash mvn clean install -Pdelombok -f pom.xml clean ```