|
|
@@ -0,0 +1,405 @@
|
|
|
+package cn.com.ctop.job.bytedance.changdu.service.impl;
|
|
|
+
|
|
|
+import cn.com.ctop.job.bytedance.changdu.config.ChangduSeriesFeishuProperties;
|
|
|
+import cn.com.ctop.job.bytedance.changdu.entity.ChangduSeriesCardCandidate;
|
|
|
+import cn.com.ctop.job.bytedance.changdu.entity.ChangduSeriesSendResult;
|
|
|
+import cn.com.ctop.job.bytedance.changdu.mapper.ChangduSeriesFeishuMapper;
|
|
|
+import cn.com.ctop.job.bytedance.changdu.service.IChangduSeriesFeishuService;
|
|
|
+import cn.com.ctop.job.bytedance.changdu.support.ChangduSeriesCardConstants;
|
|
|
+import cn.com.ctop.job.bytedance.changdu.support.ChangduSeriesCardBuilder;
|
|
|
+import cn.com.ctop.job.bytedance.changdu.support.ChangduSeriesFeishuClient;
|
|
|
+import com.alibaba.fastjson.JSON;
|
|
|
+import com.lark.oapi.event.cardcallback.model.CallBackAction;
|
|
|
+import com.lark.oapi.event.cardcallback.model.CallBackCard;
|
|
|
+import com.lark.oapi.event.cardcallback.model.CallBackOperator;
|
|
|
+import com.lark.oapi.event.cardcallback.model.CallBackToast;
|
|
|
+import com.lark.oapi.event.cardcallback.model.P2CardActionTrigger;
|
|
|
+import com.lark.oapi.event.cardcallback.model.P2CardActionTriggerResponse;
|
|
|
+import org.apache.commons.lang3.StringUtils;
|
|
|
+import org.slf4j.Logger;
|
|
|
+import org.slf4j.LoggerFactory;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+
|
|
|
+import java.util.List;
|
|
|
+import java.util.Map;
|
|
|
+import java.util.concurrent.ExecutorService;
|
|
|
+import java.util.concurrent.Executors;
|
|
|
+
|
|
|
+@Service
|
|
|
+public class ChangduSeriesFeishuServiceImpl implements IChangduSeriesFeishuService {
|
|
|
+
|
|
|
+ private static final Logger log = LoggerFactory.getLogger(ChangduSeriesFeishuServiceImpl.class);
|
|
|
+ private static final ExecutorService CARD_UPDATE_EXECUTOR = Executors.newFixedThreadPool(2);
|
|
|
+
|
|
|
+ private final ChangduSeriesFeishuMapper changduSeriesFeishuMapper;
|
|
|
+ private final ChangduSeriesFeishuClient changduSeriesFeishuClient;
|
|
|
+ private final ChangduSeriesCardBuilder changduSeriesCardBuilder;
|
|
|
+ private final ChangduSeriesFeishuProperties properties;
|
|
|
+
|
|
|
+ public ChangduSeriesFeishuServiceImpl(ChangduSeriesFeishuMapper changduSeriesFeishuMapper,
|
|
|
+ ChangduSeriesFeishuClient changduSeriesFeishuClient,
|
|
|
+ ChangduSeriesCardBuilder changduSeriesCardBuilder,
|
|
|
+ ChangduSeriesFeishuProperties properties) {
|
|
|
+ this.changduSeriesFeishuMapper = changduSeriesFeishuMapper;
|
|
|
+ this.changduSeriesFeishuClient = changduSeriesFeishuClient;
|
|
|
+ this.changduSeriesCardBuilder = changduSeriesCardBuilder;
|
|
|
+ this.properties = properties;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void pushPendingCards() {
|
|
|
+ if (!properties.hasAppCredential()) {
|
|
|
+ log.warn("changdu series feishu app credential not configured");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (!properties.hasReceiveTarget()) {
|
|
|
+ log.warn("changdu series feishu target not configured");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ List<ChangduSeriesCardCandidate> candidates = changduSeriesFeishuMapper.listPendingSeries(
|
|
|
+ properties.getBatchSize(),
|
|
|
+ properties.getRetryLimit(),
|
|
|
+ properties.getProcessingTimeoutMinutes()
|
|
|
+ );
|
|
|
+ if (candidates == null || candidates.isEmpty()) {
|
|
|
+ log.info("changdu series feishu no pending data");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ for (ChangduSeriesCardCandidate candidate : candidates) {
|
|
|
+ sendSingleCard(candidate);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public P2CardActionTriggerResponse handleCardAction(P2CardActionTrigger event) {
|
|
|
+ try {
|
|
|
+ CallBackAction action = event == null || event.getEvent() == null ? null : event.getEvent().getAction();
|
|
|
+ if (action == null) {
|
|
|
+ return buildToast("warning", "未识别到卡片动作");
|
|
|
+ }
|
|
|
+
|
|
|
+ String actionName = readActionValue(action.getValue(), "action");
|
|
|
+ log.info("changdu series card action received, actionName={}, actionValue={}, formValue={}",
|
|
|
+ actionName,
|
|
|
+ JSON.toJSONString(action.getValue()),
|
|
|
+ JSON.toJSONString(action.getFormValue()));
|
|
|
+ if (StringUtils.equals("select_changdu_series_type", actionName)) {
|
|
|
+ return handleSelectSeriesType(action);
|
|
|
+ }
|
|
|
+ if (StringUtils.equals("reset_changdu_series_type", actionName)) {
|
|
|
+ return handleResetSeriesType(action);
|
|
|
+ }
|
|
|
+ if (StringUtils.equals("bind_changdu_series_type", actionName)) {
|
|
|
+ return handleBindSeriesType(event, action);
|
|
|
+ }
|
|
|
+ return buildToast("info", "已忽略非目标卡片操作");
|
|
|
+ } catch (Exception exception) {
|
|
|
+ log.error("changdu series handle card action failed, event={}", JSON.toJSONString(event), exception);
|
|
|
+ return buildToast("error", "绑定失败,请联系开发排查日志");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private P2CardActionTriggerResponse handleSelectSeriesType(CallBackAction action) {
|
|
|
+ String bookId = readActionValue(action.getValue(), "book_id");
|
|
|
+ String seriesType = readActionValue(action.getValue(), "series_type");
|
|
|
+ if (StringUtils.isBlank(bookId)) {
|
|
|
+ return buildToast("warning", "短剧ID为空,无法选择");
|
|
|
+ }
|
|
|
+ if (!getTypeOptions().contains(seriesType)) {
|
|
|
+ return buildToast("warning", "短剧类型不在允许范围内");
|
|
|
+ }
|
|
|
+ return rebuildCard(bookId, readActionValue(action.getValue(), "image_key"), seriesType);
|
|
|
+ }
|
|
|
+
|
|
|
+ private P2CardActionTriggerResponse handleResetSeriesType(CallBackAction action) {
|
|
|
+ String bookId = readActionValue(action.getValue(), "book_id");
|
|
|
+ if (StringUtils.isBlank(bookId)) {
|
|
|
+ return buildToast("warning", "短剧ID为空,无法重置");
|
|
|
+ }
|
|
|
+ return rebuildCard(bookId, readActionValue(action.getValue(), "image_key"), null);
|
|
|
+ }
|
|
|
+
|
|
|
+ private P2CardActionTriggerResponse handleBindSeriesType(P2CardActionTrigger event, CallBackAction action) {
|
|
|
+ String bookId = readActionValue(action.getValue(), "book_id");
|
|
|
+ String seriesType = resolveSeriesType(action);
|
|
|
+ String operator = resolveOperator(event == null ? null : event.getEvent().getOperator());
|
|
|
+
|
|
|
+ if (StringUtils.isBlank(bookId)) {
|
|
|
+ return buildToast("warning", "短剧ID为空,无法提交");
|
|
|
+ }
|
|
|
+ if (StringUtils.isBlank(seriesType)) {
|
|
|
+ return buildToast("warning", "请选择短剧类型后再绑定");
|
|
|
+ }
|
|
|
+ if (StringUtils.equals("MULTIPLE", seriesType)) {
|
|
|
+ return buildToast("warning", "短剧类型只能选择一个,请重置后重新选择");
|
|
|
+ }
|
|
|
+ if (!getTypeOptions().contains(seriesType)) {
|
|
|
+ return buildToast("warning", "短剧类型不在允许范围内");
|
|
|
+ }
|
|
|
+ if (changduSeriesFeishuMapper.existsSeries(bookId) <= 0) {
|
|
|
+ return buildToast("warning", "未找到对应短剧,绑定失败");
|
|
|
+ }
|
|
|
+
|
|
|
+ changduSeriesFeishuMapper.upsertSeriesTypeBind(bookId, seriesType, operator);
|
|
|
+ String seriesTypeLabel = ChangduSeriesCardConstants.getTypeLabel(seriesType);
|
|
|
+ log.info("changdu series type bind success, bookId={}, seriesType={}, seriesTypeLabel={}, operator={}", bookId, seriesType, seriesTypeLabel, operator);
|
|
|
+
|
|
|
+ String callbackOpenMessageId = event == null || event.getEvent() == null || event.getEvent().getContext() == null
|
|
|
+ ? null
|
|
|
+ : event.getEvent().getContext().getOpenMessageId();
|
|
|
+ String callbackToken = event == null || event.getEvent() == null ? null : event.getEvent().getToken();
|
|
|
+ asyncUpdateBoundCard(bookId, readActionValue(action.getValue(), "image_key"), seriesType, callbackToken, callbackOpenMessageId);
|
|
|
+ return new P2CardActionTriggerResponse();
|
|
|
+ }
|
|
|
+
|
|
|
+ private P2CardActionTriggerResponse rebuildCard(String bookId, String imageKey, String selectedType) {
|
|
|
+ return rebuildCard(bookId, imageKey, selectedType, false);
|
|
|
+ }
|
|
|
+
|
|
|
+ private P2CardActionTriggerResponse rebuildBoundCard(String bookId, String imageKey, String selectedType) {
|
|
|
+ return rebuildCard(bookId, imageKey, selectedType, true);
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean updateBoundMessageCard(String bookId, String imageKey, String selectedType, String callbackOpenMessageId) {
|
|
|
+ ChangduSeriesCardCandidate candidate = changduSeriesFeishuMapper.getSeriesByBookId(bookId);
|
|
|
+ if (candidate == null) {
|
|
|
+ log.warn("changdu series update bound message skipped, candidate not found, bookId={}", bookId);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ String effectiveImageKey = imageKey;
|
|
|
+ if (StringUtils.isBlank(effectiveImageKey)) {
|
|
|
+ effectiveImageKey = changduSeriesFeishuClient.uploadImageAndGetKey(candidate.getOssCover());
|
|
|
+ }
|
|
|
+ String messageId = changduSeriesFeishuMapper.getMessageIdByBookId(bookId);
|
|
|
+ if (StringUtils.isBlank(messageId)) {
|
|
|
+ messageId = callbackOpenMessageId;
|
|
|
+ log.info("changdu series update bound message use callback openMessageId, bookId={}, openMessageId={}", bookId, callbackOpenMessageId);
|
|
|
+ }
|
|
|
+ if (StringUtils.isBlank(messageId)) {
|
|
|
+ log.warn("changdu series update bound message skipped, messageId not found, bookId={}, callbackOpenMessageId={}", bookId, callbackOpenMessageId);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ String cardJson = changduSeriesCardBuilder.buildCard(candidate, effectiveImageKey, selectedType, true);
|
|
|
+ return changduSeriesFeishuClient.updateInteractiveCard(messageId, cardJson);
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ private void asyncUpdateBoundCard(String bookId, String imageKey, String selectedType, String callbackToken, String callbackOpenMessageId) {
|
|
|
+ CARD_UPDATE_EXECUTOR.submit(new Runnable() {
|
|
|
+ @Override
|
|
|
+ public void run() {
|
|
|
+ sleepSilently(1000L);
|
|
|
+ ChangduSeriesCardCandidate candidate = changduSeriesFeishuMapper.getSeriesByBookId(bookId);
|
|
|
+ if (candidate == null) {
|
|
|
+ log.warn("changdu series async update skipped, candidate not found, bookId={}", bookId);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ String effectiveImageKey = imageKey;
|
|
|
+ if (StringUtils.isBlank(effectiveImageKey)) {
|
|
|
+ effectiveImageKey = changduSeriesFeishuClient.uploadImageAndGetKey(candidate.getOssCover());
|
|
|
+ }
|
|
|
+ String cardJson = changduSeriesCardBuilder.buildCard(candidate, effectiveImageKey, selectedType, true);
|
|
|
+ boolean updated = false;
|
|
|
+ if (StringUtils.isNotBlank(callbackToken)) {
|
|
|
+ updated = changduSeriesFeishuClient.delayUpdateInteractiveCard(callbackToken, cardJson);
|
|
|
+ }
|
|
|
+ if (!updated) {
|
|
|
+ updated = updateBoundMessageCard(bookId, effectiveImageKey, selectedType, callbackOpenMessageId);
|
|
|
+ }
|
|
|
+ log.info("changdu series async update finished, bookId={}, updated={}, callbackTokenPresent={}", bookId, updated, StringUtils.isNotBlank(callbackToken));
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ private void sleepSilently(long millis) {
|
|
|
+ try {
|
|
|
+ Thread.sleep(millis);
|
|
|
+ } catch (InterruptedException interruptedException) {
|
|
|
+ Thread.currentThread().interrupt();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private P2CardActionTriggerResponse rebuildCard(String bookId, String imageKey, String selectedType, boolean bound) {
|
|
|
+ ChangduSeriesCardCandidate candidate = changduSeriesFeishuMapper.getSeriesByBookId(bookId);
|
|
|
+ if (candidate == null) {
|
|
|
+ return buildToast("warning", "未找到对应短剧,无法更新卡片");
|
|
|
+ }
|
|
|
+ String effectiveImageKey = imageKey;
|
|
|
+ if (StringUtils.isBlank(effectiveImageKey)) {
|
|
|
+ effectiveImageKey = changduSeriesFeishuClient.uploadImageAndGetKey(candidate.getOssCover());
|
|
|
+ }
|
|
|
+ String cardJson = changduSeriesCardBuilder.buildCard(candidate, effectiveImageKey, selectedType, bound);
|
|
|
+ return buildCardUpdate(cardJson);
|
|
|
+ }
|
|
|
+
|
|
|
+ private void sendSingleCard(ChangduSeriesCardCandidate candidate) {
|
|
|
+ if (candidate == null || StringUtils.isBlank(candidate.getBookId())) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (!claimSendRecord(candidate.getBookId())) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ String imageKey = changduSeriesFeishuClient.uploadImageAndGetKey(candidate.getOssCover());
|
|
|
+ String cardJson = changduSeriesCardBuilder.buildCard(candidate, imageKey);
|
|
|
+ ChangduSeriesSendResult result = changduSeriesFeishuClient.sendInteractiveCard(
|
|
|
+ properties.getReceiveType(),
|
|
|
+ properties.getReceiveId(),
|
|
|
+ cardJson
|
|
|
+ );
|
|
|
+ if (result.isSuccess()) {
|
|
|
+ changduSeriesFeishuMapper.markSendSuccess(candidate.getBookId(), result.getMessageId());
|
|
|
+ log.info("changdu series feishu card sent, bookId={}, messageId={}", candidate.getBookId(), result.getMessageId());
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ changduSeriesFeishuMapper.markSendFailure(candidate.getBookId(), trimError(result.getErrorMsg()));
|
|
|
+ log.warn("changdu series feishu card send failed, bookId={}, error={}", candidate.getBookId(), result.getErrorMsg());
|
|
|
+ } catch (Exception exception) {
|
|
|
+ changduSeriesFeishuMapper.markSendFailure(candidate.getBookId(), trimError(exception.getMessage()));
|
|
|
+ log.error("changdu series feishu send exception, bookId={}", candidate.getBookId(), exception);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean claimSendRecord(String bookId) {
|
|
|
+ int inserted = changduSeriesFeishuMapper.insertSendRecord(bookId, properties.getReceiveType(), properties.getReceiveId());
|
|
|
+ if (inserted > 0) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ int updated = changduSeriesFeishuMapper.claimFailedOrExpiredSendRecord(
|
|
|
+ bookId,
|
|
|
+ properties.getReceiveType(),
|
|
|
+ properties.getReceiveId(),
|
|
|
+ properties.getRetryLimit(),
|
|
|
+ properties.getProcessingTimeoutMinutes()
|
|
|
+ );
|
|
|
+ return updated > 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ private String resolveSeriesType(CallBackAction action) {
|
|
|
+ String actionSeriesType = readActionValue(action.getValue(), "series_type");
|
|
|
+ if (StringUtils.isNotBlank(actionSeriesType)) {
|
|
|
+ return actionSeriesType;
|
|
|
+ }
|
|
|
+ String selectSeriesType = readFormValue(action.getFormValue(), "series_type");
|
|
|
+ if (StringUtils.isNotBlank(selectSeriesType)) {
|
|
|
+ return selectSeriesType;
|
|
|
+ }
|
|
|
+ return readCheckedSeriesType(action.getFormValue());
|
|
|
+ }
|
|
|
+
|
|
|
+ private String readCheckedSeriesType(Map<String, Object> formValues) {
|
|
|
+ if (formValues == null || formValues.isEmpty()) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ String selectedType = null;
|
|
|
+ for (String typeValue : ChangduSeriesCardConstants.TYPE_OPTIONS) {
|
|
|
+ Object rawValue = formValues.get("series_type_" + typeValue);
|
|
|
+ if (!isChecked(rawValue)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ if (StringUtils.isNotBlank(selectedType)) {
|
|
|
+ return "MULTIPLE";
|
|
|
+ }
|
|
|
+ selectedType = typeValue;
|
|
|
+ }
|
|
|
+ return selectedType;
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean isChecked(Object rawValue) {
|
|
|
+ if (rawValue == null) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (rawValue instanceof Boolean) {
|
|
|
+ return ((Boolean) rawValue).booleanValue();
|
|
|
+ }
|
|
|
+ if (rawValue instanceof Number) {
|
|
|
+ return ((Number) rawValue).intValue() != 0;
|
|
|
+ }
|
|
|
+ if (rawValue instanceof Map) {
|
|
|
+ Map<?, ?> valueMap = (Map<?, ?>) rawValue;
|
|
|
+ Object checked = valueMap.get("checked");
|
|
|
+ if (checked == null) {
|
|
|
+ checked = valueMap.get("value");
|
|
|
+ }
|
|
|
+ if (checked == null) {
|
|
|
+ checked = valueMap.get("selected");
|
|
|
+ }
|
|
|
+ return isChecked(checked);
|
|
|
+ }
|
|
|
+ String value = String.valueOf(rawValue);
|
|
|
+ return StringUtils.equalsIgnoreCase(value, "true")
|
|
|
+ || StringUtils.equals(value, "1")
|
|
|
+ || StringUtils.equalsIgnoreCase(value, "checked")
|
|
|
+ || StringUtils.equalsIgnoreCase(value, "selected");
|
|
|
+ }
|
|
|
+
|
|
|
+ private String resolveOperator(CallBackOperator operator) {
|
|
|
+ if (operator == null) {
|
|
|
+ return "unknown";
|
|
|
+ }
|
|
|
+ if (StringUtils.isNotBlank(operator.getUserId())) {
|
|
|
+ return operator.getUserId();
|
|
|
+ }
|
|
|
+ if (StringUtils.isNotBlank(operator.getOpenId())) {
|
|
|
+ return operator.getOpenId();
|
|
|
+ }
|
|
|
+ if (StringUtils.isNotBlank(operator.getUnionId())) {
|
|
|
+ return operator.getUnionId();
|
|
|
+ }
|
|
|
+ return "unknown";
|
|
|
+ }
|
|
|
+
|
|
|
+ private String readActionValue(Map<String, Object> values, String key) {
|
|
|
+ if (values == null || values.isEmpty() || StringUtils.isBlank(key)) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ Object rawValue = values.get(key);
|
|
|
+ return rawValue == null ? null : String.valueOf(rawValue);
|
|
|
+ }
|
|
|
+
|
|
|
+ private String readFormValue(Map<String, Object> formValues, String key) {
|
|
|
+ if (formValues == null || formValues.isEmpty() || StringUtils.isBlank(key)) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ Object rawValue = formValues.get(key);
|
|
|
+ if (rawValue == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ if (rawValue instanceof Map) {
|
|
|
+ Object value = ((Map<?, ?>) rawValue).get("value");
|
|
|
+ return value == null ? null : String.valueOf(value);
|
|
|
+ }
|
|
|
+ return String.valueOf(rawValue);
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<String> getTypeOptions() {
|
|
|
+ return ChangduSeriesCardConstants.TYPE_OPTIONS;
|
|
|
+ }
|
|
|
+
|
|
|
+ private String trimError(String errorMsg) {
|
|
|
+ String message = StringUtils.defaultIfBlank(errorMsg, "未知异常");
|
|
|
+ if (message.length() <= 1000) {
|
|
|
+ return message;
|
|
|
+ }
|
|
|
+ return message.substring(0, 1000);
|
|
|
+ }
|
|
|
+
|
|
|
+ private P2CardActionTriggerResponse buildCardUpdate(String cardJson) {
|
|
|
+ P2CardActionTriggerResponse response = new P2CardActionTriggerResponse();
|
|
|
+ CallBackCard card = new CallBackCard();
|
|
|
+ card.setType("raw");
|
|
|
+ card.setData(JSON.parseObject(cardJson));
|
|
|
+ response.setCard(card);
|
|
|
+ return response;
|
|
|
+ }
|
|
|
+
|
|
|
+ private P2CardActionTriggerResponse buildToast(String type, String content) {
|
|
|
+ P2CardActionTriggerResponse response = new P2CardActionTriggerResponse();
|
|
|
+ CallBackToast toast = new CallBackToast();
|
|
|
+ toast.setType(type);
|
|
|
+ toast.setContent(content);
|
|
|
+ response.setToast(toast);
|
|
|
+ return response;
|
|
|
+ }
|
|
|
+}
|