|
@@ -0,0 +1,610 @@
|
|
|
|
|
+package com.fs.xiaoshouyi.service;
|
|
|
|
|
+
|
|
|
|
|
+import cn.hutool.json.JSONArray;
|
|
|
|
|
+import cn.hutool.json.JSONObject;
|
|
|
|
|
+import cn.hutool.json.JSONUtil;
|
|
|
|
|
+import com.fs.xiaoshouyi.client.XiaoShouYiHttpClient;
|
|
|
|
|
+import com.fs.xiaoshouyi.config.XiaoShouYiProperties;
|
|
|
|
|
+import com.fs.xiaoshouyi.dto.*;
|
|
|
|
|
+import lombok.RequiredArgsConstructor;
|
|
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
|
|
+
|
|
|
|
|
+import java.io.ByteArrayOutputStream;
|
|
|
|
|
+import java.io.File;
|
|
|
|
|
+import java.io.FileOutputStream;
|
|
|
|
|
+import java.io.InputStream;
|
|
|
|
|
+import java.net.HttpURLConnection;
|
|
|
|
|
+import java.net.URL;
|
|
|
|
|
+import java.util.*;
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 销售易素材服务(自动选账号、严格模式)
|
|
|
|
|
+ *
|
|
|
|
|
+ * 设计说明:
|
|
|
|
|
+ * 1. 所有业务接口统一只接收 companyUserId
|
|
|
|
|
+ * 2. 通过 companyUserId -> 绑定的 accountId 自动选账号
|
|
|
|
|
+ * 3. 不做默认账号兜底,未绑定直接报错
|
|
|
|
|
+ * 4. 所有请求统一走 executeWithRetry,减少重复代码
|
|
|
|
|
+ */
|
|
|
|
|
+@Slf4j
|
|
|
|
|
+@Service
|
|
|
|
|
+@RequiredArgsConstructor
|
|
|
|
|
+public class XiaoShouYiMaterialService {
|
|
|
|
|
+
|
|
|
|
|
+ private static final String GET_LINK_PATH = "/rest/mc/v2.0/cms/api/buried-point/generateMaterialTrackLink";
|
|
|
|
|
+ private static final String SEND_PATH = "/rest/mc/v2.0/cms/api/buried-point/forwarded";
|
|
|
|
|
+ private static final String QUERY_ALL_PATH = "/rest/mc/v2.0/cms/material-center/material/queryAll";
|
|
|
|
|
+ private static final String UPLOAD_FILE_PATH = "/rest/mc/v2.0/cms/api/action/uploadFile";
|
|
|
|
|
+ private static final String CREATE_MATERIAL_PATH = "/rest/mc/v2.0/cms/api/action/createMaterial";
|
|
|
|
|
+
|
|
|
|
|
+ private final XiaoShouYiOAuthService oAuthService;
|
|
|
|
|
+ private final XiaoShouYiHttpClient httpClient;
|
|
|
|
|
+ private final XiaoShouYiProperties properties;
|
|
|
|
|
+ private final XsyAccountResolver xsyAccountResolver;
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 自动选账号:生成素材追踪链接
|
|
|
|
|
+ */
|
|
|
|
|
+ public GenerateLinkResponse generateMaterialTrackLink(Long companyUserId,
|
|
|
|
|
+ List<Long> materialIdList,
|
|
|
|
|
+ Long sendUserId) {
|
|
|
|
|
+ Long accountId = resolveAccountId(companyUserId);
|
|
|
|
|
+
|
|
|
|
|
+ Map<String, Object> body = new HashMap<String, Object>();
|
|
|
|
|
+ body.put("materialIdList", materialIdList);
|
|
|
|
|
+ body.put("sendUserId", sendUserId);
|
|
|
|
|
+
|
|
|
|
|
+ return executeWithRetry(
|
|
|
|
|
+ accountId,
|
|
|
|
|
+ properties.getDefaultApiBaseUrl() + GET_LINK_PATH,
|
|
|
|
|
+ authHeader -> httpClient.postJson(normalizeUrl(properties.getDefaultApiBaseUrl() + GET_LINK_PATH), authHeader, body),
|
|
|
|
|
+ this::parseGenerateResponse,
|
|
|
|
|
+ GenerateLinkResponse::error
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 自动选账号:发送确认
|
|
|
|
|
+ */
|
|
|
|
|
+ public SendConfirmResponse sendConfirmAuto(Long companyUserId,
|
|
|
|
|
+ Long forwardId,
|
|
|
|
|
+ Integer forwardType) {
|
|
|
|
|
+ Long accountId = resolveAccountId(companyUserId);
|
|
|
|
|
+
|
|
|
|
|
+ String url = String.format("%s%s?forwardId=%d&forwardType=%d",
|
|
|
|
|
+ properties.getDefaultApiBaseUrl(), SEND_PATH, forwardId, forwardType);
|
|
|
|
|
+
|
|
|
|
|
+ return executeWithRetry(
|
|
|
|
|
+ accountId,
|
|
|
|
|
+ url,
|
|
|
|
|
+ authHeader -> httpClient.get(normalizeUrl(url), authHeader),
|
|
|
|
|
+ this::parseSendResponse,
|
|
|
|
|
+ SendConfirmResponse::error
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 自动选账号:查询素材
|
|
|
|
|
+ */
|
|
|
|
|
+ public QueryMaterialResponse queryMaterialsAuto(Long companyUserId,
|
|
|
|
|
+ QueryMaterialRequest request) {
|
|
|
|
|
+ Long accountId = resolveAccountId(companyUserId);
|
|
|
|
|
+ String url = normalizeUrl(oAuthService.getApiBaseUrl(accountId) + QUERY_ALL_PATH);
|
|
|
|
|
+
|
|
|
|
|
+ return executeWithRetry(
|
|
|
|
|
+ accountId,
|
|
|
|
|
+ url,
|
|
|
|
|
+ authHeader -> httpClient.postJson(url, authHeader, request),
|
|
|
|
|
+ this::parseQueryMaterialResponse,
|
|
|
|
|
+ QueryMaterialResponse::error
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 自动选账号:上传素材资源
|
|
|
|
|
+ */
|
|
|
|
|
+ public UploadMaterialFileResponse uploadMaterialFileAuto(Long companyUserId,
|
|
|
|
|
+ File file,
|
|
|
|
|
+ Boolean isVideo) {
|
|
|
|
|
+ if (file == null || !file.exists() || !file.isFile()) {
|
|
|
|
|
+ return UploadMaterialFileResponse.error("上传文件不存在");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Long accountId = resolveAccountId(companyUserId);
|
|
|
|
|
+ String url = normalizeUrl(properties.getDefaultApiBaseUrl() + UPLOAD_FILE_PATH);
|
|
|
|
|
+
|
|
|
|
|
+ log.info("上传素材资源请求URL: {}, accountId={}", url, accountId);
|
|
|
|
|
+ log.info("上传素材资源文件: {}, isVideo={}", file.getAbsolutePath(), isVideo);
|
|
|
|
|
+
|
|
|
|
|
+ return executeWithRetry(
|
|
|
|
|
+ accountId,
|
|
|
|
|
+ url,
|
|
|
|
|
+ authHeader -> httpClient.postMultipart(url, authHeader, file, isVideo),
|
|
|
|
|
+ this::parseUploadMaterialFileResponse,
|
|
|
|
|
+ UploadMaterialFileResponse::error
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 自动选账号:创建素材
|
|
|
|
|
+ */
|
|
|
|
|
+ public CreateMaterialResponse createMaterialAuto(Long companyUserId,
|
|
|
|
|
+ CreateMaterialRequest request) {
|
|
|
|
|
+ String validateMsg = validateCreateMaterialRequest(request);
|
|
|
|
|
+ if (validateMsg != null) {
|
|
|
|
|
+ return CreateMaterialResponse.error(validateMsg);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Long accountId = resolveAccountId(companyUserId);
|
|
|
|
|
+ String url = normalizeUrl(properties.getDefaultApiBaseUrl() + CREATE_MATERIAL_PATH);
|
|
|
|
|
+
|
|
|
|
|
+ log.info("创建素材请求URL: {}, accountId={}", url, accountId);
|
|
|
|
|
+ log.info("创建素材请求参数: {}", JSONUtil.toJsonStr(request));
|
|
|
|
|
+
|
|
|
|
|
+ return executeWithRetry(
|
|
|
|
|
+ accountId,
|
|
|
|
|
+ url,
|
|
|
|
|
+ authHeader -> httpClient.postJson(url, authHeader, request),
|
|
|
|
|
+ this::parseCreateMaterialResponse,
|
|
|
|
|
+ CreateMaterialResponse::error
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 自动选账号:上传素材资源 + 创建素材
|
|
|
|
|
+ */
|
|
|
|
|
+ public CreateMaterialWithUploadResponse createMaterialWithUploadAuto(Long companyUserId,
|
|
|
|
|
+ File file,
|
|
|
|
|
+ Boolean isVideo,
|
|
|
|
|
+ CreateMaterialRequest request) {
|
|
|
|
|
+ if (file == null || !file.exists() || !file.isFile()) {
|
|
|
|
|
+ throw new RuntimeException("上传文件不存在");
|
|
|
|
|
+ }
|
|
|
|
|
+ if (request == null) {
|
|
|
|
|
+ throw new RuntimeException("创建素材参数不能为空");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 1. 上传素材资源
|
|
|
|
|
+ UploadMaterialFileResponse uploadResp = uploadMaterialFileAuto(companyUserId, file, isVideo);
|
|
|
|
|
+ log.info("上传图片数据:{}", uploadResp);
|
|
|
|
|
+
|
|
|
|
|
+ if (!Boolean.TRUE.equals(uploadResp.getSuccess())) {
|
|
|
|
|
+ throw new RuntimeException("上传素材资源失败: " + uploadResp.getMsg());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (uploadResp.getData() == null) {
|
|
|
|
|
+ throw new RuntimeException("上传素材资源成功,但返回数据为空");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 2. 回填 fileId / coverFileId
|
|
|
|
|
+ request.setFileId(uploadResp.getData().getFileId());
|
|
|
|
|
+
|
|
|
|
|
+ // 外链类素材也需要封面,这里先回填 fileId 兜住
|
|
|
|
|
+ request.setCoverFileId(uploadResp.getData().getFileId());
|
|
|
|
|
+
|
|
|
|
|
+ if (uploadResp.getData().getCoverFileDto() != null) {
|
|
|
|
|
+ request.setCoverFileId(uploadResp.getData().getCoverFileDto().getFileId());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 3. 创建素材
|
|
|
|
|
+ CreateMaterialResponse createResp = createMaterialAuto(companyUserId, request);
|
|
|
|
|
+ log.info("素材返回:{}", createResp);
|
|
|
|
|
+
|
|
|
|
|
+ if (!Boolean.TRUE.equals(createResp.getSuccess())) {
|
|
|
|
|
+ throw new RuntimeException("创建素材失败: " + createResp.getMsg());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (createResp.getData() == null
|
|
|
|
|
+ || createResp.getData().getEntry() == null
|
|
|
|
|
+ || createResp.getData().getEntry().getId() == null) {
|
|
|
|
|
+ throw new RuntimeException("创建素材成功,但返回素材ID为空");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 4. 组装返回
|
|
|
|
|
+ CreateMaterialWithUploadResponse response = new CreateMaterialWithUploadResponse();
|
|
|
|
|
+ response.setMaterialId(createResp.getData().getEntry().getId());
|
|
|
|
|
+ response.setFileId(uploadResp.getData().getFileId());
|
|
|
|
|
+ response.setFileUrl(uploadResp.getData().getUrl());
|
|
|
|
|
+
|
|
|
|
|
+ if (uploadResp.getData().getCoverFileDto() != null) {
|
|
|
|
|
+ response.setCoverFileId(uploadResp.getData().getCoverFileDto().getFileId());
|
|
|
|
|
+ response.setCoverUrl(uploadResp.getData().getCoverFileDto().getUrl());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return response;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 自动选账号:通过 URL 下载文件后再上传并创建素材
|
|
|
|
|
+ */
|
|
|
|
|
+ public CreateMaterialWithUploadResponse createMaterialWithUploadByUrl(Long companyUserId,
|
|
|
|
|
+ String fileUrl,
|
|
|
|
|
+ Boolean isVideo,
|
|
|
|
|
+ CreateMaterialRequest request) {
|
|
|
|
|
+ if (fileUrl == null || fileUrl.trim().isEmpty()) {
|
|
|
|
|
+ throw new RuntimeException("文件URL不能为空");
|
|
|
|
|
+ }
|
|
|
|
|
+ if (request == null) {
|
|
|
|
|
+ throw new RuntimeException("创建素材参数不能为空");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ HttpURLConnection conn = null;
|
|
|
|
|
+ InputStream inputStream = null;
|
|
|
|
|
+ FileOutputStream outputStream = null;
|
|
|
|
|
+ File tempFile = null;
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ URL url = new URL(fileUrl);
|
|
|
|
|
+ conn = (HttpURLConnection) url.openConnection();
|
|
|
|
|
+ conn.setRequestMethod("GET");
|
|
|
|
|
+ conn.setConnectTimeout(10000);
|
|
|
|
|
+ conn.setReadTimeout(20000);
|
|
|
|
|
+ conn.setInstanceFollowRedirects(true);
|
|
|
|
|
+
|
|
|
|
|
+ // 防止部分 CDN 或源站拦截
|
|
|
|
|
+ conn.setRequestProperty("User-Agent", "Mozilla/5.0");
|
|
|
|
|
+ conn.setRequestProperty("Accept", "*/*");
|
|
|
|
|
+ conn.setRequestProperty("Connection", "Keep-Alive");
|
|
|
|
|
+ conn.setRequestProperty("Referer", fileUrl);
|
|
|
|
|
+
|
|
|
|
|
+ int code = conn.getResponseCode();
|
|
|
|
|
+ if (code != HttpURLConnection.HTTP_OK) {
|
|
|
|
|
+ String errorMsg = readErrorMsg(conn);
|
|
|
|
|
+ throw new RuntimeException("下载文件失败,HTTP状态码:" + code + ",错误信息:" + errorMsg);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ inputStream = conn.getInputStream();
|
|
|
|
|
+
|
|
|
|
|
+ String suffix = getSuffix(fileUrl, conn.getContentType());
|
|
|
|
|
+ tempFile = File.createTempFile("material_upload_", suffix);
|
|
|
|
|
+ outputStream = new FileOutputStream(tempFile);
|
|
|
|
|
+
|
|
|
|
|
+ byte[] buffer = new byte[8192];
|
|
|
|
|
+ int len;
|
|
|
|
|
+ while ((len = inputStream.read(buffer)) != -1) {
|
|
|
|
|
+ outputStream.write(buffer, 0, len);
|
|
|
|
|
+ }
|
|
|
|
|
+ outputStream.flush();
|
|
|
|
|
+
|
|
|
|
|
+ return createMaterialWithUploadAuto(companyUserId, tempFile, isVideo, request);
|
|
|
|
|
+
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ throw new RuntimeException("通过URL上传并创建素材失败: " + e.getMessage(), e);
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (inputStream != null) {
|
|
|
|
|
+ inputStream.close();
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (Exception ignored) {
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (outputStream != null) {
|
|
|
|
|
+ outputStream.close();
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (Exception ignored) {
|
|
|
|
|
+ }
|
|
|
|
|
+ if (conn != null) {
|
|
|
|
|
+ conn.disconnect();
|
|
|
|
|
+ }
|
|
|
|
|
+ if (tempFile != null && tempFile.exists()) {
|
|
|
|
|
+ if (!tempFile.delete()) {
|
|
|
|
|
+ tempFile.deleteOnExit();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 通用执行模板:
|
|
|
|
|
+ * 1. 先取 token 发请求
|
|
|
|
|
+ * 2. 遇到 401 时自动刷新 token 后重试一次
|
|
|
|
|
+ * 3. 统一组装错误返回
|
|
|
|
|
+ */
|
|
|
|
|
+ private <T> T executeWithRetry(Long accountId,
|
|
|
|
|
+ String url,
|
|
|
|
|
+ RequestExecutor executor,
|
|
|
|
|
+ ResponseParser<T> successParser,
|
|
|
|
|
+ ErrorResponseFactory<T> errorFactory) {
|
|
|
|
|
+
|
|
|
|
|
+ String authHeader = oAuthService.getAuthorizationHeader(accountId);
|
|
|
|
|
+ if (authHeader == null) {
|
|
|
|
|
+ return errorFactory.build("未获取到有效token,请先授权");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ XiaoShouYiHttpClient.HttpResult result = executor.execute(authHeader);
|
|
|
|
|
+
|
|
|
|
|
+ if (result.getStatus() == 200) {
|
|
|
|
|
+ return successParser.parse(result.getBody());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (result.getStatus() == 401) {
|
|
|
|
|
+ log.warn("请求遇到401,尝试刷新token后重试,accountId={}, url={}", accountId, url);
|
|
|
|
|
+
|
|
|
|
|
+ TokenResponse refreshResp = oAuthService.refreshAccessToken(accountId);
|
|
|
|
|
+ if (!refreshResp.isSuccess()) {
|
|
|
|
|
+ log.error("刷新token失败,accountId={}, url={}, error={}",
|
|
|
|
|
+ accountId, url, refreshResp.getError());
|
|
|
|
|
+ oAuthService.clearTokenCache(accountId);
|
|
|
|
|
+ return errorFactory.build("token失效,请重新授权");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ authHeader = oAuthService.getAuthorizationHeader(accountId);
|
|
|
|
|
+ if (authHeader == null) {
|
|
|
|
|
+ oAuthService.clearTokenCache(accountId);
|
|
|
|
|
+ return errorFactory.build("获取刷新后的token失败,请重新授权");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ XiaoShouYiHttpClient.HttpResult retry = executor.execute(authHeader);
|
|
|
|
|
+
|
|
|
|
|
+ if (retry.getStatus() == 200) {
|
|
|
|
|
+ return successParser.parse(retry.getBody());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (retry.getStatus() == 401) {
|
|
|
|
|
+ oAuthService.clearTokenCache(accountId);
|
|
|
|
|
+ return errorFactory.build("刷新后仍未授权,请重新授权");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return errorFactory.build(
|
|
|
|
|
+ "重试失败,HTTP状态码:" + retry.getStatus() + ",响应:" + retry.getBody()
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return errorFactory.build(
|
|
|
|
|
+ "HTTP状态码:" + result.getStatus() + ",响应:" + result.getBody()
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private GenerateLinkResponse parseGenerateResponse(String jsonStr) {
|
|
|
|
|
+ JSONObject json = JSONUtil.parseObj(jsonStr);
|
|
|
|
|
+ GenerateLinkResponse response = new GenerateLinkResponse();
|
|
|
|
|
+ response.setSuccess(json.getBool("success", false));
|
|
|
|
|
+ response.setCode(json.getStr("code"));
|
|
|
|
|
+ response.setMsg(json.getStr("msg"));
|
|
|
|
|
+
|
|
|
|
|
+ GenerateLinkResponse.GenerateLinkData data = new GenerateLinkResponse.GenerateLinkData();
|
|
|
|
|
+ if (json.containsKey("data")) {
|
|
|
|
|
+ JSONObject jsonData = json.getJSONObject("data");
|
|
|
|
|
+ data.setTrackedMaterials(parseMaterialList(jsonData.getJSONArray("trackedMaterials"), true));
|
|
|
|
|
+ data.setUntrackedMaterials(parseMaterialList(jsonData.getJSONArray("untrackedMaterials"), false));
|
|
|
|
|
+ } else {
|
|
|
|
|
+ data.setTrackedMaterials(Collections.<MaterialTrackItem>emptyList());
|
|
|
|
|
+ data.setUntrackedMaterials(Collections.<MaterialTrackItem>emptyList());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ response.setData(data);
|
|
|
|
|
+ return response;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private List<MaterialTrackItem> parseMaterialList(JSONArray array, boolean hasTrackLink) {
|
|
|
|
|
+ if (array == null || array.isEmpty()) {
|
|
|
|
|
+ return Collections.emptyList();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ List<MaterialTrackItem> list = new ArrayList<MaterialTrackItem>();
|
|
|
|
|
+ for (Object obj : array) {
|
|
|
|
|
+ JSONObject item = (JSONObject) obj;
|
|
|
|
|
+ MaterialTrackItem trackItem = new MaterialTrackItem();
|
|
|
|
|
+ trackItem.setMaterialId(item.getLong("materialId"));
|
|
|
|
|
+ trackItem.setMaterialType(item.getInt("materialType"));
|
|
|
|
|
+
|
|
|
|
|
+ if (hasTrackLink && item.containsKey("trackLink")) {
|
|
|
|
|
+ JSONObject link = item.getJSONObject("trackLink");
|
|
|
|
|
+ TrackLinkInfo info = new TrackLinkInfo();
|
|
|
|
|
+ info.setForwardId(link.getLong("forwardId"));
|
|
|
|
|
+ info.setRedirectUrl(link.getStr("redirectUrl"));
|
|
|
|
|
+ trackItem.setTrackLink(info);
|
|
|
|
|
+ }
|
|
|
|
|
+ list.add(trackItem);
|
|
|
|
|
+ }
|
|
|
|
|
+ return list;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private SendConfirmResponse parseSendResponse(String jsonStr) {
|
|
|
|
|
+ JSONObject json = JSONUtil.parseObj(jsonStr);
|
|
|
|
|
+ SendConfirmResponse response = new SendConfirmResponse();
|
|
|
|
|
+ response.setSuccess(json.getBool("success", false));
|
|
|
|
|
+ response.setCode(json.getStr("code"));
|
|
|
|
|
+ response.setMsg(json.getStr("msg"));
|
|
|
|
|
+ return response;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private QueryMaterialResponse parseQueryMaterialResponse(String jsonStr) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ return JSONUtil.toBean(jsonStr, QueryMaterialResponse.class);
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("解析素材列表响应失败, json={}", jsonStr, e);
|
|
|
|
|
+ return QueryMaterialResponse.error("解析素材列表响应失败:" + e.getMessage());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private UploadMaterialFileResponse parseUploadMaterialFileResponse(String jsonStr) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ return JSONUtil.toBean(jsonStr, UploadMaterialFileResponse.class);
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("解析上传素材资源响应失败, json={}", jsonStr, e);
|
|
|
|
|
+ return UploadMaterialFileResponse.error("解析上传素材资源响应失败:" + e.getMessage());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private CreateMaterialResponse parseCreateMaterialResponse(String jsonStr) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ return JSONUtil.toBean(jsonStr, CreateMaterialResponse.class);
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("解析创建素材响应失败, json={}", jsonStr, e);
|
|
|
|
|
+ return CreateMaterialResponse.error("解析创建素材响应失败:" + e.getMessage());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /* ========== 参数校验与工具方法 ============= */
|
|
|
|
|
+
|
|
|
|
|
+ private String validateCreateMaterialRequest(CreateMaterialRequest request) {
|
|
|
|
|
+ if (request == null) {
|
|
|
|
|
+ return "请求参数不能为空";
|
|
|
|
|
+ }
|
|
|
|
|
+ if (isBlank(request.getCorpName())) {
|
|
|
|
|
+ return "corpName不能为空";
|
|
|
|
|
+ }
|
|
|
|
|
+ if (request.getMaterialType() == null) {
|
|
|
|
|
+ return "materialType不能为空";
|
|
|
|
|
+ }
|
|
|
|
|
+ if (isBlank(request.getCategoryName())) {
|
|
|
|
|
+ return "categoryName不能为空";
|
|
|
|
|
+ }
|
|
|
|
|
+ if (isBlank(request.getTitle())) {
|
|
|
|
|
+ return "title不能为空";
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Integer materialType = request.getMaterialType();
|
|
|
|
|
+
|
|
|
|
|
+ // 图片 / 视频 / 文件:必须有 fileId
|
|
|
|
|
+ if ((materialType == 2 || materialType == 5 || materialType == 6) && request.getFileId() == null) {
|
|
|
|
|
+ return "当前素材类型必须传fileId";
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 视频:必须有 coverFileId
|
|
|
|
|
+ if (materialType == 5 && request.getCoverFileId() == null) {
|
|
|
|
|
+ return "视频素材必须传coverFileId";
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 小程序:必须 coverFileId + appId + path
|
|
|
|
|
+ if (materialType == 7) {
|
|
|
|
|
+ if (request.getCoverFileId() == null) {
|
|
|
|
|
+ return "小程序素材必须传coverFileId";
|
|
|
|
|
+ }
|
|
|
|
|
+ if (isBlank(request.getAppId())) {
|
|
|
|
|
+ return "小程序素材必须传appId";
|
|
|
|
|
+ }
|
|
|
|
|
+ if (isBlank(request.getPath())) {
|
|
|
|
|
+ return "小程序素材必须传path";
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 自建网页:必须 coverFileId + content
|
|
|
|
|
+ if (materialType == 31) {
|
|
|
|
|
+ if (request.getCoverFileId() == null) {
|
|
|
|
|
+ return "自建网页素材必须传coverFileId";
|
|
|
|
|
+ }
|
|
|
|
|
+ if (isBlank(request.getContent())) {
|
|
|
|
|
+ return "自建网页素材必须传content";
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 外链 / 公众号图文:必须 coverFileId + url
|
|
|
|
|
+ if (materialType == 32 || materialType == 33) {
|
|
|
|
|
+ if (request.getCoverFileId() == null) {
|
|
|
|
|
+ return "外链类素材必须传coverFileId";
|
|
|
|
|
+ }
|
|
|
|
|
+ if (isBlank(request.getUrl())) {
|
|
|
|
|
+ return "外链类素材必须传url";
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private Long resolveAccountId(Long companyUserId) {
|
|
|
|
|
+ return xsyAccountResolver.resolveAccountId(companyUserId);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private String normalizeUrl(String url) {
|
|
|
|
|
+ if (url == null || url.trim().isEmpty()) {
|
|
|
|
|
+ return url;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!url.startsWith("http")) {
|
|
|
|
|
+ return "https://" + url;
|
|
|
|
|
+ }
|
|
|
|
|
+ return url;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private String getSuffix(String fileUrl, String contentType) {
|
|
|
|
|
+ if (fileUrl != null && !fileUrl.trim().isEmpty()) {
|
|
|
|
|
+ String pureUrl = fileUrl;
|
|
|
|
|
+ int queryIndex = pureUrl.indexOf("?");
|
|
|
|
|
+ if (queryIndex > -1) {
|
|
|
|
|
+ pureUrl = pureUrl.substring(0, queryIndex);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ int dotIndex = pureUrl.lastIndexOf('.');
|
|
|
|
|
+ int slashIndex = pureUrl.lastIndexOf('/');
|
|
|
|
|
+ if (dotIndex > slashIndex) {
|
|
|
|
|
+ String suffix = pureUrl.substring(dotIndex);
|
|
|
|
|
+ if (suffix.length() <= 10) {
|
|
|
|
|
+ return suffix;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (contentType != null) {
|
|
|
|
|
+ String lowerContentType = contentType.toLowerCase();
|
|
|
|
|
+ if (lowerContentType.contains("jpeg")) return ".jpg";
|
|
|
|
|
+ if (lowerContentType.contains("jpg")) return ".jpg";
|
|
|
|
|
+ if (lowerContentType.contains("png")) return ".png";
|
|
|
|
|
+ if (lowerContentType.contains("gif")) return ".gif";
|
|
|
|
|
+ if (lowerContentType.contains("bmp")) return ".bmp";
|
|
|
|
|
+ if (lowerContentType.contains("webp")) return ".webp";
|
|
|
|
|
+ if (lowerContentType.contains("mp4")) return ".mp4";
|
|
|
|
|
+ if (lowerContentType.contains("quicktime")) return ".mov";
|
|
|
|
|
+ if (lowerContentType.contains("mpeg")) return ".mpg";
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return ".tmp";
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private String readErrorMsg(HttpURLConnection conn) {
|
|
|
|
|
+ InputStream errorStream = null;
|
|
|
|
|
+ ByteArrayOutputStream baos = null;
|
|
|
|
|
+ try {
|
|
|
|
|
+ errorStream = conn.getErrorStream();
|
|
|
|
|
+ if (errorStream == null) {
|
|
|
|
|
+ return "";
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ baos = new ByteArrayOutputStream();
|
|
|
|
|
+ byte[] buffer = new byte[1024];
|
|
|
|
|
+ int len;
|
|
|
|
|
+ while ((len = errorStream.read(buffer)) != -1) {
|
|
|
|
|
+ baos.write(buffer, 0, len);
|
|
|
|
|
+ }
|
|
|
|
|
+ return baos.toString("UTF-8");
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ return "";
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (errorStream != null) {
|
|
|
|
|
+ errorStream.close();
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (Exception ignored) {
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (baos != null) {
|
|
|
|
|
+ baos.close();
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (Exception ignored) {
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private boolean isBlank(String str) {
|
|
|
|
|
+ return str == null || str.trim().isEmpty();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ @FunctionalInterface
|
|
|
|
|
+ private interface RequestExecutor {
|
|
|
|
|
+ XiaoShouYiHttpClient.HttpResult execute(String authHeader);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @FunctionalInterface
|
|
|
|
|
+ private interface ResponseParser<T> {
|
|
|
|
|
+ T parse(String body);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @FunctionalInterface
|
|
|
|
|
+ private interface ErrorResponseFactory<T> {
|
|
|
|
|
+ T build(String msg);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|