# M3TokenService.java

    package cloud.wconnect.finance.service.pl;

    import cloud.wconnect.commonlib.pojo.dto.m3Config.TenantM3ConfigDTO;
    import cloud.wconnect.commonlib.util.HttpUtils;
    import cloud.wconnect.commonlib.util.JsonUtils;
    import cloud.wconnect.finance.pojo.dto.pl.m3api.M3TokenDTO;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.google.common.collect.Maps;
    import jakarta.validation.Valid;
    import lombok.extern.slf4j.Slf4j;
    import org.apache.commons.lang3.StringUtils;
    import org.redisson.api.RBucket;
    import org.redisson.api.RLock;
    import org.redisson.api.RedissonClient;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.retry.annotation.Backoff;
    import org.springframework.retry.annotation.Retryable;
    import org.springframework.stereotype.Service;

    import java.time.Duration;
    import java.util.HashMap;

    /**
     * M3 Token接口
     *
     * @author lichen
     * @date 2025/9/26
     */
    @Slf4j
    @Service
    public class M3TokenService {

        public static final String KEY_PREFIX_FOR_TENANT_TOKEN = "wconnect:tenant:m3-token:";
        public static final String KEY_PREFIX_FOR_LOCK = "wconnect:tenant:m3-token:lock:";
        @Autowired
        private ObjectMapper objectMapper;
        @Autowired
        private RedissonClient redissonClient;
        @Value(value = "${m3-api.get-token}")
        private String getTokenUrl;

        /**
         * 获取token
         *
         * @param tenantM3ConfigDTO 租户的M3 Config
         * @return {@link String}
         */
        @Retryable(
                retryFor = {Exception.class}, // 指定需要重试的异常类型
                maxAttempts = 3, // 最大重试次数（包括首次调用）, 默认3次
                backoff = @Backoff(delay = 300, multiplier = 2) //初次重试延迟300毫秒，后续每次重试延迟时间翻倍（乘以2）
        )
        public String getToken(TenantM3ConfigDTO tenantM3ConfigDTO) {
            Integer tenantId = tenantM3ConfigDTO.getTenantId();
            String clientId = tenantM3ConfigDTO.getClientId();
            String clientSecret = tenantM3ConfigDTO.getClientSecret();

            // 从缓存中获取Token数据
            RBucket<String> bucket = redissonClient.getBucket(getKey(tenantId));
            String token = bucket.get();
            if (StringUtils.isNotBlank(token)) {
                return token;
            }

            RLock lock = redissonClient.getLock(getLockKey(tenantId));
            try {
                if (lock.tryLock()) {
                    try {
                        // 双重检查：再次从缓存获取，避免重复请求
                        token = bucket.get();
                        if (StringUtils.isNotBlank(token)) {
                            return token;
                        }

                        // 请求参数
                        HashMap<String, Object> hashMap = Maps.newHashMapWithExpectedSize(2);
                        hashMap.put("client-id", clientId);
                        hashMap.put("client-secret", clientSecret);
                        String requestJson = JsonUtils.convertObjToString(objectMapper, hashMap);

                        M3TokenDTO m3TokenDTO = HttpUtils.postJson(
                                getTokenUrl,
                                null,
                                requestJson,
                                M3TokenDTO.class,
                                objectMapper
                        );
                        if (m3TokenDTO != null && StringUtils.isNotBlank(m3TokenDTO.getToken())) {
                            //过期时间设置为15分钟
                            bucket.set(m3TokenDTO.getToken(), Duration.ofMinutes(15));
                            return m3TokenDTO.getToken();
                        } else {
                            log.warn("Received empty or invalid token from remote server for tenantId={}, clientId={}", tenantId, clientId);
                            throw new RuntimeException("Received empty or invalid token from remote server");
                        }
                    } finally {
                        try {
                            lock.unlock();
                        } catch (Exception unlockException) {
                            log.warn("Failed to unlock for tenantId={}", tenantId, unlockException);
                        }
                    }
                } else {
                    throw new RuntimeException("Failed to acquire lock for token fetching");
                }
            } catch (Exception e) {
                log.error("Error occurred while fetching token for tenantId={}, clientId={}", tenantId, clientId, e);
                throw new RuntimeException("Token fetching failed", e);
            }
        }

        /**
         * 获取 key
         *
         * @param tenantId 租户 id
         * @return key
         * @author lichen
         * @date 2025/09/26
         */
        private String getKey(Integer tenantId) {
            return KEY_PREFIX_FOR_TENANT_TOKEN + tenantId.toString();
        }

        /**
         * 获取锁
         *
         * @param tenantId 租户 id
         * @return java.lang.String
         * @author lichen
         * @date 2025/09/26
         */
        private String getLockKey(Integer tenantId) {
            return KEY_PREFIX_FOR_LOCK + tenantId.toString();
        }

        /**
         * 删除 token
         *
         * @param tenantId 租户 id
         * @return void
         * @author lichen
         * @date 2025/9/30
         */
        public void deleteToken(Integer tenantId) {
            redissonClient.getBucket(getKey(tenantId)).delete();
        }

    }