# 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
```