diff --git a/dy-java-common/pom.xml b/dy-java-common/pom.xml index ce1397bfc3391f36a84421c5a935056f1ae134ae..7480046e8f9250619073ab48862895ed6f1eb4b5 100644 --- a/dy-java-common/pom.xml +++ b/dy-java-common/pom.xml @@ -19,13 +19,19 @@ com.dtflys.forest - forest-spring-boot-starter + forest-spring-boot3-starter com.alibaba fastjson + + + org.projectlombok + lombok + + diff --git a/dy-java-common/src/main/java/com/dyj/common/client/TikTokAuthClient.java b/dy-java-common/src/main/java/com/dyj/common/client/TikTokAuthClient.java new file mode 100644 index 0000000000000000000000000000000000000000..2e743875d66d6cb522af09f089587c968422d755 --- /dev/null +++ b/dy-java-common/src/main/java/com/dyj/common/client/TikTokAuthClient.java @@ -0,0 +1,19 @@ +package com.dyj.common.client; + +import com.dtflys.forest.annotation.BaseRequest; +import com.dtflys.forest.annotation.Body; +import com.dtflys.forest.annotation.Post; +import com.dyj.common.domain.vo.AccessTikTokTokenVo; +import com.dyj.common.interceptor.NoTokenInterceptor; + +@BaseRequest(baseURL = "${tikTokDomain}",interceptor = NoTokenInterceptor.class) +public interface TikTokAuthClient { + + /** 获取accessToken */ + @Post(value = "${tikTokOauthAccessToken}") + AccessTikTokTokenVo getAccessToken(@Body("client_key") String clientKey, @Body("client_secret") String clientSecret, @Body("code") String code, @Body("grant_type") String grantType, @Body("redirect_uri") String redirectUri); + + /** 刷新accessToken */ + @Post(value = "${tikTokOauthAccessToken}") + AccessTikTokTokenVo refreshToken(@Body("client_key") String clientKey, @Body("client_secret") String clientSecret, @Body("grant_type") String grantType, @Body("refresh_token") String refreshToken); +} diff --git a/dy-java-common/src/main/java/com/dyj/common/domain/DyError.java b/dy-java-common/src/main/java/com/dyj/common/domain/DyError.java new file mode 100644 index 0000000000000000000000000000000000000000..922166c2d8c0562fd9cf20a20840129290dc6231 --- /dev/null +++ b/dy-java-common/src/main/java/com/dyj/common/domain/DyError.java @@ -0,0 +1,14 @@ +package com.dyj.common.domain; + +import lombok.Data; + +@Data +public class DyError { + + private String code; + + private String message; + + private String log_id; + +} diff --git a/dy-java-common/src/main/java/com/dyj/common/domain/DyTikTokResult.java b/dy-java-common/src/main/java/com/dyj/common/domain/DyTikTokResult.java new file mode 100644 index 0000000000000000000000000000000000000000..8eb4663f0141496264d34e7886790504f9883a5b --- /dev/null +++ b/dy-java-common/src/main/java/com/dyj/common/domain/DyTikTokResult.java @@ -0,0 +1,14 @@ +package com.dyj.common.domain; + +import lombok.Data; + +@Data +public class DyTikTokResult { + /** + * 返回对象 + */ + private T data; + + private DyError error; + +} diff --git a/dy-java-common/src/main/java/com/dyj/common/domain/query/UserInfoQuery.java b/dy-java-common/src/main/java/com/dyj/common/domain/query/UserInfoQuery.java index a77adb3e870934106c47630ea7d298d66b3bb195..50cdb0e9ae7ef3bedf31d635285d8dda770ceb9a 100644 --- a/dy-java-common/src/main/java/com/dyj/common/domain/query/UserInfoQuery.java +++ b/dy-java-common/src/main/java/com/dyj/common/domain/query/UserInfoQuery.java @@ -13,13 +13,11 @@ public class UserInfoQuery extends BaseQuery { */ protected String open_id; - public String getOpen_id() { - return open_id; - } - public void setOpen_id(String open_id) { this.open_id = open_id; } - + public String getOpen_id() { + return open_id; + } } diff --git a/dy-java-common/src/main/java/com/dyj/common/domain/vo/AccessTikTokTokenVo.java b/dy-java-common/src/main/java/com/dyj/common/domain/vo/AccessTikTokTokenVo.java new file mode 100644 index 0000000000000000000000000000000000000000..2f16b1ea282d1435d660f52f125298b5b490f0d2 --- /dev/null +++ b/dy-java-common/src/main/java/com/dyj/common/domain/vo/AccessTikTokTokenVo.java @@ -0,0 +1,67 @@ +package com.dyj.common.domain.vo; + +import com.alibaba.fastjson.annotation.JSONField; +import lombok.*; +import lombok.experimental.Accessors; + +import java.io.Serial; +import java.io.Serializable; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@EqualsAndHashCode(callSuper = true) +public class AccessTikTokTokenVo extends BaseVo implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + + /** + * 授权用户唯一标识 + */ + @JSONField(name = "open_id") + private String openId; + + /** + * 用户授权的作用域(Scope),使用逗号(,)分隔,开放平台几乎每个接口都需要特定的Scope。 + */ + private String scope; + + /** + * 接口调用凭证 + */ + @JSONField(name = "access_token") + private String accessToken; + + /** + * access_token接口调用凭证超时时间,单位(秒) + */ + @JSONField(name = "expires_in") + private Long expiresIn; + + /** + * 日志ID + */ + @JSONField(name = "log_id") + private String logId; + + /** + * 用户刷新access_token + */ + @JSONField(name = "refresh_token") + private String refreshToken; + + /** + * refresh_token凭证超时时间,单位(秒) + */ + @JSONField(name = "refresh_expires_in") + private Long refreshExpiresIn; + + /** + * token类型 + */ + @JSONField(name = "token_type") + private String tokenType; + +} diff --git a/dy-java-common/src/main/java/com/dyj/common/enums/DyTikTokPathEnum.java b/dy-java-common/src/main/java/com/dyj/common/enums/DyTikTokPathEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..c96cea3ebd3f8e71df243cc958d1cb6d260458c3 --- /dev/null +++ b/dy-java-common/src/main/java/com/dyj/common/enums/DyTikTokPathEnum.java @@ -0,0 +1,68 @@ +package com.dyj.common.enums; + +public enum DyTikTokPathEnum { + + /** + * 域名 + */ + DOMAIN("tikTokDomain", "https://open.tiktokapis.com/v2"), + /** + * 获取 access_token + */ + OAUTH_ACCESS_TOKEN("tikTokOauthAccessToken", "/oauth/token/"), + + /** + * 用户信息 + */ + USER_INFO("tikTokUserInfo", "/user/info/"), + + /** + * 查询视频列表 + */ + QUERY_VIDEO_LIST("tikTokQueryVideoList", "/video/list/"), + + /** + * 初始化上传视频 + */ + INIT_UPLOAD_VIDEO("tikTokInitUploadVideo", "/post/publish/inbox/video/init/"), + + /** + * 上传图片 + */ + UPLOAD_IMAGE("tikTokUploadImage", "/post/publish/content/init/"), + + QUERY_POST_STATUS("tikTokQueryPostStatus", "/post/publish/status/fetch/"); + + private String key; + private String value; + + DyTikTokPathEnum(String key, String value) { + this.key = key; + this.value = value; + } + + public String getKey() { + return key; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public void setKey(String key) { + this.key = key; + } + + public static String getValueByKey(String key) { + for (DyWebUrlPathEnum e : DyWebUrlPathEnum.values()) { + if (e.getKey().equals(key)) { + return e.getValue(); + } + } + return null; + } +} diff --git a/dy-java-common/src/main/java/com/dyj/common/interceptor/ClientQueryTokenInterceptor.java b/dy-java-common/src/main/java/com/dyj/common/interceptor/ClientQueryTokenInterceptor.java index 2d48e5b5a5c9d711714f440737168d096d9b0ea4..d1aa5849fdd73e8fa2ecdb081ebdc9b7b45e4b8d 100644 --- a/dy-java-common/src/main/java/com/dyj/common/interceptor/ClientQueryTokenInterceptor.java +++ b/dy-java-common/src/main/java/com/dyj/common/interceptor/ClientQueryTokenInterceptor.java @@ -18,7 +18,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.stereotype.Component; -import javax.annotation.Resource; +import jakarta.annotation.Resource; import java.util.Objects; @Component diff --git a/dy-java-common/src/main/java/com/dyj/common/interceptor/ClientTokenInterceptor.java b/dy-java-common/src/main/java/com/dyj/common/interceptor/ClientTokenInterceptor.java index a9333fffc702bfc0cbb6ff19352bc334fc73703e..16faaac2e15c272c25a12edffbe1c8ea8d667bb8 100644 --- a/dy-java-common/src/main/java/com/dyj/common/interceptor/ClientTokenInterceptor.java +++ b/dy-java-common/src/main/java/com/dyj/common/interceptor/ClientTokenInterceptor.java @@ -26,8 +26,8 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.stereotype.Component; -import javax.annotation.PostConstruct; -import javax.annotation.Resource; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.Resource; import java.util.Objects; @Component diff --git a/dy-java-common/src/main/java/com/dyj/common/interceptor/SubscriptionTokenInterceptor.java b/dy-java-common/src/main/java/com/dyj/common/interceptor/SubscriptionTokenInterceptor.java index 8490e3cdf04279a2cb1b167bc14d120a7fb27301..48390b236981f43284d7279ab7fa01afb828db6b 100644 --- a/dy-java-common/src/main/java/com/dyj/common/interceptor/SubscriptionTokenInterceptor.java +++ b/dy-java-common/src/main/java/com/dyj/common/interceptor/SubscriptionTokenInterceptor.java @@ -22,7 +22,7 @@ import org.apache.commons.logging.LogFactory; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; -import javax.annotation.Resource; +import jakarta.annotation.Resource; import java.util.Objects; /** diff --git a/dy-java-common/src/main/java/com/dyj/common/interceptor/TikTokBodyTokenInterceptor.java b/dy-java-common/src/main/java/com/dyj/common/interceptor/TikTokBodyTokenInterceptor.java new file mode 100644 index 0000000000000000000000000000000000000000..0a017d34147abb26b7c141472e757c1dba1d7ac6 --- /dev/null +++ b/dy-java-common/src/main/java/com/dyj/common/interceptor/TikTokBodyTokenInterceptor.java @@ -0,0 +1,62 @@ +package com.dyj.common.interceptor; + +import com.alibaba.fastjson.JSONObject; +import com.dtflys.forest.exceptions.ForestRuntimeException; +import com.dtflys.forest.http.ForestRequest; +import com.dtflys.forest.http.ForestResponse; +import com.dtflys.forest.interceptor.Interceptor; +import com.dyj.common.domain.UserTokenInfo; +import com.dyj.common.domain.query.UserInfoQuery; +import com.dyj.common.service.IAgentTokenService; +import com.dyj.common.utils.DyConfigUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.stereotype.Component; + +import java.util.Objects; + +@Component +public class TikTokBodyTokenInterceptor implements Interceptor { + + private final Log log = LogFactory.getLog(TikTokBodyTokenInterceptor.class); + @Override + public boolean beforeExecute(ForestRequest request) { + Integer tenantId = null; + String clientKey = ""; + String openId = ""; + Object[] arguments = request.getArguments(); + for (Object argument : arguments) { + if(argument instanceof UserInfoQuery){ + UserInfoQuery query = (UserInfoQuery) argument; + openId = query.getOpen_id(); + tenantId = query.getTenantId(); + clientKey = query.getClientKey(); + } + } + IAgentTokenService agentTokenService = DyConfigUtils.getAgentTokenService(); + UserTokenInfo userTokenInfo = agentTokenService.getUserTokenInfo(tenantId, clientKey,openId); + if (Objects.isNull(userTokenInfo)) { + throw new RuntimeException("access_token is null"); + } + request.addHeader("Authorization", "Bearer " + userTokenInfo.getAccessToken()); + return true; + } + + @Override + public void onError(ForestRuntimeException ex, ForestRequest request, ForestResponse response) { + StringBuilder sb = new StringBuilder("AccessTokenInterceptor onError "); + sb.append("url:"); + sb.append(request.getUrl()); + sb.append(", "); + sb.append("params:"); + sb.append(JSONObject.toJSONString(request.getArguments())); + sb.append(", "); + sb.append("result:"); + sb.append(response.getContent()); + sb.append(", "); + sb.append("msg:"); + sb.append(ex.getMessage()); + log.info(sb.toString()); + } + +} diff --git a/dy-java-common/src/main/java/com/dyj/common/interceptor/TpThirdV2TokenHeaderInterceptor.java b/dy-java-common/src/main/java/com/dyj/common/interceptor/TpThirdV2TokenHeaderInterceptor.java index 28e71f0d970f43a28132544e5b254aa81a427b19..ef7ac852bf79687af3d2b85c10fb5bec00ca6010 100644 --- a/dy-java-common/src/main/java/com/dyj/common/interceptor/TpThirdV2TokenHeaderInterceptor.java +++ b/dy-java-common/src/main/java/com/dyj/common/interceptor/TpThirdV2TokenHeaderInterceptor.java @@ -15,7 +15,7 @@ import org.apache.commons.logging.LogFactory; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; -import javax.annotation.Resource; +import jakarta.annotation.Resource; import java.util.Objects; /** diff --git a/dy-java-common/src/main/java/com/dyj/common/interceptor/TpV2AuthTokenHeaderInterceptor.java b/dy-java-common/src/main/java/com/dyj/common/interceptor/TpV2AuthTokenHeaderInterceptor.java index 636c03e437bf555e6032e07652525cfd58a40ce3..e010053c2055a8479f2afff83c781c34b67b4270 100644 --- a/dy-java-common/src/main/java/com/dyj/common/interceptor/TpV2AuthTokenHeaderInterceptor.java +++ b/dy-java-common/src/main/java/com/dyj/common/interceptor/TpV2AuthTokenHeaderInterceptor.java @@ -15,7 +15,7 @@ import org.apache.commons.logging.LogFactory; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; -import javax.annotation.Resource; +import jakarta.annotation.Resource; import java.util.Objects; /** diff --git a/dy-java-common/src/main/java/com/dyj/common/interceptor/TransactionMerchantTokenInterceptor.java b/dy-java-common/src/main/java/com/dyj/common/interceptor/TransactionMerchantTokenInterceptor.java index b2abf5544d46eb616d42d34f41fde0b65a1a95ef..2e56604d4a961265f1bd22356537392279a4f5fb 100644 --- a/dy-java-common/src/main/java/com/dyj/common/interceptor/TransactionMerchantTokenInterceptor.java +++ b/dy-java-common/src/main/java/com/dyj/common/interceptor/TransactionMerchantTokenInterceptor.java @@ -20,7 +20,7 @@ import org.apache.commons.logging.LogFactory; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; -import javax.annotation.Resource; +import jakarta.annotation.Resource; import java.util.Objects; /** diff --git a/dy-java-common/src/main/java/com/dyj/common/utils/DecryptUtils.java b/dy-java-common/src/main/java/com/dyj/common/utils/DecryptUtils.java index ea1d2c24e7a35071e41f13b4c4adc4c2ba09308e..e38c4c468a3a08158e4f4dad0270cf41ce6e2eb2 100644 --- a/dy-java-common/src/main/java/com/dyj/common/utils/DecryptUtils.java +++ b/dy-java-common/src/main/java/com/dyj/common/utils/DecryptUtils.java @@ -1,8 +1,6 @@ package com.dyj.common.utils; import org.springframework.core.io.ClassPathResource; -import org.springframework.util.Base64Utils; -import org.springframework.util.StringUtils; import javax.crypto.*; import javax.crypto.spec.IvParameterSpec; diff --git a/dy-java-common/src/main/java/com/dyj/common/utils/FieldUtils.java b/dy-java-common/src/main/java/com/dyj/common/utils/FieldUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..f0fa971455405cec9e7d3c8c4d4363974a0f76c8 --- /dev/null +++ b/dy-java-common/src/main/java/com/dyj/common/utils/FieldUtils.java @@ -0,0 +1,38 @@ +package com.dyj.common.utils; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.stream.Collectors; + +public class FieldUtils { + + /** + * 获取实体类的所有非静态字段名并用逗号连接 + * + * @param clazz 实体类的Class对象 + * @return 逗号连接的非静态字段名字符串 + */ + public static String getFieldsString(Class clazz) { + return java.util.Arrays.stream(clazz.getDeclaredFields()) + .filter(field -> !Modifier.isStatic(field.getModifiers())) + .map(field -> camelToSnake(field.getName())) + .collect(Collectors.joining(",")); + } + + /** + * 驼峰转下划线 + */ + private static String camelToSnake(String str) { + StringBuilder result = new StringBuilder(); + for (int i = 0; i < str.length(); i++) { + char c = str.charAt(i); + if (Character.isUpperCase(c)) { + result.append('_').append(Character.toLowerCase(c)); + } else { + result.append(c); + } + } + return result.toString(); + } + +} diff --git a/dy-java-spring/src/main/java/com/dyj/spring/DyConfigurationRegister.java b/dy-java-spring/src/main/java/com/dyj/spring/DyConfigurationRegister.java index 332767979208bb2461884f1fe743ebe4f973c059..a74c2c9973aab0149f7a28f8a534bdff7af3fd08 100644 --- a/dy-java-spring/src/main/java/com/dyj/spring/DyConfigurationRegister.java +++ b/dy-java-spring/src/main/java/com/dyj/spring/DyConfigurationRegister.java @@ -3,6 +3,7 @@ package com.dyj.spring; import com.dtflys.forest.config.ForestConfiguration; import com.dyj.common.config.DyConfiguration; import com.dyj.common.enums.DyAppletUrlPathEnum; +import com.dyj.common.enums.DyTikTokPathEnum; import com.dyj.common.enums.DyWebUrlPathEnum; import com.dyj.common.handler.RequestHandler; import com.dyj.common.service.IAgentConfigService; @@ -104,6 +105,9 @@ public class DyConfigurationRegister implements ResourceLoaderAware, BeanPostPro for (DyAppletUrlPathEnum value : DyAppletUrlPathEnum.values()) { forestProperties.getVariables().put(value.getKey(), value.getValue()); } + for (DyTikTokPathEnum value : DyTikTokPathEnum.values()) { + forestProperties.getVariables().put(value.getKey(), value.getValue()); + } if (StringUtils.hasLength(dyConfigurationProperties.getDomain())) { forestProperties.getVariables().put("domain", dyConfigurationProperties.getDomain()); } @@ -111,6 +115,7 @@ public class DyConfigurationRegister implements ResourceLoaderAware, BeanPostPro forestProperties.getVariables().put("ttDomain", dyConfigurationProperties.getTtDomain()); } + forestConfiguration.setVariables(forestProperties.getVariables()); } diff --git a/dy-java-spring/src/main/java/com/dyj/spring/config/DyAutoConfiguration.java b/dy-java-spring/src/main/java/com/dyj/spring/config/DyAutoConfiguration.java index 3bc2a2af7a26d4f378f0d91aeeb7681d1a65e637..2fd7590e1ffc6a4c294049ec4e6291d9095edb5f 100644 --- a/dy-java-spring/src/main/java/com/dyj/spring/config/DyAutoConfiguration.java +++ b/dy-java-spring/src/main/java/com/dyj/spring/config/DyAutoConfiguration.java @@ -16,7 +16,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.util.StringUtils; -import javax.annotation.Resource; +import jakarta.annotation.Resource; /** * @author danmo diff --git a/dy-java-tiktok/pom.xml b/dy-java-tiktok/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..71eac23885c37b166fe014a6233002cbb00f90ee --- /dev/null +++ b/dy-java-tiktok/pom.xml @@ -0,0 +1,34 @@ + + 4.0.0 + + com.dyj + dy-java + ${revision} + + + dy-java-tiktok + jar + dy-java-tiktok + + + + + + com.dyj + dy-java-spring + ${revision} + compile + + + com.dyj + dy-java-common + ${revision} + compile + + + org.projectlombok + lombok + + + diff --git a/dy-java-tiktok/src/main/java/com/dyj/tiktok/DyTiktokClient.java b/dy-java-tiktok/src/main/java/com/dyj/tiktok/DyTiktokClient.java new file mode 100644 index 0000000000000000000000000000000000000000..6b163874f0c44b786455396be1c8e3e8cf0c0267 --- /dev/null +++ b/dy-java-tiktok/src/main/java/com/dyj/tiktok/DyTiktokClient.java @@ -0,0 +1,151 @@ +package com.dyj.tiktok; + +import com.dyj.common.client.BaseClient; +import com.dyj.common.config.AgentConfiguration; +import com.dyj.common.config.DyConfiguration; +import com.dyj.common.domain.*; +import com.dyj.common.domain.vo.*; +import com.dyj.common.utils.DyConfigUtils; +import com.dyj.tiktok.domain.vo.*; +import com.dyj.tiktok.handler.AccessTokenHandler; +import com.dyj.tiktok.handler.UserHandler; +import com.dyj.tiktok.handler.VideoHandler; +import org.apache.commons.codec.digest.DigestUtils; + +import java.io.File; +import java.io.InputStream; +import java.util.List; + +/** + * @author danmo + * @date 2024-04-03 17:25 + **/ +public class DyTiktokClient extends BaseClient { + + public DyTiktokClient() { + } + + public static DyTiktokClient getInstance() { + return new DyTiktokClient(); + } + + public DyTiktokClient tenantId(Integer tenantId) { + super.tenantId = tenantId; + return this; + } + + public DyTiktokClient clientKey(String clientKey) { + this.clientKey = clientKey; + return this; + } + + private final String AUTHORIZE_URL = "https://www.tiktok.com/v2/auth/authorize"; + + public String getPreAuthUrl(String redirectUri,String scope){ + List agents = DyConfigUtils.getAgents(); + String clientKey = agents.get(0).getClientKey(); + return AUTHORIZE_URL + "?client_key=" + clientKey + + "&redirect_uri=" + redirectUri + + "&scope=" + scope + + "&response_type=code" + + "&state="+ DigestUtils.md5Hex(redirectUri); + } + + /** + * 回调事件签名验证 + * + * @param signature 签名 + * @param wholeStr 消息体字符串 + * @return 验证结果 + */ + public Boolean checkSign(String signature, String wholeStr) { + AgentConfiguration agentConfiguration = configuration().getAgentConfigService().loadAgentByTenantId(tenantId, clientKey); + String data = agentConfiguration.getClientSecret() + wholeStr; + String sign = DigestUtils.sha1Hex(data); + if (!sign.equals(signature)) { + return false; + } + return true; + } + + /** + * 通过代码获取访问令牌。 + * + * @param code 用户授权后返回的授权码。 + * @return 返回一个包含访问令牌信息的结果对象。 + */ + public AccessTikTokTokenVo accessToken(String code, String redirect_uri) { + AgentConfiguration agentConfiguration = configuration().getAgentConfigService().loadAgentByTenantId(tenantId, clientKey); + // 使用配置信息和授权码获取访问令牌 + return new AccessTokenHandler(agentConfiguration).getAccessToken(code, redirect_uri); + } + + /** + * 刷新访问令牌。 + * 本方法用于根据租户ID和应用ID获取新的访问令牌。 + * + * @return 返回一个包含刷新后的访问令牌信息的结果对象。 + */ + public AccessTikTokTokenVo refreshToken(String openId) { + DyConfiguration configuration = configuration(); + AgentConfiguration agentConfiguration = configuration.getAgentConfigService().loadAgentByTenantId(tenantId, clientKey); + UserTokenInfo userTokenInfo = configuration.getAgentTokenService().getUserTokenInfo(agentConfiguration.getTenantId(), agentConfiguration.getClientKey(), openId); + // 利用配置信息和授权码获取新的访问令牌 + return new AccessTokenHandler(agentConfiguration).refreshToken(userTokenInfo.getRefreshToken(), openId); + } + + /** + * 获取用户信息的函数,支持指定租户ID和客户端ID。 + * + * @param openId 用户的唯一标识。 + * @return 返回一个包含用户信息的DyResult对象。 + */ + public DyTikTokResult getUserInfo(String openId) { + // 根据配置获取Agent配置 + DyConfiguration configuration = configuration(); + AgentConfiguration agentConfiguration = configuration.getAgentConfigService().loadAgentByTenantId(tenantId, clientKey); + // 使用Agent配置和用户ID获取用户信息 + return new UserHandler(agentConfiguration).getUserInfo(openId); + } + + /** + * 查询授权账号视频列表。 + * + * @param openId 用户ID。 + * @param cursor 分页游标。 + * @param count 每页数量。 + * @return DyResult。 + */ + public DyTikTokResult queryVideoList(String openId, Long cursor, Integer count) { + return new VideoHandler(configuration().getAgentConfigService().loadAgentByTenantId(tenantId, clientKey)).queryVideoList(openId, cursor, count); + } + + /** + * 初始化上传视频 + */ + public DyTikTokResult initUploadVideo(String openId, InitUploadVideoReqVo initUploadVideoReqVo) { + return new VideoHandler(configuration().getAgentConfigService().loadAgentByTenantId(tenantId, clientKey)).initUploadVideo(openId, initUploadVideoReqVo); + } + + /** + * 分片上传草稿箱 + */ + public Boolean uploadVideo(String upload_url, InputStream fileInputStream, Long fileSize) { + return new VideoHandler(configuration().getAgentConfigService().loadAgentByTenantId(tenantId, clientKey)).uploadVideo(upload_url, fileInputStream, fileSize); + } + + /** + * 上传图片 + */ + public DyTikTokResult uploadImage(String openId, UploadImageReqVo uploadImageReqVo) { + return new VideoHandler(configuration().getAgentConfigService().loadAgentByTenantId(tenantId, clientKey)).uploadImage(openId, uploadImageReqVo); + } + + /** + * 查询上传进度 + */ + public DyTikTokResult queryPostStatus(String openId, String publishId){ + return new VideoHandler(configuration().getAgentConfigService().loadAgentByTenantId(tenantId, clientKey)).queryPostStatus(openId, publishId); + } + +} diff --git a/dy-java-tiktok/src/main/java/com/dyj/tiktok/client/UserClient.java b/dy-java-tiktok/src/main/java/com/dyj/tiktok/client/UserClient.java new file mode 100644 index 0000000000000000000000000000000000000000..d21f756f0b9a031602d6282d7cdbe8c1ec83b0e0 --- /dev/null +++ b/dy-java-tiktok/src/main/java/com/dyj/tiktok/client/UserClient.java @@ -0,0 +1,21 @@ +package com.dyj.tiktok.client; + +import com.dtflys.forest.annotation.*; +import com.dyj.common.domain.DyTikTokResult; +import com.dyj.common.domain.query.UserInfoQuery; +import com.dyj.common.interceptor.TikTokBodyTokenInterceptor; +import com.dyj.tiktok.domain.vo.TikTokInfoVo; + +/** + * @author danmo + * @date 2024-04-07 14:49 + **/ +@BaseRequest(baseURL = "${tikTokDomain}") +public interface UserClient { + + @Get(url = "${tikTokUserInfo}", interceptor = TikTokBodyTokenInterceptor.class) + DyTikTokResult getUserInfo(UserInfoQuery query, @Query("fields") String fields); + + + +} diff --git a/dy-java-tiktok/src/main/java/com/dyj/tiktok/client/VideoClient.java b/dy-java-tiktok/src/main/java/com/dyj/tiktok/client/VideoClient.java new file mode 100644 index 0000000000000000000000000000000000000000..0df780dd72f8738c6855d80d7272530ed675053c --- /dev/null +++ b/dy-java-tiktok/src/main/java/com/dyj/tiktok/client/VideoClient.java @@ -0,0 +1,29 @@ +package com.dyj.tiktok.client; + +import com.dtflys.forest.annotation.*; +import com.dtflys.forest.backend.ContentType; +import com.dyj.common.domain.DyTikTokResult; +import com.dyj.common.domain.query.UserInfoQuery; +import com.dyj.common.interceptor.TikTokBodyTokenInterceptor; +import com.dyj.tiktok.domain.vo.*; + +import java.io.File; + +@BaseRequest(baseURL = "${tikTokDomain}") +public interface VideoClient { + + @Post(url = "${tikTokQueryVideoList}", contentType = ContentType.APPLICATION_JSON, interceptor = TikTokBodyTokenInterceptor.class) + DyTikTokResult queryVideoList(@Var("query") UserInfoQuery query, @Body("cursor") Long cursor, @Body("max_count") Integer count, @Query("fields") String fields); + + @Post(url = "${tikTokInitUploadVideo}", contentType = ContentType.APPLICATION_JSON, interceptor = TikTokBodyTokenInterceptor.class) + DyTikTokResult initUploadVideo(@Var("query") UserInfoQuery query, @Body InitUploadVideoReqVo initUploadVideoReqVo); + + @Put(url = "{uploadUrl}",contentType = "video/mp4", headers = {"Content-Length: {contentLength}","Content-Range: {contentRange}"}) + void uploadVideo(@Var("uploadUrl") String uploadUrl, @Var("contentLength") Long contentLength, @Var("contentRange") String contentRange, @DataFile("data") File videoFile); + + @Post(url = "${tikTokUploadImage}", contentType = ContentType.APPLICATION_JSON, interceptor = TikTokBodyTokenInterceptor.class) + DyTikTokResult uploadImage(@Var("query") UserInfoQuery query, @Body UploadImageReqVo uploadImageReqVo); + + @Post(url = "${tikTokQueryPostStatus}", contentType = ContentType.APPLICATION_JSON, interceptor = TikTokBodyTokenInterceptor.class) + DyTikTokResult queryPostStatus(@Var("query")UserInfoQuery query, @Body("publish_id") String publishId); +} diff --git a/dy-java-tiktok/src/main/java/com/dyj/tiktok/domain/vo/InitUploadVideoReqVo.java b/dy-java-tiktok/src/main/java/com/dyj/tiktok/domain/vo/InitUploadVideoReqVo.java new file mode 100644 index 0000000000000000000000000000000000000000..63c728378bc818b20515b163a09eb91d59e5bd6e --- /dev/null +++ b/dy-java-tiktok/src/main/java/com/dyj/tiktok/domain/vo/InitUploadVideoReqVo.java @@ -0,0 +1,151 @@ +package com.dyj.tiktok.domain.vo; + +import com.alibaba.fastjson.annotation.JSONField; +import com.dyj.tiktok.enums.PrivacyLevelEnum; +import com.dyj.tiktok.enums.UploadSourceEnum; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serial; +import java.io.Serializable; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +public class InitUploadVideoReqVo implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @JSONField(name = "post_info") + private PostInfo postInfo; + + @JSONField(name = "source_info") + private SourceInfo sourceInfo; + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + @Accessors(chain = true) + public static class PostInfo implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * (必填) + * 隐私等级: + * PUBLIC_TO_EVERYONE: 向所有人公开 + * MUTUAL_FOLLOW_FRIENDS: 互相关注的人 + * FOLLOWER_OF_CREATOR: 粉丝 + * SELF_ONLY: 仅自己可见 + * 与查询创作者信息接口返回的privacy_level_options的值之一比配 + * 枚举 {@link PrivacyLevelEnum} + */ + @JSONField(name = "privacy_level") + private String privacyLevel; + + /** + * 标题 + */ + private String title; + + /** + * 是否禁止Duet + * Duet是TikTok上的一项功能,允许用户使用其他TikToker的视频创建内容。这两个视频以分屏并排显示,并将同时播放,这与Stitch不同,在Stitch中,两个剪辑被“拼接”在一起形成一个画面,并一个接一个地播放。 + */ + @JSONField(name = "disable_duet") + private Boolean disableDuet; + + /** + * 是否禁止Stitch + * Stitch是TikTok平台上的一种合作内容,意为“缝合”。它允许用户使用其他创作者拍摄的视频(最多5秒),并在该原视频的基础上添加新内容,创作出新的视频。 + */ + @JSONField(name = "disable_stitch") + private Boolean disableStitch; + + /** + * 是否禁用评论 + */ + @JSONField(name = "disable_comment") + private Boolean disableComment; + + /** + * 视频封面帧数(毫秒) + * 指定哪一帧(以毫秒为单位)将用作视频封面。 + * 如果未设置,或者指定的值无效,则将封面设置为上传视频的第一帧。 + */ + @JSONField(name = "video_cover_timestamp_ms") + private Integer videoCoverTimestampMs; + + /** + * 品牌内容切换 + * 如果视频是为了推广第三方业务而进行的付费合作,则设置为true + */ + @JSONField(name = "brand_content_toggle") + private Boolean brandContentToggle; + + /** + * 是否宣传创作者自己的业务 + * 如果此视频是为了宣传创作者自己的业务,则设置为true + */ + @JSONField(name = "brand_organic_toggle") + private Boolean brandOrganicToggle; + + /** + * 是否ai生成 + * 如果视频是 AI 生成的内容,则设置为则设置为true + * 如果设置,视频描述中将会标有标签 + */ + @JSONField(name = "is_aigc") + private Boolean isAigc; + + } + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + @Accessors(chain = true) + public static class SourceInfo implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 来源(FILE_UPLOAD、PULL_FROM_URL) + * 枚举 {@link UploadSourceEnum} + */ + private String source; + + /** + * 视频大小;source=FILE_UPLOAD 时必填 + */ + @JSONField(name = "video_size") + private Long videoSize; + + /** + * 分片大小;source=FILE_UPLOAD 时必填 + */ + @JSONField(name = "chunk_size") + private Long chunkSize; + + /** + * 总分片次数;source=FILE_UPLOAD 时必填 + */ + @JSONField(name = "total_chunk_count") + private Integer totalChunkCount; + + /** + * 视频网址;source=PULL_FROM_URL 时必填,video_url 的域名或网址前缀应该已经验证 + */ + @JSONField(name = "video_url") + private String videoUrl; + } +} diff --git a/dy-java-tiktok/src/main/java/com/dyj/tiktok/domain/vo/InitUploadVideoRespVo.java b/dy-java-tiktok/src/main/java/com/dyj/tiktok/domain/vo/InitUploadVideoRespVo.java new file mode 100644 index 0000000000000000000000000000000000000000..208551ecb95ba94ec416ea7cf0a430ec6b340515 --- /dev/null +++ b/dy-java-tiktok/src/main/java/com/dyj/tiktok/domain/vo/InitUploadVideoRespVo.java @@ -0,0 +1,36 @@ +package com.dyj.tiktok.domain.vo; + +import com.alibaba.fastjson.annotation.JSONField; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serial; +import java.io.Serializable; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +public class InitUploadVideoRespVo implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 发布ID + */ + @JSONField(name = "publish_id") + private String publishId; + + /** + * 上传地址 + * TikTok 提供的可上传视频文件的 URL + */ + @JSONField(name = "upload_url") + private String uploadUrl; + +} diff --git a/dy-java-tiktok/src/main/java/com/dyj/tiktok/domain/vo/QueryPostStatusRespVo.java b/dy-java-tiktok/src/main/java/com/dyj/tiktok/domain/vo/QueryPostStatusRespVo.java new file mode 100644 index 0000000000000000000000000000000000000000..4ba015f325573094b1803fb01f21e7976aaa1fcd --- /dev/null +++ b/dy-java-tiktok/src/main/java/com/dyj/tiktok/domain/vo/QueryPostStatusRespVo.java @@ -0,0 +1,66 @@ +package com.dyj.tiktok.domain.vo; + +import com.alibaba.fastjson.annotation.JSONField; +import com.dyj.tiktok.enums.PostStatusEnum; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serial; +import java.io.Serializable; +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +public class QueryPostStatusRespVo implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 状态 + * PROCESSING_UPLOAD: 仅适用于FILE_UPLOAD。表示上传正在进行中。 + * PROCESSING_DOWNLOAD: 仅适用于PULL_FROM_URL。表示正在从 URL 下载。 + * SEND_TO_USER_INBOX: 仅当您选择上传内容时可用。表示已向创作者的收件箱发送通知,以使用 TikTok 的编辑流程完成草稿帖子。 + * PUBLISH_COMPLETE: 对于直接发布,表示内容已发布。对于上传内容,表示用户已点击收件箱通知,并已使用TikTok编辑流程成功发布媒体。 + * FAILED: 表示发生了错误,整个过程失败。 + * + * 枚举 {@link PostStatusEnum} + */ + private String status; + + /** + * 失败原因 + */ + @JSONField(name = "fail_reason") + private String failReason; + + /** + * 公开可用的发布id + * post_id仅当帖子发布供公众查看并且已通过 TikTok 审核流程批准时才会返回。 + * 创作者可以使用上传的内容草稿来创作多篇内容。 + */ + @JSONField(name = "publicaly_available_post_id") + private List publicalyAvailablePostId; + + /** + * 仅适用于 FILE_UPLOAD + * 已上传的字节数 + */ + @JSONField(name = "uploaded_bytes") + private Long uploadedBytes; + + /** + * 仅适用于 PULL_FROM_URL + * 下载的字节数 + */ + @JSONField(name = "downloaded_bytes") + private Long downloadedBytes; + + +} diff --git a/dy-java-tiktok/src/main/java/com/dyj/tiktok/domain/vo/QueryVideoListVo.java b/dy-java-tiktok/src/main/java/com/dyj/tiktok/domain/vo/QueryVideoListVo.java new file mode 100644 index 0000000000000000000000000000000000000000..56fa63662f3fb28472eebc4796cd9a0452507e48 --- /dev/null +++ b/dy-java-tiktok/src/main/java/com/dyj/tiktok/domain/vo/QueryVideoListVo.java @@ -0,0 +1,136 @@ +package com.dyj.tiktok.domain.vo; + +import com.alibaba.fastjson.annotation.JSONField; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serial; +import java.io.Serializable; +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +public class QueryVideoListVo implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 视频列表 + */ + private List videos; + + /** + * 分页的游标 + */ + private Long cursor; + + /** + * 是否有更多视频 + */ + @JSONField(name = "has_more") + private Boolean hasMore; + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + @Accessors(chain = true) + public static class Videos implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * TikTok 视频的唯一标识符 + */ + private String id; + + /** + * 创建时间 + */ + @JSONField(name = "create_time") + private Long createTime; + + /** + * 封面图片网址 + */ + @JSONField(name = "cover_image_url") + private String coverImageUrl; + + /** + * 分享网址 + */ + @JSONField(name = "share_url") + private String shareUrl; + + /** + * 视频描述 + */ + @JSONField(name = "video_description") + private String videoDescription; + + /** + * 视频的时长(以秒为单位) + */ + private Integer duration; + + /** + * 视频的高度 + */ + private Integer height; + + /** + * 视频的宽度 + */ + private Integer width; + + /** + * 标题 + */ + private String title; + + /** + * 嵌入HTML + */ + @JSONField(name = "embed_html") + private String embedHtml; + + /** + * 嵌入链接 + */ + @JSONField(name = "embed_link") + private String embedLink; + + /** + * 点赞数 + */ + @JSONField(name = "like_count") + private Integer likeCount; + + /** + * 评论数 + */ + @JSONField(name = "comment_count") + private Integer commentCount; + + /** + * 分享数量 + */ + @JSONField(name = "share_count") + private Integer shareCount; + + /** + * 观看次数 + */ + @JSONField(name = "view_count") + private Integer viewCount; + + } +} diff --git a/dy-java-tiktok/src/main/java/com/dyj/tiktok/domain/vo/TikTokInfoVo.java b/dy-java-tiktok/src/main/java/com/dyj/tiktok/domain/vo/TikTokInfoVo.java new file mode 100644 index 0000000000000000000000000000000000000000..0e0a208e9d05a0dee69c052a9082dd0ee5b02b4c --- /dev/null +++ b/dy-java-tiktok/src/main/java/com/dyj/tiktok/domain/vo/TikTokInfoVo.java @@ -0,0 +1,115 @@ +package com.dyj.tiktok.domain.vo; + +import com.alibaba.fastjson.annotation.JSONField; +import com.dyj.common.domain.vo.BaseVo; +import lombok.*; +import lombok.experimental.Accessors; + +import java.io.Serial; +import java.io.Serializable; + +/** + * @author danmo + * @date 2024-04-07 14:56 + **/ + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@EqualsAndHashCode(callSuper = true) +public class TikTokInfoVo extends BaseVo implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + private User user; + + @Data + public static class User{ + /** + * 用户在当前应用的唯一标识 + */ + @JSONField(name = "open_id") + private String openId; + + /** + * 用户在当前开发者账号下的唯一标识(未绑定开发者账号没有该字段) + */ + @JSONField(name = "union_id") + private String unionId; + + /** + * 头像url + */ + @JSONField(name = "avatar_url") + private String avatarUrl; + + /** + * 头像url(100x100 尺寸) + */ + @JSONField(name = "avatar_url_100") + private String avatarUrl100; + + /** + * 大头像url + */ + @JSONField(name = "avatar_large_url") + private String avatarLargeUrl; + + /** + * 个人资料名称 + */ + @JSONField(name = "display_name") + private String displayName; + + /** + * 描述 + */ + @JSONField(name = "bio_description") + private String bioDescription; + + /** + * 个人资料页面的链接 + */ + @JSONField(name = "profile_deep_link") + private String profileDeepLink; + + /** + * 是否验证 + */ + @JSONField(name = "is_verified") + private Boolean isVerified; + + /** + * 用户名 + */ + private String username; + + /** + * 粉丝数 + */ + @JSONField(name = "follower_count") + private Integer followerCount; + + /** + * 关注人数 + */ + @JSONField(name = "following_count") + private Integer followingCount; + + /** + * 点赞数 + */ + @JSONField(name = "likes_count") + private Integer likesCount; + + /** + * 视频数量 + */ + @JSONField(name = "video_count") + private Integer videoCount; + } + +} diff --git a/dy-java-tiktok/src/main/java/com/dyj/tiktok/domain/vo/UploadImageReqVo.java b/dy-java-tiktok/src/main/java/com/dyj/tiktok/domain/vo/UploadImageReqVo.java new file mode 100644 index 0000000000000000000000000000000000000000..196457d588cead3e2abdcd4487bb5c3c8530b991 --- /dev/null +++ b/dy-java-tiktok/src/main/java/com/dyj/tiktok/domain/vo/UploadImageReqVo.java @@ -0,0 +1,150 @@ +package com.dyj.tiktok.domain.vo; + +import com.alibaba.fastjson.annotation.JSONField; +import com.dyj.tiktok.enums.PostModeEnum; +import com.dyj.tiktok.enums.PrivacyLevelEnum; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serial; +import java.io.Serializable; +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +public class UploadImageReqVo implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 必填 + * 媒体类型:目前仅PHOTO允许 + */ + @JSONField(name = "media_type") + private String mediaType = "PHOTO"; + + /** + * 必填 + * 发布模式: + * DIRECT POST: 直接将内容发布到TikTok用户的帐户 + * MEDIA_UPLOAD: 将内容上传至TikTok,用户可使用TikTok的编辑流程完成帖子。用户将收到收件箱通知。 + * 枚举 {@link PostModeEnum} + */ + @JSONField(name = "post_mode") + private String postMode; + + /** + * 必填 + * 发布信息 + */ + @JSONField(name = "post_info") + private PostInfo postInfo; + + /** + * 必填 + * 图片信息 + */ + @JSONField(name = "source_info") + private SourceInfo sourceInfo; + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + @Accessors(chain = true) + public static class PostInfo implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + + /** + * 标题 + */ + private String title; + + /** + * 描述 + */ + private String description; + + /** + * 发布模式(postMode)为: DIRECT POST(必填) + * 隐私等级: + * PUBLIC_TO_EVERYONE: 向所有人公开 + * MUTUAL_FOLLOW_FRIENDS: 互相关注的人 + * FOLLOWER_OF_CREATOR: 粉丝 + * SELF_ONLY: 仅自己可见 + * 与查询创作者信息接口返回的privacy_level_options的值之一比配 + * 枚举 {@link PrivacyLevelEnum} + */ + @JSONField(name = "privacy_level") + private String privacyLevel; + + /** + * 仅适用于 发布模式(postMode)为: DIRECT POST + * 是否禁用评论 + */ + @JSONField(name = "disable_comment") + private Boolean disableComment; + + /** + * 仅适用于 发布模式(postMode)为: DIRECT POST + * 是否自动添加音乐 + * 如果设置为true,推荐的音乐将自动添加到照片中,并且如果用户喜欢其他音乐,他们可以稍后选择在 TikTok 中更改帖子的音乐。 + */ + @JSONField(name = "auto_add_music") + private Boolean autoAddMusic; + + /** + * 仅适用于 发布模式(postMode)为: DIRECT POST + * 如果内容是付费合作以推广第三方业务,则设置为 true + */ + @JSONField(name = "brand_content_toggle") + private Boolean brandContentToggle; + + /** + * 仅适用于 发布模式(postMode)为: DIRECT POST + * 如果此内容是为了宣传创作者自己的业务,则设置为true + */ + @JSONField(name = "brand_organic_toggle") + private Boolean brandOrganicToggle; + } + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + @Accessors(chain = true) + public static class SourceInfo implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + + /** + * 必填 + * 来源:仅 PULL_FROM_URL 允许 + */ + @JSONField(name = "source") + private String source = "PULL_FROM_URL"; + + /** + * 照片图像url集合 + * 最多包含 35 个照片内容 URL 的数组。这些 URL 必须可公开访问,并经过您的应用验证。 + */ + @JSONField(name = "photo_images") + private List photoImages; + + /** + * 照片封面索引 + * 表示用作封面的照片的索引(从0开始) + * 如果是0,则取 photoImages 中的第一张图片为封面 + */ + @JSONField(name = "photo_cover_index") + private Integer photoCoverIndex; + } +} diff --git a/dy-java-tiktok/src/main/java/com/dyj/tiktok/domain/vo/UploadImageRespVo.java b/dy-java-tiktok/src/main/java/com/dyj/tiktok/domain/vo/UploadImageRespVo.java new file mode 100644 index 0000000000000000000000000000000000000000..55ac1d79e9c963840939878cd68d55b638e53625 --- /dev/null +++ b/dy-java-tiktok/src/main/java/com/dyj/tiktok/domain/vo/UploadImageRespVo.java @@ -0,0 +1,29 @@ +package com.dyj.tiktok.domain.vo; + +import com.alibaba.fastjson.annotation.JSONField; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.Serial; +import java.io.Serializable; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +public class UploadImageRespVo implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 发布ID + */ + @JSONField(name = "publish_id") + private String publishId; + +} diff --git a/dy-java-tiktok/src/main/java/com/dyj/tiktok/enums/PostModeEnum.java b/dy-java-tiktok/src/main/java/com/dyj/tiktok/enums/PostModeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..43cd322ae4a4cd8c388b5d2eb0b86c33fbc2e322 --- /dev/null +++ b/dy-java-tiktok/src/main/java/com/dyj/tiktok/enums/PostModeEnum.java @@ -0,0 +1,25 @@ +package com.dyj.tiktok.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 发布模式 枚举 + */ +@Getter +@AllArgsConstructor +public enum PostModeEnum { + + /** + * 直接将内容发布到TikTok用户的帐户 + */ + DIRECT_POST("DIRECT POST"), + + /** + * 将内容上传至TikTok,用户可使用TikTok的编辑流程完成帖子。用户将收到收件箱通知。 + */ + MEDIA_UPLOAD("MEDIA_UPLOAD"); + + private final String postMode; + +} diff --git a/dy-java-tiktok/src/main/java/com/dyj/tiktok/enums/PostStatusEnum.java b/dy-java-tiktok/src/main/java/com/dyj/tiktok/enums/PostStatusEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..28df9499a8f7720220e665c8ce5e491776df5a87 --- /dev/null +++ b/dy-java-tiktok/src/main/java/com/dyj/tiktok/enums/PostStatusEnum.java @@ -0,0 +1,49 @@ +package com.dyj.tiktok.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Objects; + +/** + * 发布状态 枚举 + */ +@Getter +@AllArgsConstructor +public enum PostStatusEnum { + + /** + * 仅适用于FILE_UPLOAD。表示上传正在进行中 + */ + PROCESSING_UPLOAD("PROCESSING_UPLOAD"), + + /** + * 仅适用于PULL_FROM_URL。表示正在从 URL 下载。 + */ + PROCESSING_DOWNLOAD("PROCESSING_DOWNLOAD"), + + /** + * 仅当您选择上传内容时可用。表示已向创作者的收件箱发送通知,以使用 TikTok 的编辑流程完成草稿帖子。 + */ + SEND_TO_USER_INBOX("SEND_TO_USER_INBOX"), + + /** + * 对于直接发布,表示内容已发布。对于上传内容,表示用户已点击收件箱通知,并已使用TikTok编辑流程成功发布媒体。 + */ + PUBLISH_COMPLETE("PUBLISH_COMPLETE"), + + /** + * 表示发生了错误,整个过程失败。 + */ + FAILED("FAILED"); + + /** + * 状态值 + */ + private final String code; + + public static Boolean isFailed(String code) { + return Objects.equals(FAILED.getCode(), code); + } + +} diff --git a/dy-java-tiktok/src/main/java/com/dyj/tiktok/enums/PrivacyLevelEnum.java b/dy-java-tiktok/src/main/java/com/dyj/tiktok/enums/PrivacyLevelEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..a720511a56f4b5f987b486340e56c1867e78a818 --- /dev/null +++ b/dy-java-tiktok/src/main/java/com/dyj/tiktok/enums/PrivacyLevelEnum.java @@ -0,0 +1,24 @@ +package com.dyj.tiktok.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 隐私等级 枚举 + */ +@Getter +@AllArgsConstructor +public enum PrivacyLevelEnum { + + PUBLIC_TO_EVERYONE("PUBLIC_TO_EVERYONE"), + + MUTUAL_FOLLOW_FRIENDS("MUTUAL_FOLLOW_FRIENDS"), + + FOLLOWER_OF_CREATOR("FOLLOWER_OF_CREATOR"), + + SELF_ONLY("SELF_ONLY"); + + + private final String privacyLevel; + +} diff --git a/dy-java-tiktok/src/main/java/com/dyj/tiktok/enums/TiktokScopeEnum.java b/dy-java-tiktok/src/main/java/com/dyj/tiktok/enums/TiktokScopeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..3bbe4de634290933278b4d31e71aab26b309ecd2 --- /dev/null +++ b/dy-java-tiktok/src/main/java/com/dyj/tiktok/enums/TiktokScopeEnum.java @@ -0,0 +1,29 @@ +package com.dyj.tiktok.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 授权范围 枚举 + */ +@Getter +@AllArgsConstructor +public enum TiktokScopeEnum { + + USER_INFO_BASIC("user.info.basic", "用户信息(基本信息)"), + USER_INFO_PROFILE("user.info.profile", "用户信息(个人资料)"), + USER_INFO_STATS("user.info.stats","用户信息统计"), + VIDEO_LIST("video.list", "视频列表查询"), + VIDEO_PUBLISH("video.publish", "视频发布"), + VIDEO_UPLOAD("video.upload", "视频上传"); + + /** + * 状态值 + */ + private final String status; + /** + * 状态名 + */ + private final String name; + +} diff --git a/dy-java-tiktok/src/main/java/com/dyj/tiktok/enums/UploadSourceEnum.java b/dy-java-tiktok/src/main/java/com/dyj/tiktok/enums/UploadSourceEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..7f0035a46cf03c31115d7c9ffc88510e6391ccf4 --- /dev/null +++ b/dy-java-tiktok/src/main/java/com/dyj/tiktok/enums/UploadSourceEnum.java @@ -0,0 +1,21 @@ +package com.dyj.tiktok.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 视频来源 枚举 + */ +@Getter +@AllArgsConstructor +public enum UploadSourceEnum { + + FILE_UPLOAD("FILE_UPLOAD", "文件上传"), + + PULL_FROM_URL("PULL_FROM_URL", "从URL中提取"); + + private final String value; + + private final String desc; + +} diff --git a/dy-java-tiktok/src/main/java/com/dyj/tiktok/handler/AbstractTikTokHandler.java b/dy-java-tiktok/src/main/java/com/dyj/tiktok/handler/AbstractTikTokHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..b3b7b84fe4e01e7c778ef58d8f6876fc91982822 --- /dev/null +++ b/dy-java-tiktok/src/main/java/com/dyj/tiktok/handler/AbstractTikTokHandler.java @@ -0,0 +1,29 @@ +package com.dyj.tiktok.handler; + +import com.dyj.common.client.TikTokAuthClient; +import com.dyj.common.config.AgentConfiguration; +import com.dyj.spring.utils.SpringUtils; +import com.dyj.tiktok.client.UserClient; +import com.dyj.tiktok.client.VideoClient; + +public abstract class AbstractTikTokHandler { + + protected final AgentConfiguration agentConfiguration; + + public AbstractTikTokHandler(AgentConfiguration agentConfiguration) { + this.agentConfiguration = agentConfiguration; + } + + protected TikTokAuthClient getTikTokAuthClient() { + return SpringUtils.getBean(TikTokAuthClient.class); + } + + protected UserClient getUserClient() { + return SpringUtils.getBean(UserClient.class); + } + + protected VideoClient getVideoClient() { + return SpringUtils.getBean(VideoClient.class); + } + +} diff --git a/dy-java-tiktok/src/main/java/com/dyj/tiktok/handler/AccessTokenHandler.java b/dy-java-tiktok/src/main/java/com/dyj/tiktok/handler/AccessTokenHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..9b08c086904d42c51463eb6deb033dbb54d6ec9e --- /dev/null +++ b/dy-java-tiktok/src/main/java/com/dyj/tiktok/handler/AccessTokenHandler.java @@ -0,0 +1,32 @@ +package com.dyj.tiktok.handler; + +import com.dyj.common.config.AgentConfiguration; +import com.dyj.common.domain.vo.AccessTikTokTokenVo; +import com.dyj.common.utils.DyConfigUtils; + +import java.util.Objects; + +public class AccessTokenHandler extends AbstractTikTokHandler { + + public AccessTokenHandler(AgentConfiguration agentConfiguration) { + super(agentConfiguration); + } + + public AccessTikTokTokenVo getAccessToken(String code, String redirect_uri) { + AccessTikTokTokenVo result = getTikTokAuthClient().getAccessToken(agentConfiguration.getClientKey(),agentConfiguration.getClientSecret(), code, "authorization_code", redirect_uri); + if (Objects.nonNull(result) && Objects.isNull(result.getLogId())) { + DyConfigUtils.getAgentTokenService().setUserTokenInfo(agentConfiguration.getTenantId(), agentConfiguration.getClientKey(), result.getAccessToken(), result.getExpiresIn(), result.getRefreshToken(), result.getRefreshExpiresIn(), result.getOpenId()); + return result; + }else{ + return null; + } + } + + public AccessTikTokTokenVo refreshToken(String refreshToken, String openId) { + AccessTikTokTokenVo result = getTikTokAuthClient().refreshToken(agentConfiguration.getClientKey(),agentConfiguration.getClientSecret(), "refresh_token", refreshToken); + if (Objects.nonNull(result) && Objects.isNull(result.getLogId())) { + DyConfigUtils.getAgentTokenService().updateRefreshTokenInfo(agentConfiguration.getTenantId(), agentConfiguration.getClientKey(), result.getRefreshToken(), result.getExpiresIn(), openId); + } + return result; + } +} diff --git a/dy-java-tiktok/src/main/java/com/dyj/tiktok/handler/UserHandler.java b/dy-java-tiktok/src/main/java/com/dyj/tiktok/handler/UserHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..f2f3bc08902e553004814215ae93fe031f4aab24 --- /dev/null +++ b/dy-java-tiktok/src/main/java/com/dyj/tiktok/handler/UserHandler.java @@ -0,0 +1,23 @@ +package com.dyj.tiktok.handler; + +import com.dyj.common.config.AgentConfiguration; +import com.dyj.common.domain.DyTikTokResult; +import com.dyj.common.domain.query.UserInfoQuery; +import com.dyj.tiktok.domain.vo.TikTokInfoVo; + +public class UserHandler extends AbstractTikTokHandler{ + + public UserHandler(AgentConfiguration agentConfiguration) { + super(agentConfiguration); + } + + private final static String FIEIDS = "open_id,union_id,avatar_url,avatar_url_100,avatar_large_url,display_name,bio_description,profile_deep_link,is_verified,username,follower_count,following_count,likes_count,video_count"; + + public DyTikTokResult getUserInfo(String openId) { + UserInfoQuery query = new UserInfoQuery(); + query.setOpen_id(openId); + query.setTenantId(agentConfiguration.getTenantId()); + query.setClientKey(agentConfiguration.getClientKey()); + return getUserClient().getUserInfo(query, FIEIDS); + } +} diff --git a/dy-java-tiktok/src/main/java/com/dyj/tiktok/handler/VideoHandler.java b/dy-java-tiktok/src/main/java/com/dyj/tiktok/handler/VideoHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..3966a1eddeac299b8fe5a6b2a64f74178dbb673b --- /dev/null +++ b/dy-java-tiktok/src/main/java/com/dyj/tiktok/handler/VideoHandler.java @@ -0,0 +1,220 @@ +package com.dyj.tiktok.handler; + +import com.dyj.common.config.AgentConfiguration; +import com.dyj.common.domain.DyTikTokResult; +import com.dyj.common.domain.query.UserInfoQuery; +import com.dyj.common.utils.FieldUtils; +import com.dyj.tiktok.domain.vo.*; + +import java.io.File; +import java.io.InputStream; + +public class VideoHandler extends AbstractTikTokHandler { + + /** + * 分片大小(5MB) + * TikTok API分片要求: + * 1. 每个块必须至少为5MB但不超过64MB,最后一个块除外 + * 2. 最后一个块可以大于chunk_size(最多128MB)以容纳尾随字节 + * 3. 总大小小于5MB的视频必须整体上传,chunk_size等于整个视频的字节大小 + * 4. 总大小大于64MB的视频必须分多个块上传 + * 5. 必须至少有1个块,最多有1000个块 + * 6. 文件块必须按顺序上传 + * 7. total_chunk_count应等于video_size除以chunk_size,向下舍入到最接近的整数 + * + * 当前实现策略: + * 1. 文件小于10MB(标准分片大小的2倍)时,整个文件作为一次上传 + * 2. 文件大于等于10MB时,按5MB进行分片上传 + * 3. 分片数计算使用向下舍入方式(floor) + */ + private final static long CHUNK_SIZE = 5 * 1024 * 1024; + + public VideoHandler(AgentConfiguration agentConfiguration) { + super(agentConfiguration); + } + + public DyTikTokResult queryVideoList(String openId, Long cursor, Integer count) { + UserInfoQuery query = new UserInfoQuery(); + query.setTenantId(agentConfiguration.getTenantId()); + query.setClientKey(agentConfiguration.getClientKey()); + query.setOpen_id(openId); + return getVideoClient().queryVideoList(query, cursor, count, FieldUtils.getFieldsString(QueryVideoListVo.Videos.class)); + } + + /** + * 初始化上传视频 + */ + public DyTikTokResult initUploadVideo(String openId, InitUploadVideoReqVo initUploadVideoReqVo) { + UserInfoQuery query = new UserInfoQuery(); + query.setTenantId(agentConfiguration.getTenantId()); + query.setClientKey(agentConfiguration.getClientKey()); + query.setOpen_id(openId); + return getVideoClient().initUploadVideo(query, initUploadVideoReqVo); + } + + /** + * 分片上传视频到草稿箱 + * @param uploadUrl 上传地址 + * @param fileInputStream 文件流 + * @param fileSize 上传文件大小 + * @return 上传结果 + */ + public Boolean uploadVideo(String uploadUrl, InputStream fileInputStream, Long fileSize) { + try { + // 总上传字节数 + long totalByteLength = fileSize; + + // 根据视频大小确定合适的分片大小 + long chunkSize; + + // 如果视频小于10MB(5MB的2倍),则整个视频作为一个分片上传 + if (totalByteLength < 10 * 1024 * 1024) { + chunkSize = totalByteLength; + System.out.println("文件小于10MB,整体上传,分片大小: " + chunkSize + " 字节"); + } else { + // 否则使用5MB作为分片大小 + chunkSize = CHUNK_SIZE; + System.out.println("文件大于等于10MB,按5MB分片上传"); + } + + // 分片数量 - 向下舍入到最接近的整数 + int chunks = (int) Math.floor((double) totalByteLength / chunkSize); + // 确保至少有1个分片 + chunks = Math.max(1, chunks); + + System.out.println("上传视频:总大小 " + totalByteLength + " 字节, 分片大小 " + chunkSize + " 字节, 分片数量 " + chunks); + + // 需要标记流的当前位置,以便后续分片读取 + // 注意:如果输入流不支持mark/reset,则需要创建一个支持此功能的包装流 + if (!fileInputStream.markSupported()) { + System.out.println("警告: 输入流不支持标记/重置功能,可能会导致分片读取错误"); + } + + // 按分片进行上传 + for (int i = 0; i < chunks; i++) { + // 当前分片的起始位置 + long firstByte = i * chunkSize; + + // 计算当前分片的结束位置 + long lastByte; + if (i == chunks - 1) { + // 最后一个分片可能小于标准分片大小,包含剩余所有数据 + lastByte = totalByteLength - 1; // 注意:索引从0开始 + } else { + lastByte = firstByte + chunkSize - 1; // 注意:索引从0开始 + } + + // 当前分片大小 + long currentChunkSize = lastByte - firstByte + 1; + + System.out.println("处理分片 " + (i + 1) + "/" + chunks + ": 字节范围 " + firstByte + "-" + lastByte); + + // 创建分片临时文件 + File tempChunkFile = createChunkFile(fileInputStream, firstByte, currentChunkSize); + + // 构建Content-Range头 + String contentRange = "bytes " + firstByte + "-" + lastByte + "/" + totalByteLength; + + // 上传当前分片 + getVideoClient().uploadVideo(uploadUrl, currentChunkSize, contentRange, tempChunkFile); + + // 删除临时文件 + if (tempChunkFile.exists()) { + boolean deleted = tempChunkFile.delete(); + if (!deleted) { + System.out.println("警告: 临时文件 " + tempChunkFile.getAbsolutePath() + " 删除失败"); + // 添加到JVM退出时删除的文件列表 + tempChunkFile.deleteOnExit(); + } + } + + System.out.println("上传进度: " + (i + 1) + "/" + chunks + " 分片"); + } + + return true; + } catch (Exception e) { + System.err.println("视频上传失败: " + e.getMessage()); + e.printStackTrace(); + return false; + } finally { + try { + if (fileInputStream != null) { + fileInputStream.close(); + } + } catch (Exception e) { + System.err.println("关闭文件流失败: " + e.getMessage()); + } + } + } + + /** + * 创建分片临时文件 + * @param inputStream 源文件输入流 + * @param startPos 开始位置 + * @param length 长度 + * @return 分片临时文件 + */ + private File createChunkFile(InputStream inputStream, long startPos, long length) throws Exception { + java.io.FileOutputStream fos = null; + + try { + // 创建临时文件 + File tempFile = File.createTempFile("chunk_", ".tmp"); + fos = new java.io.FileOutputStream(tempFile); + + // 跳过前面的字节 + long bytesSkipped = 0; + while (bytesSkipped < startPos) { + long skipped = inputStream.skip(startPos - bytesSkipped); + if (skipped <= 0) { + break; + } + bytesSkipped += skipped; + } + + // 写入数据 + byte[] buffer = new byte[1024 * 1024]; // 1MB缓冲区 + long remainingBytes = length; + + while (remainingBytes > 0) { + int bytesToRead = (int) Math.min(buffer.length, remainingBytes); + int bytesRead = inputStream.read(buffer, 0, bytesToRead); + + if (bytesRead == -1) { + break; + } + + fos.write(buffer, 0, bytesRead); + remainingBytes -= bytesRead; + } + + fos.flush(); + return tempFile; + } finally { + if (fos != null) { + try { + fos.close(); + } catch (Exception e) { + // 忽略 + } + } + // 注意:不关闭inputStream,由调用者负责关闭 + } + } + + public DyTikTokResult uploadImage(String openId, UploadImageReqVo uploadImageReqVo) { + UserInfoQuery query = new UserInfoQuery(); + query.setTenantId(agentConfiguration.getTenantId()); + query.setClientKey(agentConfiguration.getClientKey()); + query.setOpen_id(openId); + return getVideoClient().uploadImage(query, uploadImageReqVo); + } + + public DyTikTokResult queryPostStatus(String openId, String publishId) { + UserInfoQuery query = new UserInfoQuery(); + query.setTenantId(agentConfiguration.getTenantId()); + query.setClientKey(agentConfiguration.getClientKey()); + query.setOpen_id(openId); + return getVideoClient().queryPostStatus(query, publishId); + } +} diff --git a/pom.xml b/pom.xml index 3b1e1bb67405a3e16dc57f8067d122bd6cefb985..b7c6ccd5e31d46e4021f2fb7e847d7793c5ce7df 100644 --- a/pom.xml +++ b/pom.xml @@ -14,15 +14,18 @@ dy-java-common dy-java-web dy-java-applet + dy-java-tiktok 1.0.0 - 8 + 17 5.3.20 - 2.7.18 - 1.5.36 - 1.2.83 + 3.4.2 + 1.6.4 + 2.0.57 + 1.3.0 + 1.18.34 @@ -56,7 +59,7 @@ com.dtflys.forest - forest-spring-boot-starter + forest-spring-boot3-starter ${forest.version} @@ -73,8 +76,20 @@ ${fastjson.version} + + org.projectlombok + lombok + ${lombok.version} + + + + + nexus-releases + http://192.168.10.205:8081/repository/private-releases/ + + @@ -86,6 +101,43 @@ ${java.version} + + + org.codehaus.mojo + flatten-maven-plugin + ${flatten-maven-plugin.version} + + true + resolveCiFriendliesOnly + + + + flatten + process-resources + + flatten + + + + flatten.clean + clean + + clean + + + + + + + + public + jianghe nexus + http://192.168.10.205:8081/repository/private-releases/ + + true + + + \ No newline at end of file