MediaAuthServiceImpl.java 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510
  1. package com.nuojing.media.auth.service.impl;
  2. import com.fasterxml.jackson.databind.JsonNode;
  3. import com.fasterxml.jackson.databind.ObjectMapper;
  4. import com.nuojing.admin.common.BusinessException;
  5. import com.nuojing.admin.context.CurrentUser;
  6. import com.nuojing.admin.context.CurrentUserContext;
  7. import com.nuojing.media.auth.client.BytedanceMediaClient;
  8. import com.nuojing.media.auth.client.KuaishouMediaClient;
  9. import com.nuojing.media.auth.client.TencentMediaClient;
  10. import com.nuojing.media.auth.domain.dto.AuthUrlRequest;
  11. import com.nuojing.media.auth.domain.entity.MediaAdvertiserAccount;
  12. import com.nuojing.media.auth.domain.entity.MediaAuthAccount;
  13. import com.nuojing.media.auth.domain.entity.MediaAuthConfig;
  14. import com.nuojing.media.auth.domain.vo.AuthUrlVO;
  15. import com.nuojing.media.auth.domain.vo.MediaAuthConfigVO;
  16. import com.nuojing.media.auth.mapper.MediaAuthMapper;
  17. import com.nuojing.media.auth.service.MediaAuthService;
  18. import com.nuojing.media.callback.domain.vo.CallbackResultVO;
  19. import com.nuojing.media.common.enums.MediaType;
  20. import lombok.RequiredArgsConstructor;
  21. import lombok.extern.slf4j.Slf4j;
  22. import org.springframework.stereotype.Service;
  23. import org.springframework.transaction.annotation.Transactional;
  24. import org.springframework.util.StringUtils;
  25. import org.springframework.web.util.UriComponentsBuilder;
  26. import java.util.ArrayList;
  27. import java.util.List;
  28. import java.util.Map;
  29. @Slf4j
  30. @Service
  31. @RequiredArgsConstructor
  32. public class MediaAuthServiceImpl implements MediaAuthService {
  33. private final MediaAuthMapper mediaAuthMapper;
  34. private final BytedanceMediaClient bytedanceMediaClient;
  35. private final KuaishouMediaClient kuaishouMediaClient;
  36. private final TencentMediaClient tencentMediaClient;
  37. private final ObjectMapper objectMapper;
  38. @Override
  39. public List<MediaAuthConfigVO> configs() {
  40. return mediaAuthMapper.selectActiveConfigs(null).stream().map(config -> {
  41. MediaType media = MediaType.ofMediaId(config.getMediaType());
  42. MediaAuthConfigVO vo = new MediaAuthConfigVO();
  43. vo.setMediaId(media.getMediaId());
  44. vo.setMediaCode(media.getCode());
  45. vo.setMediaName(media.getName());
  46. vo.setAppId(config.getAppId());
  47. vo.setRedirectUri(config.getRedirectUri());
  48. vo.setCallbackReturnUrl(config.getCallbackReturnUrl());
  49. vo.setConfigured(StringUtils.hasText(config.getAuthUrl())
  50. && (StringUtils.hasText(config.getAppSecret()) || StringUtils.hasText(config.getClientSecret())));
  51. return vo;
  52. }).toList();
  53. }
  54. @Override
  55. public AuthUrlVO getAuthUrl(AuthUrlRequest request) {
  56. if (request.getMediaId() == null) {
  57. throw new BusinessException(400, "请输入媒体类型");
  58. }
  59. MediaType media = MediaType.ofMediaId(request.getMediaId());
  60. MediaAuthConfig config = findConfig(media);
  61. if (config == null || !StringUtils.hasText(config.getAuthUrl())) {
  62. throw new BusinessException(400, media.getName() + "授权配置未完成");
  63. }
  64. Long userId = currentUserId();
  65. String state = buildState(userId, config.getAppId());
  66. AuthUrlVO vo = new AuthUrlVO();
  67. vo.setMediaId(media.getMediaId());
  68. vo.setMediaCode(media.getCode());
  69. vo.setMediaName(media.getName());
  70. vo.setAppId(config.getAppId());
  71. vo.setUserId(userId);
  72. vo.setState(state);
  73. vo.setAuthUrl(appendState(config.getAuthUrl(), state));
  74. return vo;
  75. }
  76. @Override
  77. @Transactional(rollbackFor = Exception.class)
  78. public CallbackResultVO handleCallback(Integer mediaId, Map<String, String> params) {
  79. MediaType media = MediaType.ofMediaId(mediaId);
  80. CallbackResultVO result = new CallbackResultVO();
  81. result.setMediaType(media.getMediaId());
  82. result.setBindStatus(2);
  83. result.setState(params.get("state"));
  84. result.setTime(System.currentTimeMillis());
  85. result.setCallbackReturnUrl(callbackReturnUrl(media));
  86. try {
  87. validateCallbackParams(media, params);
  88. Long adminAdvertiserId = handleCallback(media, params);
  89. result.setCode(200);
  90. result.setAdminAdvertiserId(adminAdvertiserId);
  91. result.setMessage(media.getName() + "授权回调已接收");
  92. log.info("{} authorization callback received, state={}, params={}", media.getName(), params.get("state"), params);
  93. } catch (Exception ex) {
  94. result.setCode(500);
  95. result.setMessage(ex.getMessage());
  96. log.error("{} authorization callback failed, params={}", media.getName(), params, ex);
  97. }
  98. return result;
  99. }
  100. @Override
  101. public String buildReturnUrl(CallbackResultVO result) {
  102. String callbackReturnUrl = result.getCallbackReturnUrl();
  103. if (!StringUtils.hasText(callbackReturnUrl)) {
  104. callbackReturnUrl = callbackReturnUrl(MediaType.ofMediaId(result.getMediaType()));
  105. }
  106. UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(callbackReturnUrl)
  107. .queryParam("code", result.getCode())
  108. .queryParam("mediaType", result.getMediaType())
  109. .queryParam("bindStatus", result.getBindStatus())
  110. .queryParam("time", result.getTime());
  111. if (result.getAdminAdvertiserId() != null) {
  112. builder.queryParam("adminAdvertiserId", result.getAdminAdvertiserId());
  113. }
  114. if (StringUtils.hasText(result.getState())) {
  115. builder.queryParam("state", result.getState());
  116. }
  117. if (StringUtils.hasText(result.getMessage())) {
  118. builder.queryParam(result.getCode() == 200 ? "message" : "msg", result.getMessage());
  119. }
  120. return builder.build().encode().toUriString();
  121. }
  122. private MediaAuthConfig findConfig(MediaType media) {
  123. return mediaAuthMapper.selectActiveConfigs(media.getMediaId()).stream().findFirst().orElse(null);
  124. }
  125. private String callbackReturnUrl(MediaType media) {
  126. return mediaAuthMapper.selectActiveConfigs(media.getMediaId()).stream()
  127. .map(MediaAuthConfig::getCallbackReturnUrl)
  128. .filter(StringUtils::hasText)
  129. .findFirst()
  130. .orElseThrow(() -> new BusinessException(400, media.getName() + "前端回跳地址未配置"));
  131. }
  132. private Long currentUserId() {
  133. CurrentUser currentUser = CurrentUserContext.get();
  134. return currentUser == null ? null : currentUser.getUserId();
  135. }
  136. private String buildState(Long userId, Long appId) {
  137. return safe(userId) + "_" + safe(appId);
  138. }
  139. private String safe(Long value) {
  140. return value == null ? "" : String.valueOf(value);
  141. }
  142. private String appendState(String authUrl, String state) {
  143. UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(authUrl);
  144. builder.replaceQueryParam("state", state);
  145. return builder.build().encode().toUriString();
  146. }
  147. private void validateCallbackParams(MediaType media, Map<String, String> params) {
  148. if (!StringUtils.hasText(params.get("state"))) {
  149. throw new BusinessException(400, "state不能为空");
  150. }
  151. if (media == MediaType.BYTEDANCE && !StringUtils.hasText(params.get("app_id"))) {
  152. throw new BusinessException(400, "app_id不能为空");
  153. }
  154. if (media == MediaType.BYTEDANCE && !StringUtils.hasText(params.get("auth_code"))) {
  155. throw new BusinessException(400, "auth_code不能为空");
  156. }
  157. if (media == MediaType.KUAISHOU && !StringUtils.hasText(params.get("auth_code"))) {
  158. throw new BusinessException(400, "auth_code不能为空");
  159. }
  160. if (media == MediaType.TENCENT && !StringUtils.hasText(params.get("authorization_code"))) {
  161. throw new BusinessException(400, "authorization_code不能为空");
  162. }
  163. }
  164. private Long handleBytedanceCallback(Map<String, String> params) {
  165. Long appId = parseLong(params.get("app_id"), "app_id");
  166. MediaAuthConfig config = mediaAuthMapper.selectActiveConfigByAppId(MediaType.BYTEDANCE.getMediaId(), appId);
  167. if (config == null) {
  168. throw new BusinessException(400, "通过此appId未获取到相关配置信息,请联系技术");
  169. }
  170. JsonNode tokenResult = bytedanceMediaClient.exchangeToken(config, params.get("auth_code"));
  171. JsonNode tokenData = bytedanceMediaClient.requireSuccess(tokenResult, "获取字节token");
  172. String accessToken = tokenData.path("access_token").asText();
  173. String refreshToken = tokenData.path("refresh_token").asText();
  174. if (!StringUtils.hasText(accessToken)) {
  175. throw new BusinessException(500, "获取字节token失败:access_token为空");
  176. }
  177. JsonNode advertiserResult = bytedanceMediaClient.advertiserList(accessToken);
  178. JsonNode advertiserData = bytedanceMediaClient.requireSuccess(advertiserResult, "获取字节授权账户");
  179. JsonNode advertisers = advertiserData.path("list");
  180. if (!advertisers.isArray()) {
  181. throw new BusinessException(500, "获取字节授权账户list信息为空");
  182. }
  183. Long userId = parseStateUserId(params.get("state"));
  184. JsonNode adminAdvertiser = null;
  185. List<JsonNode> customerAccounts = new ArrayList<>();
  186. for (JsonNode advertiser : advertisers) {
  187. String accountRole = advertiser.path("account_role").asText();
  188. if ("CUSTOMER_ADMIN".equals(accountRole)) {
  189. adminAdvertiser = advertiser;
  190. }
  191. if ("CUSTOMER_ADMIN".equals(accountRole) || "CUSTOMER_OPERATOR".equals(accountRole)) {
  192. customerAccounts.add(advertiser);
  193. }
  194. }
  195. if (adminAdvertiser == null) {
  196. throw new BusinessException(500, "未获取到字节管家账户");
  197. }
  198. Long adminAdvertiserId = adminAdvertiser.path("advertiser_id").asLong();
  199. MediaAuthAccount authAccount = toAuthAccount(config, adminAdvertiser, accessToken, refreshToken, userId);
  200. applyUserInfo(authAccount, accessToken);
  201. mediaAuthMapper.replaceAuthAccount(authAccount);
  202. // /oauth2/advertiser/get 返回的是工作台(纵横组织)账号,需进一步展开组织下的真实投放账户
  203. syncBytedanceDeliveryAccounts(accessToken, customerAccounts, adminAdvertiserId);
  204. return adminAdvertiserId;
  205. }
  206. private void syncBytedanceDeliveryAccounts(String accessToken, List<JsonNode> customerAccounts, Long adminAdvertiserId) {
  207. for (JsonNode org : customerAccounts) {
  208. long ccAccountId = org.path("advertiser_id").asLong();
  209. try {
  210. int page = 1;
  211. int totalPage = 1;
  212. do {
  213. JsonNode data = bytedanceMediaClient.requireSuccess(
  214. bytedanceMediaClient.ccAdvertiserList(accessToken, ccAccountId, page),
  215. "获取字节纵横组织投放账户");
  216. JsonNode list = data.path("list");
  217. if (list.isArray()) {
  218. for (JsonNode item : list) {
  219. long advertiserId = item.path("advertiser_id").asLong(0);
  220. if (advertiserId > 0) {
  221. mediaAuthMapper.replaceAdvertiserAccount(toDeliveryAccount(MediaType.BYTEDANCE, adminAdvertiserId, item));
  222. }
  223. }
  224. }
  225. totalPage = Math.max(data.path("page_info").path("total_page").asInt(page), page);
  226. page++;
  227. } while (page <= totalPage);
  228. } catch (Exception ex) {
  229. log.warn("获取字节纵横组织投放账户失败,跳过该组织:ccAccountId={}", ccAccountId, ex);
  230. }
  231. }
  232. }
  233. private MediaAdvertiserAccount toDeliveryAccount(MediaType media, Long adminAdvertiserId, JsonNode item) {
  234. MediaAdvertiserAccount account = new MediaAdvertiserAccount();
  235. account.setMediaType(media.getMediaId());
  236. account.setAdminAdvertiserId(adminAdvertiserId);
  237. account.setAdvertiserId(item.path("advertiser_id").asLong());
  238. account.setAdvertiserName(item.path("advertiser_name").asText(null));
  239. account.setAccountRole(item.path("advertiser_type").asText(null));
  240. account.setIsValid(1);
  241. account.setRawResponse(toJson(item));
  242. return account;
  243. }
  244. private Long handleCallback(MediaType media, Map<String, String> params) {
  245. if (media == MediaType.BYTEDANCE) {
  246. return handleBytedanceCallback(params);
  247. }
  248. if (media == MediaType.KUAISHOU) {
  249. return handleKuaishouCallback(params);
  250. }
  251. if (media == MediaType.TENCENT) {
  252. return handleTencentCallback(params);
  253. }
  254. throw new BusinessException(400, media.getName() + "回调暂未接入");
  255. }
  256. private Long handleKuaishouCallback(Map<String, String> params) {
  257. MediaAuthConfig config = requireConfig(MediaType.KUAISHOU);
  258. JsonNode tokenResult = kuaishouMediaClient.exchangeToken(config, params.get("auth_code"));
  259. JsonNode tokenData = kuaishouMediaClient.requireSuccess(tokenResult, "获取快手token");
  260. String accessToken = tokenData.path("access_token").asText();
  261. String refreshToken = tokenData.path("refresh_token").asText(null);
  262. if (!StringUtils.hasText(accessToken)) {
  263. throw new BusinessException(500, "获取快手token失败:access_token为空");
  264. }
  265. // 广告账户来源:token响应的advertiser_ids(广告账户授权场景);为空则用approval/list拉取(用户授权/服务商场景)
  266. List<Long> advertiserIds = new ArrayList<>();
  267. JsonNode tokenAdvertiserIds = tokenData.path("advertiser_ids");
  268. if (tokenAdvertiserIds.isArray() && !tokenAdvertiserIds.isEmpty()) {
  269. tokenAdvertiserIds.forEach(node -> advertiserIds.add(node.asLong()));
  270. } else {
  271. advertiserIds.addAll(syncKuaishouApprovalList(config, accessToken));
  272. }
  273. if (advertiserIds.isEmpty()) {
  274. throw new BusinessException(500, "未获取到快手授权广告账户");
  275. }
  276. Long userId = parseStateUserId(params.get("state"));
  277. // 磁力引擎token响应未单独返回管家账户ID,优先取响应内的账户字段,否则以首个广告账户作为管家标识
  278. Long adminAdvertiserId = resolveKuaishouAdminId(tokenData, advertiserIds);
  279. MediaAuthAccount authAccount = baseAuthAccount(config, MediaType.KUAISHOU, userId, adminAdvertiserId, accessToken, refreshToken);
  280. authAccount.setRawResponse(toJson(tokenData));
  281. mediaAuthMapper.replaceAuthAccount(authAccount);
  282. for (Long advertiserId : advertiserIds) {
  283. mediaAuthMapper.replaceAdvertiserAccount(toKuaishouAdvertiserAccount(adminAdvertiserId, advertiserId));
  284. }
  285. return adminAdvertiserId;
  286. }
  287. private List<Long> syncKuaishouApprovalList(MediaAuthConfig config, String accessToken) {
  288. List<Long> advertiserIds = new ArrayList<>();
  289. int pageNo = 1;
  290. boolean isEnd;
  291. do {
  292. JsonNode data = kuaishouMediaClient.requireSuccess(
  293. kuaishouMediaClient.approvalList(config, accessToken, pageNo), "获取快手授权广告账户列表");
  294. JsonNode details = data.path("details");
  295. if (details.isArray()) {
  296. for (JsonNode detail : details) {
  297. long advertiserId = detail.asLong(0);
  298. if (advertiserId > 0) {
  299. advertiserIds.add(advertiserId);
  300. }
  301. }
  302. }
  303. isEnd = data.path("isEnd").asBoolean(true);
  304. pageNo++;
  305. } while (!isEnd);
  306. return advertiserIds;
  307. }
  308. private Long handleTencentCallback(Map<String, String> params) {
  309. MediaAuthConfig config = requireConfig(MediaType.TENCENT);
  310. JsonNode tokenResult = tencentMediaClient.exchangeToken(config, params.get("authorization_code"));
  311. JsonNode tokenData = tencentMediaClient.requireSuccess(tokenResult, "获取腾讯token");
  312. String accessToken = tokenData.path("access_token").asText();
  313. String refreshToken = tokenData.path("refresh_token").asText(null);
  314. if (!StringUtils.hasText(accessToken)) {
  315. throw new BusinessException(500, "获取腾讯token失败:access_token为空");
  316. }
  317. JsonNode authorizer = tokenData.path("authorizer_info");
  318. long accountId = authorizer.path("account_id").asLong(0);
  319. if (accountId <= 0) {
  320. throw new BusinessException(500, "未获取到腾讯授权账号ID");
  321. }
  322. Long adminAdvertiserId = accountId;
  323. Long userId = parseStateUserId(params.get("state"));
  324. MediaAuthAccount authAccount = baseAuthAccount(config, MediaType.TENCENT, userId, adminAdvertiserId, accessToken, refreshToken);
  325. authAccount.setAccountRole(authorizer.path("account_role_type").asText(null));
  326. authAccount.setRawResponse(toJson(authorizer));
  327. mediaAuthMapper.replaceAuthAccount(authAccount);
  328. syncTencentAdvertisers(accessToken, adminAdvertiserId);
  329. return adminAdvertiserId;
  330. }
  331. private void syncTencentAdvertisers(String accessToken, Long adminAdvertiserId) {
  332. int page = 1;
  333. int totalPage = 1;
  334. do {
  335. JsonNode relations;
  336. try {
  337. relations = tencentMediaClient.requireSuccess(
  338. tencentMediaClient.businessManagerRelations(accessToken, page), "获取腾讯商务管家广告主");
  339. } catch (Exception ex) {
  340. log.warn("获取腾讯商务管家广告主列表失败,仅保留管家授权信息:adminAdvertiserId={}", adminAdvertiserId, ex);
  341. return;
  342. }
  343. JsonNode list = relations.path("list");
  344. if (list.isArray()) {
  345. for (JsonNode item : list) {
  346. long advertiserId = item.path("account_id").asLong(0);
  347. if (advertiserId > 0) {
  348. mediaAuthMapper.replaceAdvertiserAccount(toTencentAdvertiserAccount(adminAdvertiserId, item));
  349. }
  350. }
  351. }
  352. totalPage = Math.max(relations.path("page_info").path("total_page").asInt(page), page);
  353. page++;
  354. } while (page <= totalPage);
  355. }
  356. private MediaAuthConfig requireConfig(MediaType media) {
  357. MediaAuthConfig config = findConfig(media);
  358. if (config == null) {
  359. throw new BusinessException(400, media.getName() + "授权配置不存在");
  360. }
  361. return config;
  362. }
  363. private Long resolveKuaishouAdminId(JsonNode tokenData, List<Long> advertiserIds) {
  364. for (String field : new String[]{"user_id", "account_id", "corporation_id"}) {
  365. long value = tokenData.path(field).asLong(0);
  366. if (value > 0) {
  367. return value;
  368. }
  369. }
  370. return advertiserIds.get(0);
  371. }
  372. private MediaAuthAccount baseAuthAccount(MediaAuthConfig config, MediaType media, Long userId, Long adminAdvertiserId, String accessToken, String refreshToken) {
  373. MediaAuthAccount account = new MediaAuthAccount();
  374. account.setMediaType(media.getMediaId());
  375. account.setAppId(config.getAppId());
  376. account.setUserId(userId);
  377. account.setAdminAdvertiserId(adminAdvertiserId);
  378. account.setIsValid(1);
  379. account.setAccessToken(accessToken);
  380. account.setRefreshToken(refreshToken);
  381. return account;
  382. }
  383. private MediaAdvertiserAccount toKuaishouAdvertiserAccount(Long adminAdvertiserId, Long advertiserId) {
  384. MediaAdvertiserAccount account = new MediaAdvertiserAccount();
  385. account.setMediaType(MediaType.KUAISHOU.getMediaId());
  386. account.setAdminAdvertiserId(adminAdvertiserId);
  387. account.setAdvertiserId(advertiserId);
  388. account.setIsValid(1);
  389. account.setRawResponse("{\"advertiser_id\":" + advertiserId + "}");
  390. return account;
  391. }
  392. private MediaAdvertiserAccount toTencentAdvertiserAccount(Long adminAdvertiserId, JsonNode item) {
  393. MediaAdvertiserAccount account = new MediaAdvertiserAccount();
  394. account.setMediaType(MediaType.TENCENT.getMediaId());
  395. account.setAdminAdvertiserId(adminAdvertiserId);
  396. account.setAdvertiserId(item.path("account_id").asLong());
  397. account.setAdvertiserName(item.path("corporation_name").asText(null));
  398. account.setAccountRole(item.path("account_type").asText(null));
  399. account.setIsValid(1);
  400. account.setRawResponse(toJson(item));
  401. return account;
  402. }
  403. private MediaAuthAccount toAuthAccount(MediaAuthConfig config, JsonNode advertiser, String accessToken, String refreshToken, Long userId) {
  404. MediaAuthAccount account = new MediaAuthAccount();
  405. account.setMediaType(MediaType.BYTEDANCE.getMediaId());
  406. account.setAppId(config.getAppId());
  407. account.setUserId(userId);
  408. account.setAdminAdvertiserId(advertiser.path("advertiser_id").asLong());
  409. account.setAdminAdvertiserName(advertiser.path("advertiser_name").asText(null));
  410. account.setAccountRole(advertiser.path("account_role").asText(null));
  411. account.setAdvertiserRole(advertiser.path("advertiser_role").isMissingNode() ? null : advertiser.path("advertiser_role").asInt());
  412. account.setIsValid(advertiser.path("is_valid").asBoolean(false) ? 1 : 0);
  413. account.setAccessToken(accessToken);
  414. account.setRefreshToken(refreshToken);
  415. account.setRawResponse(toJson(advertiser));
  416. return account;
  417. }
  418. private void applyUserInfo(MediaAuthAccount account, String accessToken) {
  419. try {
  420. JsonNode userInfoResult = bytedanceMediaClient.userInfo(accessToken);
  421. JsonNode data = bytedanceMediaClient.requireSuccess(userInfoResult, "获取字节用户信息");
  422. String email = data.path("email").asText(null);
  423. String displayName = data.path("display_name").asText(null);
  424. if (StringUtils.hasText(email)) {
  425. account.setEmail(email);
  426. }
  427. if (StringUtils.hasText(displayName)) {
  428. account.setAdminAdvertiserName(displayName);
  429. }
  430. } catch (Exception ex) {
  431. log.warn("获取字节用户信息失败,不影响授权入库:adminAdvertiserId={}", account.getAdminAdvertiserId(), ex);
  432. }
  433. }
  434. private Long parseStateUserId(String state) {
  435. if (!StringUtils.hasText(state)) {
  436. return null;
  437. }
  438. String[] parts = state.split("_");
  439. return parts.length == 0 ? null : parseNullableLong(parts[0]);
  440. }
  441. private Long parseLong(String value, String fieldName) {
  442. Long parsed = parseNullableLong(value);
  443. if (parsed == null) {
  444. throw new BusinessException(400, fieldName + "格式错误");
  445. }
  446. return parsed;
  447. }
  448. private Long parseNullableLong(String value) {
  449. if (!StringUtils.hasText(value)) {
  450. return null;
  451. }
  452. try {
  453. return Long.valueOf(value);
  454. } catch (NumberFormatException ex) {
  455. return null;
  456. }
  457. }
  458. private String toJson(JsonNode node) {
  459. try {
  460. return objectMapper.writeValueAsString(node);
  461. } catch (Exception ex) {
  462. return "{}";
  463. }
  464. }
  465. }