|
|
@@ -0,0 +1,192 @@
|
|
|
+package com.malk.sdzl.service.Impl;
|
|
|
+
|
|
|
+import com.alibaba.excel.EasyExcel;
|
|
|
+import com.alibaba.fastjson.JSON;
|
|
|
+import com.alibaba.fastjson.JSONObject;
|
|
|
+import com.malk.sdzl.entity.dto.YqbApiResponse;
|
|
|
+import com.malk.sdzl.entity.dto.YqbOptionExcelRow;
|
|
|
+import com.malk.sdzl.entity.dto.YqbOptionPushRequest;
|
|
|
+import com.malk.sdzl.entity.dto.YqbOptionPushResult;
|
|
|
+import com.malk.sdzl.service.YqbOptionPushService;
|
|
|
+import com.malk.utils.UtilHttp;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+import org.springframework.beans.factory.annotation.Value;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+import org.springframework.web.multipart.MultipartFile;
|
|
|
+
|
|
|
+import java.io.IOException;
|
|
|
+import java.time.LocalDateTime;
|
|
|
+import java.util.HashMap;
|
|
|
+import java.util.List;
|
|
|
+import java.util.Map;
|
|
|
+
|
|
|
+/** 易企报辅助选项推送服务。 */
|
|
|
+@Slf4j
|
|
|
+@Service
|
|
|
+public class YqbOptionPushServiceImpl implements YqbOptionPushService {
|
|
|
+
|
|
|
+ private static final String TOKEN_PATH = "/auth/getAccessToken.rjson";
|
|
|
+ private static final String ADD_OPTION_PATH = "/reimburse/auxiliary/v2/item/add.rjson";
|
|
|
+ private static final int TOKEN_EXPIRE_MINUTES = 120;
|
|
|
+ private static final int TOKEN_REFRESH_AHEAD_MINUTES = 30;
|
|
|
+
|
|
|
+ private final Object tokenLock = new Object();
|
|
|
+ private volatile String accessToken;
|
|
|
+ private volatile LocalDateTime accessTokenExpireAt;
|
|
|
+
|
|
|
+ @Value("${yqb.base-url}")
|
|
|
+ private String baseUrl;
|
|
|
+ @Value("${yqb.app-key}")
|
|
|
+ private String appKey;
|
|
|
+ @Value("${yqb.app-secret}")
|
|
|
+ private String appSecret;
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public YqbApiResponse<Object> pushOne(YqbOptionPushRequest request) {
|
|
|
+ validateRequest(request);
|
|
|
+
|
|
|
+ String token = getAccessToken();
|
|
|
+ String requestUrl = trimRightSlash(baseUrl) + ADD_OPTION_PATH
|
|
|
+ + "?appKey=" + appKey + "&accessToken=" + token;
|
|
|
+
|
|
|
+ Map<String, Object> headers = new HashMap<>();
|
|
|
+ headers.put("Content-Type", "application/json;charset=UTF-8");
|
|
|
+
|
|
|
+ String responseBody = UtilHttp.doPost(requestUrl, headers, null, request.toRequestBody());
|
|
|
+ YqbApiResponse<Object> response = JSON.parseObject(responseBody, YqbApiResponse.class);
|
|
|
+ if (response == null) {
|
|
|
+ throw new IllegalStateException("易企报新增选项接口返回为空");
|
|
|
+ }
|
|
|
+ return response;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public YqbOptionPushResult pushByExcel(MultipartFile file) {
|
|
|
+ if (file == null || file.isEmpty()) {
|
|
|
+ throw new IllegalArgumentException("上传文件不能为空");
|
|
|
+ }
|
|
|
+ String fileName = file.getOriginalFilename();
|
|
|
+ if (fileName == null || (!fileName.toLowerCase().endsWith(".xlsx")
|
|
|
+ && !fileName.toLowerCase().endsWith(".xls"))) {
|
|
|
+ throw new IllegalArgumentException("仅支持 .xlsx 或 .xls 格式的 Excel 文件");
|
|
|
+ }
|
|
|
+
|
|
|
+ List<YqbOptionExcelRow> rows;
|
|
|
+ try {
|
|
|
+ rows = EasyExcel.read(file.getInputStream())
|
|
|
+ .head(YqbOptionExcelRow.class)
|
|
|
+ .sheet()
|
|
|
+ .doReadSync();
|
|
|
+ } catch (IOException e) {
|
|
|
+ throw new IllegalArgumentException("Excel 文件读取失败:" + e.getMessage(), e);
|
|
|
+ }
|
|
|
+
|
|
|
+ YqbOptionPushResult result = new YqbOptionPushResult();
|
|
|
+ result.setTotal(rows.size());
|
|
|
+
|
|
|
+ for (int i = 0; i < rows.size(); i++) {
|
|
|
+ YqbOptionExcelRow row = rows.get(i);
|
|
|
+ YqbOptionPushResult.ItemResult itemResult = new YqbOptionPushResult.ItemResult();
|
|
|
+ itemResult.setRowNumber(i + 2);
|
|
|
+ itemResult.setItemCode(row.getItemCode());
|
|
|
+ itemResult.setItemName(row.getItemName());
|
|
|
+
|
|
|
+ try {
|
|
|
+ YqbApiResponse<Object> pushResponse = pushOne(row.toRequest());
|
|
|
+ itemResult.setSuccess(pushResponse.isSuccess());
|
|
|
+ itemResult.setResult(pushResponse.getResult());
|
|
|
+ itemResult.setMessage(pushResponse.getMessage());
|
|
|
+ if (pushResponse.isSuccess()) {
|
|
|
+ result.setSuccessCount(result.getSuccessCount() + 1);
|
|
|
+ } else {
|
|
|
+ result.setFailCount(result.getFailCount() + 1);
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("第{}行推送辅助选项失败,itemCode={}", i + 2, row.getItemCode(), e);
|
|
|
+ itemResult.setSuccess(false);
|
|
|
+ itemResult.setMessage(e.getMessage());
|
|
|
+ result.setFailCount(result.getFailCount() + 1);
|
|
|
+ }
|
|
|
+ result.getDetails().add(itemResult);
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * token 有效期 2 小时;剩余不足 30 分钟时重新获取。
|
|
|
+ * 双重检查与同步锁避免并发请求同时刷新 token。
|
|
|
+ */
|
|
|
+ private String getAccessToken() {
|
|
|
+ if (isTokenUsable()) {
|
|
|
+ return accessToken;
|
|
|
+ }
|
|
|
+ synchronized (tokenLock) {
|
|
|
+ if (isTokenUsable()) {
|
|
|
+ return accessToken;
|
|
|
+ }
|
|
|
+ return refreshAccessToken();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean isTokenUsable() {
|
|
|
+ return accessToken != null
|
|
|
+ && accessTokenExpireAt != null
|
|
|
+ && LocalDateTime.now().plusMinutes(TOKEN_REFRESH_AHEAD_MINUTES).isBefore(accessTokenExpireAt);
|
|
|
+ }
|
|
|
+
|
|
|
+ private String refreshAccessToken() {
|
|
|
+ String requestUrl = trimRightSlash(baseUrl) + TOKEN_PATH;
|
|
|
+ Map<String, Object> headers = new HashMap<>();
|
|
|
+ headers.put("Content-Type", "application/json;charset=UTF-8");
|
|
|
+
|
|
|
+ Map<String, Object> requestBody = new HashMap<>();
|
|
|
+ requestBody.put("appKey", appKey);
|
|
|
+ requestBody.put("appSecret", appSecret);
|
|
|
+
|
|
|
+ String responseBody = UtilHttp.doPost(requestUrl, headers, null, requestBody);
|
|
|
+ JSONObject jsonObject = JSON.parseObject(responseBody);
|
|
|
+ Integer result = jsonObject == null ? null : jsonObject.getInteger("result");
|
|
|
+ String token = jsonObject == null || jsonObject.getJSONObject("data") == null
|
|
|
+ ? null : jsonObject.getJSONObject("data").getString("accessToken");
|
|
|
+
|
|
|
+ if (result == null || result < 100 || result >= 200 || token == null || token.trim().isEmpty()) {
|
|
|
+ String message = jsonObject == null ? responseBody : jsonObject.getString("message");
|
|
|
+ throw new IllegalStateException("获取易企报 accessToken 失败:" + message);
|
|
|
+ }
|
|
|
+
|
|
|
+ accessToken = token;
|
|
|
+ accessTokenExpireAt = LocalDateTime.now().plusMinutes(TOKEN_EXPIRE_MINUTES);
|
|
|
+ log.info("易企报 accessToken 已刷新,过期时间:{}", accessTokenExpireAt);
|
|
|
+ return accessToken;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void validateRequest(YqbOptionPushRequest request) {
|
|
|
+ if (request == null) {
|
|
|
+ throw new IllegalArgumentException("请求参数不能为空");
|
|
|
+ }
|
|
|
+ if (isBlank(request.getCategoryCode())) {
|
|
|
+ throw new IllegalArgumentException("categoryCode不能为空");
|
|
|
+ }
|
|
|
+ if (isBlank(request.getItemCode())) {
|
|
|
+ throw new IllegalArgumentException("itemCode不能为空");
|
|
|
+ }
|
|
|
+ if (isBlank(request.getItemName())) {
|
|
|
+ throw new IllegalArgumentException("itemName不能为空");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean isBlank(String value) {
|
|
|
+ return value == null || value.trim().isEmpty();
|
|
|
+ }
|
|
|
+
|
|
|
+ private String trimRightSlash(String url) {
|
|
|
+ if (url == null || url.trim().isEmpty()) {
|
|
|
+ throw new IllegalArgumentException("yqb.base-url 未配置");
|
|
|
+ }
|
|
|
+ String result = url.trim();
|
|
|
+ while (result.endsWith("/")) {
|
|
|
+ result = result.substring(0, result.length() - 1);
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+}
|