2个文件已删除
18个文件已添加
13个文件已修改
| New file |
| | |
| | | package com.stylefeng.guns.modular.system.dao; |
| | | |
| | | import org.apache.ibatis.annotations.Param; |
| | | import org.apache.ibatis.annotations.Select; |
| | | import org.apache.ibatis.annotations.Update; |
| | | |
| | | import java.util.List; |
| | | |
| | | /** |
| | | * 高德地图key管理 Mapper(注解方式,无需XML) |
| | | */ |
| | | public interface GDKeyMapper { |
| | | |
| | | /** |
| | | * 查询可用key(启用、未删除、未用完) |
| | | */ |
| | | @Select("SELECT gdKey FROM t_gaode_account WHERE status = 1 AND delFlag = 0 AND useStatus <> 3") |
| | | List<String> selectAvailableKeys(); |
| | | |
| | | /** |
| | | * 查询已用完的key(用于每日凌晨重置校验) |
| | | */ |
| | | @Select("SELECT gdKey FROM t_gaode_account WHERE status = 1 AND delFlag = 0 AND useStatus = 3") |
| | | List<String> selectExhaustedKeys(); |
| | | |
| | | /** |
| | | * 更新key的使用状态 |
| | | */ |
| | | @Update("UPDATE t_gaode_account SET useStatus = #{useStatus} WHERE gdKey = #{gdKey} AND delFlag = 0") |
| | | int updateUseStatus(@Param("gdKey") String gdKey, @Param("useStatus") Integer useStatus); |
| | | } |
| | |
| | | import org.springframework.util.MultiValueMap; |
| | | import org.springframework.web.client.RestTemplate; |
| | | |
| | | import java.util.Map; |
| | | |
| | | |
| | | /** |
| | | * 高德猎鹰服务 |
| | | * key次数用完时自动切换可用key(切换后自动重建serverId) |
| | | */ |
| | | @Component |
| | | public class GDFalconUtil implements ApplicationRunner { |
| | | |
| | | private String key = "c473445a931ee8c42eef8b63fc3602c5"; |
| | | |
| | | @Autowired |
| | | private RestTemplate restTemplate; |
| | | |
| | | @Autowired |
| | | private GDKeyManager gdKeyManager; |
| | | |
| | | private static String serverId = null; |
| | | |
| | | /** |
| | | * 当前serverId对应的key(key切换后需重建serverId) |
| | | */ |
| | | private static volatile String currentKey = null; |
| | | |
| | | |
| | | public String getServerId() { |
| | |
| | | } |
| | | |
| | | |
| | | |
| | | /** |
| | | * 初始化创建服务 |
| | | */ |
| | | public void init(){ |
| | | if(serverId == null){ |
| | | JSONArray jsonArray = this.selectServer(); |
| | | if(null == jsonArray){ |
| | | serverId = this.createServer("server"); |
| | | }else{ |
| | | serverId = jsonArray.getJSONObject(0).getString("sid"); |
| | | if (gdKeyManager == null || restTemplate == null) { |
| | | // 非容器实例(如 GunsApplication.main 中 new 出来的对象),由容器内bean完成初始化 |
| | | return; |
| | | } |
| | | ensureServerId(); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 参数构造器:key与sid切换后需按最新值重新构造 |
| | | */ |
| | | private interface ParamsBuilder { |
| | | MultiValueMap<String, Object> build(String key, String sid); |
| | | } |
| | | |
| | | /** |
| | | * 确保serverId与当前使用的key对应: |
| | | * key切换后自动查询/重建serverId;key用完自动切换可用key重试 |
| | | */ |
| | | private synchronized String ensureServerId(){ |
| | | int maxAttempts = 5; |
| | | String lastKey = gdKeyManager.getKey(); |
| | | for (int i = 0; i < maxAttempts; i++) { |
| | | String gdKey = gdKeyManager.getKey(); |
| | | lastKey = gdKey; |
| | | if (serverId != null && gdKey.equals(currentKey)) { |
| | | return gdKey; |
| | | } |
| | | // 1. 查询该key下的服务列表 |
| | | JSONArray jsonArray = null; |
| | | try { |
| | | String url = "https://tsapi.amap.com/v1/track/service/list?key=" + gdKey; |
| | | String forObject = restTemplate.getForObject(url, String.class); |
| | | JSONObject jsonObject = JSON.parseObject(forObject); |
| | | if (GDKeyManager.isFalconExhausted(jsonObject)) { |
| | | gdKeyManager.reportExhausted(gdKey); |
| | | serverId = null; |
| | | continue; |
| | | } |
| | | if (jsonObject.getIntValue("errcode") == 10000) { |
| | | JSONObject data = jsonObject.getJSONObject("data"); |
| | | if (null != data) { |
| | | jsonArray = data.getJSONArray("results"); |
| | | } |
| | | } |
| | | } catch (Exception e) { |
| | | loggerWarn("查询猎鹰服务列表异常", e); |
| | | } |
| | | if (jsonArray != null && jsonArray.size() > 0) { |
| | | serverId = jsonArray.getJSONObject(0).getString("sid"); |
| | | currentKey = gdKey; |
| | | return gdKey; |
| | | } |
| | | // 2. 无服务:创建新服务 |
| | | String sid = createServerInternal("server", gdKey); |
| | | if (sid != null) { |
| | | serverId = sid; |
| | | currentKey = gdKey; |
| | | return gdKey; |
| | | } |
| | | // 3. 创建失败(可能key用完):切换key重试 |
| | | gdKeyManager.reportExhausted(gdKey); |
| | | serverId = null; |
| | | } |
| | | return lastKey; |
| | | } |
| | | |
| | | private void loggerWarn(String msg, Exception e) { |
| | | System.err.println("[GDFalconUtil] " + msg + ":" + e.getMessage()); |
| | | } |
| | | |
| | | /** |
| | | * 执行猎鹰POST表单请求:key次数用完时自动切换key并重建serverId重试 |
| | | */ |
| | | private String doFalconPost(String url, ParamsBuilder builder) { |
| | | String lastResp = null; |
| | | int maxAttempts = 5; |
| | | for (int i = 0; i < maxAttempts; i++) { |
| | | String gdKey = ensureServerId(); |
| | | String sid = serverId; |
| | | MultiValueMap<String, Object> params = builder.build(gdKey, sid); |
| | | HttpHeaders headers = new HttpHeaders(); |
| | | headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); |
| | | HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(params, headers); |
| | | String resp = restTemplate.postForObject(url, requestEntity, String.class); |
| | | lastResp = resp; |
| | | JSONObject jsonObject; |
| | | try { |
| | | jsonObject = JSON.parseObject(resp); |
| | | } catch (Exception e) { |
| | | return resp; |
| | | } |
| | | if (GDKeyManager.isFalconExhausted(jsonObject)) { |
| | | gdKeyManager.reportExhausted(gdKey); |
| | | serverId = null; |
| | | continue; |
| | | } |
| | | return resp; |
| | | } |
| | | return lastResp; |
| | | } |
| | | |
| | | /** |
| | | * 执行猎鹰GET请求:key次数用完时自动切换可用key重试 |
| | | */ |
| | | private String doFalconGet(String baseUrl) { |
| | | String lastResp = null; |
| | | int maxAttempts = 5; |
| | | for (int i = 0; i < maxAttempts; i++) { |
| | | String gdKey = gdKeyManager.getKey(); |
| | | String url = baseUrl + (baseUrl.contains("?") ? "&" : "?") + "key=" + gdKey; |
| | | String resp = restTemplate.getForObject(url, String.class); |
| | | lastResp = resp; |
| | | JSONObject jsonObject; |
| | | try { |
| | | jsonObject = JSON.parseObject(resp); |
| | | } catch (Exception e) { |
| | | return resp; |
| | | } |
| | | if (GDKeyManager.isFalconExhausted(jsonObject)) { |
| | | gdKeyManager.reportExhausted(gdKey); |
| | | continue; |
| | | } |
| | | return resp; |
| | | } |
| | | return lastResp; |
| | | } |
| | | |
| | | |
| | |
| | | * @return |
| | | */ |
| | | public String createServer(String name){ |
| | | return createServerInternal(name, gdKeyManager.getKey()); |
| | | } |
| | | |
| | | /** |
| | | * 创建新的服务(内部直连,供ensureServerId使用) |
| | | */ |
| | | private String createServerInternal(String name, String key){ |
| | | String url = "https://tsapi.amap.com/v1/track/service/add"; |
| | | //调用推送 |
| | | HttpHeaders headers = new HttpHeaders(); |
| | |
| | | |
| | | |
| | | /** |
| | | * 查询服务 |
| | | * @return |
| | | */ |
| | | private JSONArray selectServer(){ |
| | | String url = "https://tsapi.amap.com/v1/track/service/list?key=" + key; |
| | | String forObject = restTemplate.getForObject(url, String.class); |
| | | JSONObject jsonObject = JSON.parseObject(forObject); |
| | | if(jsonObject.getIntValue("errcode") == 10000){ |
| | | JSONObject data = jsonObject.getJSONObject("data"); |
| | | if(null != data){ |
| | | return data.getJSONArray("results"); |
| | | } |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | |
| | | /** |
| | | * 创建终端数据 |
| | | * @param name |
| | | * @return |
| | |
| | | if(tid != null){ |
| | | return tid; |
| | | } |
| | | String url = "https://tsapi.amap.com/v1/track/terminal/add"; |
| | | //调用推送 |
| | | HttpHeaders headers = new HttpHeaders(); |
| | | // 以表单的方式提交 |
| | | headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); |
| | | //将请求头部和参数合成一个请求 |
| | | MultiValueMap<String, Object> params = new LinkedMultiValueMap<>(); |
| | | params.add("key", key); |
| | | params.add("sid", serverId); |
| | | params.add("name", name); |
| | | HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(params, headers); |
| | | String s = restTemplate.postForObject(url, requestEntity , String.class); |
| | | String s = doFalconPost("https://tsapi.amap.com/v1/track/terminal/add", (key, sid) -> { |
| | | MultiValueMap<String, Object> params = new LinkedMultiValueMap<>(); |
| | | params.add("key", key); |
| | | params.add("sid", sid); |
| | | params.add("name", name); |
| | | return params; |
| | | }); |
| | | JSONObject jsonObject = JSON.parseObject(s); |
| | | if(jsonObject.getIntValue("errcode") == 10000){ |
| | | JSONObject data = jsonObject.getJSONObject("data"); |
| | |
| | | * @return |
| | | */ |
| | | public String selectTerminal(String name){ |
| | | String url = "https://tsapi.amap.com/v1/track/terminal/list?key=" + key + "&name=" + name; |
| | | String forObject = restTemplate.getForObject(url, String.class); |
| | | String forObject = doFalconGet("https://tsapi.amap.com/v1/track/terminal/list?name=" + name); |
| | | JSONObject jsonObject = JSON.parseObject(forObject); |
| | | if(jsonObject.getIntValue("errcode") == 10000){ |
| | | JSONObject data = jsonObject.getJSONObject("data"); |
| | |
| | | * @return |
| | | */ |
| | | public String createTrack(String tid){ |
| | | String url = "https://tsapi.amap.com/v1/track/trace/add"; |
| | | //调用推送 |
| | | HttpHeaders headers = new HttpHeaders(); |
| | | // 以表单的方式提交 |
| | | headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); |
| | | //将请求头部和参数合成一个请求 |
| | | MultiValueMap<String, Object> params = new LinkedMultiValueMap<>(); |
| | | params.add("key", key); |
| | | params.add("sid", serverId); |
| | | params.add("tid", tid); |
| | | HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(params, headers); |
| | | String s = restTemplate.postForObject(url, requestEntity , String.class); |
| | | String s = doFalconPost("https://tsapi.amap.com/v1/track/trace/add", (key, sid) -> { |
| | | MultiValueMap<String, Object> params = new LinkedMultiValueMap<>(); |
| | | params.add("key", key); |
| | | params.add("sid", sid); |
| | | params.add("tid", tid); |
| | | return params; |
| | | }); |
| | | JSONObject jsonObject = JSON.parseObject(s); |
| | | if(jsonObject.getIntValue("errcode") == 10000){ |
| | | return jsonObject.getString("data"); |
| | |
| | | * @param trid 轨迹id |
| | | */ |
| | | public void deleteTrack(String tid, String trid){ |
| | | String url = "https://tsapi.amap.com/v1/track/trace/delete"; |
| | | //调用推送 |
| | | HttpHeaders headers = new HttpHeaders(); |
| | | // 以表单的方式提交 |
| | | headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); |
| | | //将请求头部和参数合成一个请求 |
| | | MultiValueMap<String, Object> params = new LinkedMultiValueMap<>(); |
| | | params.add("key", key); |
| | | params.add("sid", serverId); |
| | | params.add("tid", tid); |
| | | params.add("trid", trid); |
| | | HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(params, headers); |
| | | String s = restTemplate.postForObject(url, requestEntity , String.class); |
| | | String s = doFalconPost("https://tsapi.amap.com/v1/track/trace/delete", (key, sid) -> { |
| | | MultiValueMap<String, Object> params = new LinkedMultiValueMap<>(); |
| | | params.add("key", key); |
| | | params.add("sid", sid); |
| | | params.add("tid", tid); |
| | | params.add("trid", trid); |
| | | return params; |
| | | }); |
| | | JSONObject jsonObject = JSON.parseObject(s); |
| | | if(jsonObject.getIntValue("errcode") != 10000){ |
| | | System.err.println(jsonObject.getString("errmsg")); |
| | | } |
| | | } |
| | | |
| | | |
| | | // |
| | | // public Map<String, Object> queryTrsearch(){ |
| | | // |
| | | // } |
| | | } |
| New file |
| | |
| | | package com.stylefeng.guns.modular.system.util; |
| | | |
| | | import com.alibaba.fastjson.JSON; |
| | | import com.alibaba.fastjson.JSONObject; |
| | | import com.stylefeng.guns.modular.system.dao.GDKeyMapper; |
| | | import org.slf4j.Logger; |
| | | import org.slf4j.LoggerFactory; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.scheduling.annotation.Scheduled; |
| | | import org.springframework.stereotype.Component; |
| | | import org.springframework.web.client.RestTemplate; |
| | | |
| | | import javax.annotation.PostConstruct; |
| | | import java.util.ArrayList; |
| | | import java.util.List; |
| | | import java.util.Set; |
| | | import java.util.concurrent.ConcurrentHashMap; |
| | | import java.util.concurrent.CopyOnWriteArrayList; |
| | | import java.util.concurrent.atomic.AtomicInteger; |
| | | |
| | | /** |
| | | * 高德地图key管理器 |
| | | * <p> |
| | | * 功能: |
| | | * 1. key池管理:从 t_gaode_account 表加载可用key(启用、未删除、未用完),轮询获取; |
| | | * 表为空或全部用尽时降级使用兜底key(原硬编码key)。 |
| | | * 2. 自动切换:调用方检测到key"次数用完"后调用 reportExhausted 上报, |
| | | * 管理器将key移出内存池并置库表 useStatus=3(已用完)。 |
| | | * 3. 每日凌晨重置校验:每天 00:00 将 useStatus=3 的key重置为未使用并放回池, |
| | | * 随后调用高德最轻量接口校验配额是否真正恢复;未恢复的key重新标记已用完, |
| | | * 进入延迟重试队列,每30分钟校验一次直到恢复额度。 |
| | | * 4. 兜底key:当天用完仅内存禁用,次日凌晨自动恢复;不写库(表内无对应记录)。 |
| | | */ |
| | | @Component |
| | | public class GDKeyManager { |
| | | |
| | | private static final Logger logger = LoggerFactory.getLogger(GDKeyManager.class); |
| | | |
| | | /** |
| | | * 兜底key(原硬编码key):司机端 |
| | | */ |
| | | public static final String FALLBACK_KEY = "39b31fa22955efa8fce9eed487241db9"; |
| | | |
| | | /** |
| | | * 配额校验接口:最轻量的通用Web服务接口,每次仅消耗1次配额 |
| | | */ |
| | | private static final String VERIFY_URL = "https://restapi.amap.com/v3/geocode/geo?output=JSON&address=%E5%8C%97%E4%BA%AC%E5%B8%82&key="; |
| | | |
| | | @Autowired |
| | | private GDKeyMapper gdKeyMapper; |
| | | |
| | | @Autowired |
| | | private RestTemplate restTemplate; |
| | | |
| | | /** |
| | | * 内存可用key池(refreshPool整体替换,保证getKey不会看到clear/addAll中间态) |
| | | */ |
| | | private volatile List<String> availableKeys = new CopyOnWriteArrayList<>(); |
| | | |
| | | /** |
| | | * 轮询下标 |
| | | */ |
| | | private final AtomicInteger index = new AtomicInteger(0); |
| | | |
| | | /** |
| | | * 延迟校验队列:key -> 已重试次数(每日凌晨校验未恢复的key) |
| | | */ |
| | | private final ConcurrentHashMap<String, Integer> pendingVerifyKeys = new ConcurrentHashMap<>(); |
| | | |
| | | /** |
| | | * 兜底key当日是否已用完 |
| | | */ |
| | | private volatile boolean fallbackExhausted = false; |
| | | |
| | | /** |
| | | * 已标记"使用中"(useStatus=2)的key集合,避免热路径每次取用都写库 |
| | | */ |
| | | private final Set<String> inUseKeys = ConcurrentHashMap.newKeySet(); |
| | | |
| | | @PostConstruct |
| | | public void init() { |
| | | refreshPool(); |
| | | } |
| | | |
| | | /** |
| | | * 刷新可用key池 |
| | | */ |
| | | public synchronized void refreshPool() { |
| | | try { |
| | | List<String> keys = gdKeyMapper.selectAvailableKeys(); |
| | | List<String> newPool = new CopyOnWriteArrayList<>(); |
| | | if (keys != null) { |
| | | newPool.addAll(keys); |
| | | } |
| | | // 整体替换,避免clear/addAll中间态导致getKey短暂看到空池误降级兜底key |
| | | availableKeys = newPool; |
| | | logger.info("[GDKeyManager] 高德key池刷新完成,可用key数:{}", newPool.size()); |
| | | } catch (Exception e) { |
| | | // 表不存在(用户端/司机端未建表时)等异常:降级使用兜底key,不影响系统运行 |
| | | logger.warn("[GDKeyManager] 加载高德key失败(t_gaode_account表可能不存在),降级使用兜底key", e); |
| | | availableKeys = new CopyOnWriteArrayList<>(); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 获取一个可用key:池非空轮询池内key;池空使用兜底key(当日未用完时) |
| | | */ |
| | | public String getKey() { |
| | | List<String> pool = availableKeys; |
| | | if (!pool.isEmpty()) { |
| | | String key = pickFromPool(pool); |
| | | markInUse(key); |
| | | return key; |
| | | } |
| | | if (!fallbackExhausted) { |
| | | return FALLBACK_KEY; |
| | | } |
| | | // 兜底key当日也已用完:尝试刷新池(可能有新录入的key) |
| | | refreshPool(); |
| | | pool = availableKeys; |
| | | if (!pool.isEmpty()) { |
| | | String key = pickFromPool(pool); |
| | | markInUse(key); |
| | | return key; |
| | | } |
| | | return FALLBACK_KEY; |
| | | } |
| | | |
| | | /** |
| | | * 从内存池轮询取一个key(使用调用方传入的池快照,避免与定时刷新交叉导致越界) |
| | | */ |
| | | private String pickFromPool(List<String> pool) { |
| | | int i = Math.abs(index.getAndIncrement() % pool.size()); |
| | | return pool.get(i); |
| | | } |
| | | |
| | | /** |
| | | * 将表内key标记为"使用中"(useStatus=2): |
| | | * 仅key首次被取用时写库一次;兜底key不写库;写库失败不影响取key流程。 |
| | | */ |
| | | private void markInUse(String key) { |
| | | if (key == null || FALLBACK_KEY.equals(key) || inUseKeys.contains(key)) { |
| | | return; |
| | | } |
| | | try { |
| | | gdKeyMapper.updateUseStatus(key, 2); |
| | | inUseKeys.add(key); |
| | | logger.info("[GDKeyManager] 高德key[{}]开始使用,已标记为使用中", key); |
| | | } catch (Exception e) { |
| | | // 表不存在等异常:本次不标记,下次取用再尝试,不影响业务 |
| | | logger.warn("[GDKeyManager] 标记高德key[{}]使用中失败(表可能不存在),不影响使用", key); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 定时刷新key池:每5分钟一次,让管理后台新录入的key及时生效 |
| | | */ |
| | | @Scheduled(fixedDelay = 5 * 60 * 1000, initialDelay = 60 * 1000) |
| | | public void scheduledRefreshPool() { |
| | | refreshPool(); |
| | | } |
| | | |
| | | /** |
| | | * 上报某个key次数用完: |
| | | * 表内key -> 移出内存池 + 置库表 useStatus=3; |
| | | * 兜底key -> 仅内存当日禁用(不写库),次日凌晨自动恢复。 |
| | | */ |
| | | public void reportExhausted(String usedKey) { |
| | | if (usedKey == null || usedKey.isEmpty()) { |
| | | return; |
| | | } |
| | | availableKeys.remove(usedKey); |
| | | inUseKeys.remove(usedKey); |
| | | if (FALLBACK_KEY.equals(usedKey)) { |
| | | fallbackExhausted = true; |
| | | logger.warn("[GDKeyManager] 兜底key次数已用完,今日禁用,次日凌晨自动恢复"); |
| | | return; |
| | | } |
| | | try { |
| | | gdKeyMapper.updateUseStatus(usedKey, 3); |
| | | logger.warn("[GDKeyManager] 高德key[{}]次数用完,已标记为已用完并移出key池", usedKey); |
| | | } catch (Exception e) { |
| | | logger.warn("[GDKeyManager] 更新高德key[{}]使用状态失败(表可能不存在),仅内存剔除", usedKey); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 判断Web服务API响应是否为"次数用完/不可用"(依据infocode) |
| | | */ |
| | | public static boolean isWebKeyExhausted(JSONObject resp) { |
| | | if (resp == null) { |
| | | return false; |
| | | } |
| | | String infocode = resp.getString("infocode"); |
| | | if (infocode == null || infocode.isEmpty()) { |
| | | return false; |
| | | } |
| | | switch (infocode) { |
| | | case "10001": // key不正确或过期 |
| | | case "10003": // 超过每日配额 |
| | | case "10004": // 访问过于频繁 |
| | | case "10005": // 超出每秒访问量限制 |
| | | case "10010": // 权限不足 |
| | | case "10012": // 服务未开放/权限不足 |
| | | case "10019": // 使用配额已用尽 |
| | | case "10020": // 并发超限 |
| | | case "10044": // 签名错误/未开通 |
| | | return true; |
| | | default: |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 判断猎鹰API响应是否为"次数用完"(依据errcode/errmsg) |
| | | */ |
| | | public static boolean isFalconExhausted(JSONObject resp) { |
| | | if (resp == null) { |
| | | return false; |
| | | } |
| | | int errcode = resp.getIntValue("errcode"); |
| | | if (errcode == 10000) { |
| | | return false; |
| | | } |
| | | if (errcode == 20001) { |
| | | return true; // 权限不足 |
| | | } |
| | | String errmsg = resp.getString("errmsg"); |
| | | if (errmsg == null) { |
| | | return false; |
| | | } |
| | | return errmsg.contains("配额") || errmsg.contains("上限") || errmsg.contains("次数") |
| | | || errmsg.contains("用完") || errmsg.contains("超限") |
| | | || errmsg.contains("quota") || errmsg.contains("limit"); |
| | | } |
| | | |
| | | // ==================== 每日凌晨重置 + API校验恢复额度 ==================== |
| | | |
| | | /** |
| | | * 每日凌晨 00:00 自动执行: |
| | | * 1. 将 useStatus=3(已用完)的key重置为未使用并放回内存池; |
| | | * 2. 调用高德API校验配额是否真正恢复; |
| | | * 3. 未恢复的key重新标记已用完,进入延迟重试队列,直到恢复额度。 |
| | | */ |
| | | @Scheduled(cron = "0 0 0 * * ?") |
| | | public void dailyResetAndVerify() { |
| | | logger.info("[GDKeyManager] 开始每日高德key配额重置校验"); |
| | | // 兜底key当日禁用标记次日自动恢复 |
| | | fallbackExhausted = false; |
| | | List<String> exhaustedKeys; |
| | | try { |
| | | exhaustedKeys = gdKeyMapper.selectExhaustedKeys(); |
| | | } catch (Exception e) { |
| | | logger.warn("[GDKeyManager] 查询已用完key失败(表可能不存在),跳过重置校验", e); |
| | | return; |
| | | } |
| | | if (exhaustedKeys == null || exhaustedKeys.isEmpty()) { |
| | | logger.info("[GDKeyManager] 无已用完的key,跳过重置校验"); |
| | | return; |
| | | } |
| | | for (String gdKey : exhaustedKeys) { |
| | | // 1. 先重置为未使用 |
| | | try { |
| | | gdKeyMapper.updateUseStatus(gdKey, 1); |
| | | } catch (Exception e) { |
| | | logger.warn("[GDKeyManager] 重置key[{}]使用状态失败", gdKey, e); |
| | | } |
| | | // 2. 调用API校验是否真正恢复额度 |
| | | boolean recovered = verifyQuota(gdKey); |
| | | if (recovered) { |
| | | if (!availableKeys.contains(gdKey)) { |
| | | availableKeys.add(gdKey); |
| | | } |
| | | pendingVerifyKeys.remove(gdKey); |
| | | logger.info("[GDKeyManager] 高德key[{}]配额已恢复,重新加入key池", gdKey); |
| | | } else { |
| | | // 未恢复:重新标记已用完,进入延迟重试队列 |
| | | try { |
| | | gdKeyMapper.updateUseStatus(gdKey, 3); |
| | | } catch (Exception e) { |
| | | logger.warn("[GDKeyManager] 回写key[{}]已用完状态失败", gdKey, e); |
| | | } |
| | | availableKeys.remove(gdKey); |
| | | pendingVerifyKeys.put(gdKey, 0); |
| | | logger.warn("[GDKeyManager] 高德key[{}]配额尚未恢复,进入延迟重试队列", gdKey); |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 延迟重试任务:每30分钟校验一次待恢复的key,直到恢复额度为止 |
| | | */ |
| | | @Scheduled(fixedDelay = 30 * 60 * 1000, initialDelay = 30 * 60 * 1000) |
| | | public void retryPendingKeys() { |
| | | if (pendingVerifyKeys.isEmpty()) { |
| | | return; |
| | | } |
| | | for (String gdKey : new ArrayList<>(pendingVerifyKeys.keySet())) { |
| | | boolean recovered = verifyQuota(gdKey); |
| | | if (recovered) { |
| | | try { |
| | | gdKeyMapper.updateUseStatus(gdKey, 1); |
| | | } catch (Exception e) { |
| | | logger.warn("[GDKeyManager] 恢复key[{}]使用状态失败", gdKey, e); |
| | | } |
| | | if (!availableKeys.contains(gdKey)) { |
| | | availableKeys.add(gdKey); |
| | | } |
| | | pendingVerifyKeys.remove(gdKey); |
| | | logger.info("[GDKeyManager] 高德key[{}]延迟校验通过,配额已恢复,重新加入key池", gdKey); |
| | | } else { |
| | | int count = pendingVerifyKeys.merge(gdKey, 1, Integer::sum); |
| | | logger.warn("[GDKeyManager] 高德key[{}]配额仍未恢复(已校验{}次),继续等待", gdKey, count); |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 调用高德最轻量接口校验配额是否恢复 |
| | | * |
| | | * @return true=已恢复(或无法判断,保守放行);false=明确未恢复(配额类错误) |
| | | */ |
| | | private boolean verifyQuota(String gdKey) { |
| | | try { |
| | | String resp = restTemplate.getForObject(VERIFY_URL + gdKey, String.class); |
| | | JSONObject json = JSON.parseObject(resp); |
| | | String status = json.getString("status"); |
| | | String infocode = json.getString("infocode"); |
| | | if ("1".equals(status) && "10000".equals(infocode)) { |
| | | return true; |
| | | } |
| | | // 权限类错误:该key未开通Web服务校验权限(如猎鹰专用key),无法判断配额,按已恢复处理 |
| | | if ("10044".equals(infocode) || "10012".equals(infocode) |
| | | || "10002".equals(infocode) || "10010".equals(infocode)) { |
| | | logger.info("[GDKeyManager] 高德key[{}]无Web服务校验权限(infocode={}),按已恢复处理", gdKey, infocode); |
| | | return true; |
| | | } |
| | | // 配额类错误:明确未恢复 |
| | | if ("10001".equals(infocode) || "10003".equals(infocode) || "10019".equals(infocode)) { |
| | | return false; |
| | | } |
| | | // 其他错误(限流/网络等):保守放行,真实调用失败时会被自动切换机制再次标记 |
| | | return true; |
| | | } catch (Exception e) { |
| | | logger.warn("[GDKeyManager] 校验key[{}]配额异常,按已恢复处理", gdKey, e); |
| | | return true; |
| | | } |
| | | } |
| | | } |
| | |
| | | @Autowired |
| | | private IGDInterfaceService gdInterfaceService; |
| | | |
| | | private String key = "c473445a931ee8c42eef8b63fc3602c5"; |
| | | @Autowired |
| | | private GDKeyManager gdKeyManager; |
| | | |
| | | private JSONArray jsonArray = new JSONArray(); |
| | | |
| | | /** |
| | | * 执行GET请求:key次数用完时自动切换可用key重试 |
| | | */ |
| | | private String doGetWithRetry(String baseUrl) { |
| | | String lastResp = null; |
| | | int maxAttempts = 5; |
| | | for (int i = 0; i < maxAttempts; i++) { |
| | | String key = gdKeyManager.getKey(); |
| | | String url = baseUrl + (baseUrl.contains("?") ? "&" : "?") + "key=" + key; |
| | | String resp = restTemplate.getForObject(url, String.class); |
| | | lastResp = resp; |
| | | JSONObject jsonObject; |
| | | try { |
| | | jsonObject = JSON.parseObject(resp); |
| | | } catch (Exception e) { |
| | | return resp; |
| | | } |
| | | if (GDKeyManager.isWebKeyExhausted(jsonObject)) { |
| | | gdKeyManager.reportExhausted(key); |
| | | continue; |
| | | } |
| | | return resp; |
| | | } |
| | | return lastResp; |
| | | } |
| | | |
| | | /** |
| | | * 执行POST请求(JSON体):key次数用完时自动切换可用key重试 |
| | | */ |
| | | private String doPostJsonWithRetry(String baseUrl, String jsonBody) { |
| | | String lastResp = null; |
| | | int maxAttempts = 5; |
| | | HttpHeaders headers = new HttpHeaders(); |
| | | MediaType type = MediaType.parseMediaType("application/json; charset=UTF-8"); |
| | | headers.setContentType(type); |
| | | headers.add("Accept", MediaType.APPLICATION_JSON.toString()); |
| | | for (int i = 0; i < maxAttempts; i++) { |
| | | String key = gdKeyManager.getKey(); |
| | | String url = baseUrl + (baseUrl.contains("?") ? "&" : "?") + "key=" + key; |
| | | HttpEntity<String> formEntity = new HttpEntity<>(jsonBody, headers); |
| | | String resp = restTemplate.postForObject(url, formEntity, String.class); |
| | | lastResp = resp; |
| | | JSONObject jsonObject; |
| | | try { |
| | | jsonObject = JSON.parseObject(resp); |
| | | } catch (Exception e) { |
| | | return resp; |
| | | } |
| | | if (GDKeyManager.isWebKeyExhausted(jsonObject)) { |
| | | gdKeyManager.reportExhausted(key); |
| | | continue; |
| | | } |
| | | return resp; |
| | | } |
| | | return lastResp; |
| | | } |
| | | |
| | | /** |
| | | * 执行PATCH请求(JSON体):key次数用完时自动切换可用key重试 |
| | | */ |
| | | private String doPatchJsonWithRetry(String baseUrl, String jsonBody) { |
| | | String lastResp = null; |
| | | int maxAttempts = 5; |
| | | HttpHeaders headers = new HttpHeaders(); |
| | | MediaType type = MediaType.parseMediaType("application/json; charset=UTF-8"); |
| | | headers.setContentType(type); |
| | | headers.add("Accept", MediaType.APPLICATION_JSON.toString()); |
| | | for (int i = 0; i < maxAttempts; i++) { |
| | | String key = gdKeyManager.getKey(); |
| | | String url = baseUrl + (baseUrl.contains("?") ? "&" : "?") + "key=" + key; |
| | | HttpEntity<String> formEntity = new HttpEntity<>(jsonBody, headers); |
| | | String resp = restTemplate.patchForObject(url, formEntity, String.class); |
| | | lastResp = resp; |
| | | JSONObject jsonObject; |
| | | try { |
| | | jsonObject = JSON.parseObject(resp); |
| | | } catch (Exception e) { |
| | | return resp; |
| | | } |
| | | if (GDKeyManager.isWebKeyExhausted(jsonObject)) { |
| | | gdKeyManager.reportExhausted(key); |
| | | continue; |
| | | } |
| | | return resp; |
| | | } |
| | | return lastResp; |
| | | } |
| | | |
| | | |
| | | @Bean |
| | |
| | | json.put("alert_condition", "enter;leave"); |
| | | json.put("desc", id); |
| | | |
| | | HttpHeaders headers = new HttpHeaders(); |
| | | MediaType type = MediaType.parseMediaType("application/json; charset=UTF-8"); |
| | | headers.setContentType(type); |
| | | headers.add("Accept", MediaType.APPLICATION_JSON.toString()); |
| | | HttpEntity<String> formEntity = new HttpEntity<>(json.toString(), headers); |
| | | |
| | | String url = "https://restapi.amap.com/v4/geofence/meta?key=" + key; |
| | | String js = restTemplate.postForObject(url, formEntity, String.class); |
| | | String js = doPostJsonWithRetry("https://restapi.amap.com/v4/geofence/meta", json.toString()); |
| | | JSONObject jsonObject = JSON.parseObject(js); |
| | | JSONObject data = jsonObject.getJSONObject("data"); |
| | | String status = data.getString("status"); |
| | |
| | | * 查询以创建的所有电子围栏 |
| | | */ |
| | | public String findElectricFenc(String center, String radius, String points){ |
| | | String url = "https://restapi.amap.com/v4/geofence/meta?key=" + key; |
| | | String forObject = restTemplate.getForObject(url, String.class); |
| | | String url = "https://restapi.amap.com/v4/geofence/meta"; |
| | | String forObject = doGetWithRetry(url); |
| | | JSONObject jsonObject = JSON.parseObject(forObject); |
| | | JSONObject data = jsonObject.getJSONObject("data"); |
| | | |
| | |
| | | if(page == 1){ |
| | | jsonArray = new JSONArray(); |
| | | } |
| | | String url = "https://restapi.amap.com/v4/geofence/meta?key=" + key + "&page_no=" + page + "&page_size=" + 30; |
| | | String forObject = restTemplate.getForObject(url, String.class); |
| | | String url = "https://restapi.amap.com/v4/geofence/meta?page_no=" + page + "&page_size=" + 30; |
| | | String forObject = doGetWithRetry(url); |
| | | JSONObject jsonObject = JSON.parseObject(forObject); |
| | | JSONObject data = jsonObject.getJSONObject("data"); |
| | | JSONArray rs_list = data.getJSONArray("rs_list"); |
| | |
| | | json.put("alert_condition", "enter;leave"); |
| | | json.put("desc", id + "围栏更新描述"); |
| | | |
| | | HttpHeaders headers = new HttpHeaders(); |
| | | MediaType type = MediaType.parseMediaType("application/json; charset=UTF-8"); |
| | | headers.setContentType(type); |
| | | headers.add("Accept", MediaType.APPLICATION_JSON.toString()); |
| | | HttpEntity<String> formEntity = new HttpEntity<>(json.toString(), headers); |
| | | |
| | | String url = "https://restapi.amap.com/v4/geofence/meta?key=" + key + "&gid=" + gid; |
| | | String s = restTemplate.patchForObject(url, formEntity, String.class); |
| | | String url = "https://restapi.amap.com/v4/geofence/meta?gid=" + gid; |
| | | String s = doPatchJsonWithRetry(url, json.toString()); |
| | | System.out.println(s); |
| | | |
| | | // gdInterfaceService.saveData("https://restapi.amap.com/v4/geofence/meta", "更新电子围栏"); |
| | |
| | | diu = "B78F99DD-2496-4A43-843B-172CDA2D9798"; |
| | | } |
| | | locations = locations + "," + (new Date().getTime() / 1000); |
| | | String url = "https://restapi.amap.com/v4/geofence/status?key=" + key + "&diu=" + diu + "&locations=" + locations; |
| | | String forObject = restTemplate.getForObject(url, String.class); |
| | | String url = "https://restapi.amap.com/v4/geofence/status?diu=" + diu + "&locations=" + locations; |
| | | String forObject = doGetWithRetry(url); |
| | | JSONObject jsonObject = JSON.parseObject(forObject); |
| | | List<String> ids = new ArrayList<>(); |
| | | JSONObject data = jsonObject.getJSONObject("data"); |
| | |
| | | * @return |
| | | */ |
| | | public boolean deleteElectricFenc(String gid){ |
| | | restTemplate.delete("https://restapi.amap.com/v4/geofence/meta?key={1}&gid={2}", key, gid); |
| | | restTemplate.delete("https://restapi.amap.com/v4/geofence/meta?key={1}&gid={2}", gdKeyManager.getKey(), gid); |
| | | System.out.println("删除:" ); |
| | | |
| | | // gdInterfaceService.saveData("https://restapi.amap.com/v4/geofence/meta", "删除电子围栏"); |
| | |
| | | * @return |
| | | */ |
| | | public Map<String, String> getDistance(String origins, String destination, Integer type){ |
| | | String url = "https://restapi.amap.com/v3/distance?key=" + key + "&origins=" + origins + "&destination=" + destination + |
| | | String url = "https://restapi.amap.com/v3/distance?origins=" + origins + "&destination=" + destination + |
| | | "&type=" + type; |
| | | String forObject = restTemplate.getForObject(url, String.class); |
| | | String forObject = doGetWithRetry(url); |
| | | JSONObject jsonObject = JSON.parseObject(forObject); |
| | | String status = jsonObject.getString("status"); |
| | | |
| | |
| | | * @return |
| | | */ |
| | | public Map<String, String> getDriving(String origins, String destination, String waypoints){ |
| | | String url = "https://restapi.amap.com/v3/direction/driving?key=" + key + "&origin=" + origins + "&destination=" + destination + |
| | | String url = "https://restapi.amap.com/v3/direction/driving?origin=" + origins + "&destination=" + destination + |
| | | "&waypoints=" + waypoints + "&extensions=base&strategy=0"; |
| | | String forObject = restTemplate.getForObject(url, String.class); |
| | | String forObject = doGetWithRetry(url); |
| | | JSONObject jsonObject = JSON.parseObject(forObject); |
| | | String status = jsonObject.getString("status"); |
| | | |
| | |
| | | @Component |
| | | public class GDMapGeocodingUtil { |
| | | |
| | | private String key = "c473445a931ee8c42eef8b63fc3602c5"; |
| | | |
| | | @Autowired |
| | | private RestTemplate restTemplate; |
| | | |
| | | @Autowired |
| | | private IGDInterfaceService gdInterfaceService; |
| | | |
| | | @Autowired |
| | | private GDKeyManager gdKeyManager; |
| | | |
| | | /** |
| | | * 执行GET请求:key次数用完时自动切换可用key重试 |
| | | */ |
| | | private String doGetWithRetry(String baseUrl) { |
| | | String lastResp = null; |
| | | int maxAttempts = 5; |
| | | for (int i = 0; i < maxAttempts; i++) { |
| | | String key = gdKeyManager.getKey(); |
| | | String url = baseUrl + (baseUrl.contains("?") ? "&" : "?") + "key=" + key; |
| | | String resp = restTemplate.getForObject(url, String.class); |
| | | lastResp = resp; |
| | | JSONObject jsonObject; |
| | | try { |
| | | jsonObject = JSON.parseObject(resp); |
| | | } catch (Exception e) { |
| | | return resp; |
| | | } |
| | | if (GDKeyManager.isWebKeyExhausted(jsonObject)) { |
| | | gdKeyManager.reportExhausted(key); |
| | | continue; |
| | | } |
| | | return resp; |
| | | } |
| | | return lastResp; |
| | | } |
| | | |
| | | |
| | | /** |
| | |
| | | return map; |
| | | } |
| | | |
| | | String url = "https://restapi.amap.com/v3/geocode/geo?key=" + key + "&output=JSON"; |
| | | String url = "https://restapi.amap.com/v3/geocode/geo?output=JSON"; |
| | | url += "&address=" + province + (ToolUtil.isNotEmpty(city) ? city : "") + (ToolUtil.isNotEmpty(county) ? county : "") + (ToolUtil.isNotEmpty(address) ? address : ""); |
| | | String forObject = restTemplate.getForObject(url, String.class); |
| | | String forObject = doGetWithRetry(url); |
| | | JSONObject jsonObject = JSON.parseObject(forObject); |
| | | String status = jsonObject.getString("status"); |
| | | List<String> list = new ArrayList<>(); |
| | |
| | | * @throws Exception |
| | | */ |
| | | public Map<String, String> geocode(String lon, String lan) throws Exception{ |
| | | String url = "https://restapi.amap.com/v3/geocode/regeo?key=" + key + "&location=" + lon + "," + lan; |
| | | String forObject = restTemplate.getForObject(url, String.class); |
| | | String url = "https://restapi.amap.com/v3/geocode/regeo?location=" + lon + "," + lan; |
| | | String forObject = doGetWithRetry(url); |
| | | JSONObject jsonObject = JSON.parseObject(forObject); |
| | | Map<String, String> map = new HashMap<>(); |
| | | |
| New file |
| | |
| | | package com.stylefeng.guns.modular.system.controller.general; |
| | | |
| | | import com.baomidou.mybatisplus.mapper.EntityWrapper; |
| | | import com.baomidou.mybatisplus.plugins.Page; |
| | | import com.stylefeng.guns.core.base.controller.BaseController; |
| | | import com.stylefeng.guns.core.base.tips.ErrorTip; |
| | | import com.stylefeng.guns.core.common.constant.factory.PageFactory; |
| | | import com.stylefeng.guns.core.log.LogObjectHolder; |
| | | import com.stylefeng.guns.core.shiro.ShiroKit; |
| | | import com.stylefeng.guns.core.util.SinataUtil; |
| | | import com.stylefeng.guns.modular.system.model.TGaodeAccount; |
| | | import com.stylefeng.guns.modular.system.service.ITGaodeAccountService; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.stereotype.Controller; |
| | | import org.springframework.ui.Model; |
| | | import org.springframework.web.bind.annotation.PathVariable; |
| | | import org.springframework.web.bind.annotation.RequestMapping; |
| | | import org.springframework.web.bind.annotation.RequestParam; |
| | | import org.springframework.web.bind.annotation.ResponseBody; |
| | | |
| | | import java.util.Date; |
| | | import java.util.Map; |
| | | |
| | | /** |
| | | * 高德地图账号控制器 |
| | | * |
| | | * @author stylefeng |
| | | * @Date 2026-09-21 |
| | | */ |
| | | @Controller |
| | | @RequestMapping("/tGaodeAccount") |
| | | public class TGaodeAccountController extends BaseController { |
| | | |
| | | private String PREFIX = "/system/tGaodeAccount/"; |
| | | |
| | | @Autowired |
| | | private ITGaodeAccountService tGaodeAccountService; |
| | | |
| | | /** |
| | | * 跳转到高德地图账号首页 |
| | | */ |
| | | @RequestMapping("") |
| | | public String index() { |
| | | return PREFIX + "tGaodeAccount.html"; |
| | | } |
| | | |
| | | /** |
| | | * 跳转到添加高德地图账号 |
| | | */ |
| | | @RequestMapping("/tGaodeAccount_add") |
| | | public String tGaodeAccountAdd() { |
| | | return PREFIX + "tGaodeAccount_add.html"; |
| | | } |
| | | |
| | | /** |
| | | * 跳转到修改高德地图账号 |
| | | */ |
| | | @RequestMapping("/tGaodeAccount_update/{tGaodeAccountId}") |
| | | public String tGaodeAccountUpdate(@PathVariable Integer tGaodeAccountId, Model model) { |
| | | TGaodeAccount tGaodeAccount = tGaodeAccountService.selectById(tGaodeAccountId); |
| | | model.addAttribute("item", tGaodeAccount); |
| | | LogObjectHolder.me().set(tGaodeAccount); |
| | | return PREFIX + "tGaodeAccount_edit.html"; |
| | | } |
| | | |
| | | /** |
| | | * 获取高德地图账号列表 |
| | | */ |
| | | @RequestMapping(value = "/list") |
| | | @ResponseBody |
| | | public Object list(String gdKey, Integer useStatus, Integer status) { |
| | | Page<Map<String, Object>> page = new PageFactory<Map<String, Object>>().defaultPage(); |
| | | page.setRecords(tGaodeAccountService.getAccountList(page, gdKey, useStatus, status)); |
| | | return super.packForBT(page); |
| | | } |
| | | |
| | | /** |
| | | * 新增高德地图账号 |
| | | */ |
| | | @RequestMapping(value = "/add") |
| | | @ResponseBody |
| | | public Object add(TGaodeAccount tGaodeAccount) { |
| | | if (SinataUtil.isEmpty(tGaodeAccount.getGdKey())) { |
| | | return new ErrorTip(500, "高德key不能为空"); |
| | | } |
| | | //校验高德key是否重复 |
| | | TGaodeAccount exist = tGaodeAccountService.selectOne(new EntityWrapper<TGaodeAccount>() |
| | | .eq("gdKey", tGaodeAccount.getGdKey()) |
| | | .eq("delFlag", 0)); |
| | | if (SinataUtil.isNotEmpty(exist)) { |
| | | return new ErrorTip(500, "该高德key已存在,请勿重复添加"); |
| | | } |
| | | if (SinataUtil.isEmpty(tGaodeAccount.getUseStatus())) { |
| | | tGaodeAccount.setUseStatus(1); |
| | | } |
| | | if (SinataUtil.isEmpty(tGaodeAccount.getStatus())) { |
| | | tGaodeAccount.setStatus(1); |
| | | } |
| | | tGaodeAccount.setDelFlag(0); |
| | | tGaodeAccount.setInsertTime(new Date()); |
| | | tGaodeAccount.setInsertUser(ShiroKit.getUser().getId()); |
| | | tGaodeAccountService.insert(tGaodeAccount); |
| | | return SUCCESS_TIP; |
| | | } |
| | | |
| | | /** |
| | | * 删除高德地图账号(逻辑删除) |
| | | */ |
| | | @RequestMapping(value = "/delete") |
| | | @ResponseBody |
| | | public Object delete(@RequestParam Integer tGaodeAccountId) { |
| | | TGaodeAccount tGaodeAccount = tGaodeAccountService.selectById(tGaodeAccountId); |
| | | if (SinataUtil.isNotEmpty(tGaodeAccount)) { |
| | | tGaodeAccount.setDelFlag(1); |
| | | tGaodeAccount.setUpdateTime(new Date()); |
| | | tGaodeAccount.setUpdateUser(ShiroKit.getUser().getId()); |
| | | tGaodeAccountService.updateById(tGaodeAccount); |
| | | } |
| | | return SUCCESS_TIP; |
| | | } |
| | | |
| | | /** |
| | | * 修改高德地图账号 |
| | | */ |
| | | @RequestMapping(value = "/update") |
| | | @ResponseBody |
| | | public Object update(TGaodeAccount tGaodeAccount) { |
| | | if (SinataUtil.isEmpty(tGaodeAccount.getGdKey())) { |
| | | return new ErrorTip(500, "高德key不能为空"); |
| | | } |
| | | //校验高德key是否重复(排除自身) |
| | | TGaodeAccount exist = tGaodeAccountService.selectOne(new EntityWrapper<TGaodeAccount>() |
| | | .eq("gdKey", tGaodeAccount.getGdKey()) |
| | | .eq("delFlag", 0) |
| | | .ne("id", tGaodeAccount.getId())); |
| | | if (SinataUtil.isNotEmpty(exist)) { |
| | | return new ErrorTip(500, "该高德key已存在,请勿重复添加"); |
| | | } |
| | | tGaodeAccount.setUpdateTime(new Date()); |
| | | tGaodeAccount.setUpdateUser(ShiroKit.getUser().getId()); |
| | | tGaodeAccountService.updateById(tGaodeAccount); |
| | | return SUCCESS_TIP; |
| | | } |
| | | |
| | | /** |
| | | * 操作高德地图账号(启用/禁用) |
| | | */ |
| | | @RequestMapping(value = "/opt") |
| | | @ResponseBody |
| | | public Object opt(@RequestParam Integer tGaodeAccountId, @RequestParam Integer optType) { |
| | | TGaodeAccount tGaodeAccount = tGaodeAccountService.selectById(tGaodeAccountId); |
| | | if (SinataUtil.isNotEmpty(tGaodeAccount)) { |
| | | if (1 == optType.intValue()) { //启用 |
| | | tGaodeAccount.setStatus(1); |
| | | } else if (2 == optType.intValue()) { //禁用 |
| | | tGaodeAccount.setStatus(2); |
| | | } |
| | | tGaodeAccount.setUpdateTime(new Date()); |
| | | tGaodeAccount.setUpdateUser(ShiroKit.getUser().getId()); |
| | | tGaodeAccountService.updateById(tGaodeAccount); |
| | | } |
| | | return SUCCESS_TIP; |
| | | } |
| | | } |
| New file |
| | |
| | | package com.stylefeng.guns.modular.system.dao; |
| | | |
| | | import org.apache.ibatis.annotations.Param; |
| | | import org.apache.ibatis.annotations.Select; |
| | | import org.apache.ibatis.annotations.Update; |
| | | |
| | | import java.util.List; |
| | | |
| | | /** |
| | | * 高德地图key管理 Mapper(注解方式,无需XML) |
| | | */ |
| | | public interface GDKeyMapper { |
| | | |
| | | /** |
| | | * 查询可用key(启用、未删除、未用完) |
| | | */ |
| | | @Select("SELECT gdKey FROM t_gaode_account WHERE status = 1 AND delFlag = 0 AND useStatus <> 3") |
| | | List<String> selectAvailableKeys(); |
| | | |
| | | /** |
| | | * 查询已用完的key(用于每日凌晨重置校验) |
| | | */ |
| | | @Select("SELECT gdKey FROM t_gaode_account WHERE status = 1 AND delFlag = 0 AND useStatus = 3") |
| | | List<String> selectExhaustedKeys(); |
| | | |
| | | /** |
| | | * 更新key的使用状态 |
| | | */ |
| | | @Update("UPDATE t_gaode_account SET useStatus = #{useStatus} WHERE gdKey = #{gdKey} AND delFlag = 0") |
| | | int updateUseStatus(@Param("gdKey") String gdKey, @Param("useStatus") Integer useStatus); |
| | | } |
| New file |
| | |
| | | package com.stylefeng.guns.modular.system.dao; |
| | | |
| | | import com.baomidou.mybatisplus.mapper.BaseMapper; |
| | | import com.baomidou.mybatisplus.plugins.Page; |
| | | import com.stylefeng.guns.modular.system.model.TGaodeAccount; |
| | | import org.apache.ibatis.annotations.Param; |
| | | |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | |
| | | /** |
| | | * <p> |
| | | * 高德地图账号 Mapper 接口 |
| | | * </p> |
| | | * |
| | | * @author stylefeng |
| | | * @since 2026-09-21 |
| | | */ |
| | | public interface TGaodeAccountMapper extends BaseMapper<TGaodeAccount> { |
| | | |
| | | /** |
| | | * 根据条件查询高德地图账号列表 |
| | | */ |
| | | List<Map<String, Object>> getAccountList(Page<Map<String, Object>> page, |
| | | @Param("gdKey") String gdKey, |
| | | @Param("useStatus") Integer useStatus, |
| | | @Param("status") Integer status); |
| | | } |
| New file |
| | |
| | | <?xml version="1.0" encoding="UTF-8"?> |
| | | <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> |
| | | <mapper namespace="com.stylefeng.guns.modular.system.dao.TGaodeAccountMapper"> |
| | | |
| | | <!-- 通用查询映射结果 --> |
| | | <resultMap id="BaseResultMap" type="com.stylefeng.guns.modular.system.model.TGaodeAccount"> |
| | | <id column="id" property="id" /> |
| | | <result column="gdKey" property="gdKey" /> |
| | | <result column="useStatus" property="useStatus" /> |
| | | <result column="status" property="status" /> |
| | | <result column="delFlag" property="delFlag" /> |
| | | <result column="insertTime" property="insertTime" /> |
| | | <result column="insertUser" property="insertUser" /> |
| | | <result column="updateTime" property="updateTime" /> |
| | | <result column="updateUser" property="updateUser" /> |
| | | </resultMap> |
| | | |
| | | <!-- 通用查询结果列 --> |
| | | <sql id="Base_Column_List"> |
| | | id, gdKey, useStatus, status, delFlag, insertTime, insertUser, updateTime, updateUser |
| | | </sql> |
| | | |
| | | <!--根据条件查询高德地图账号列表--> |
| | | <select id="getAccountList" resultType="map" parameterType="com.baomidou.mybatisplus.plugins.Page"> |
| | | SELECT * FROM t_gaode_account |
| | | <where> |
| | | delFlag = 0 |
| | | <if test="gdKey != null and gdKey != ''"> |
| | | and gdKey LIKE CONCAT('%',#{gdKey},'%') |
| | | </if> |
| | | <if test="useStatus != null and useStatus != ''"> |
| | | and useStatus = #{useStatus} |
| | | </if> |
| | | <if test="status != null and status != ''"> |
| | | and status = #{status} |
| | | </if> |
| | | </where> |
| | | order by id desc |
| | | </select> |
| | | |
| | | </mapper> |
| New file |
| | |
| | | package com.stylefeng.guns.modular.system.model; |
| | | |
| | | import com.baomidou.mybatisplus.enums.IdType; |
| | | import java.util.Date; |
| | | import com.baomidou.mybatisplus.annotations.TableId; |
| | | import com.baomidou.mybatisplus.activerecord.Model; |
| | | import com.baomidou.mybatisplus.annotations.TableName; |
| | | import java.io.Serializable; |
| | | |
| | | /** |
| | | * <p> |
| | | * 高德地图账号 |
| | | * </p> |
| | | * |
| | | * @author stylefeng |
| | | * @since 2026-09-21 |
| | | */ |
| | | @TableName("t_gaode_account") |
| | | public class TGaodeAccount extends Model<TGaodeAccount> { |
| | | |
| | | private static final long serialVersionUID = 1L; |
| | | |
| | | /** |
| | | * 主键id |
| | | */ |
| | | @TableId(value = "id", type = IdType.AUTO) |
| | | private Integer id; |
| | | /** |
| | | * 高德的key |
| | | */ |
| | | private String gdKey; |
| | | /** |
| | | * 使用状态 1=未使用 2=使用中 3=已用完 |
| | | */ |
| | | private Integer useStatus; |
| | | /** |
| | | * 状态 1=启用 2=禁用 |
| | | */ |
| | | private Integer status; |
| | | /** |
| | | * 删除状态 0=未删除 1=已删除 |
| | | */ |
| | | private Integer delFlag; |
| | | /** |
| | | * 添加时间 |
| | | */ |
| | | private Date insertTime; |
| | | /** |
| | | * 添加人 |
| | | */ |
| | | private Integer insertUser; |
| | | /** |
| | | * 修改时间 |
| | | */ |
| | | private Date updateTime; |
| | | /** |
| | | * 修改人 |
| | | */ |
| | | private Integer updateUser; |
| | | |
| | | |
| | | public Integer getId() { |
| | | return id; |
| | | } |
| | | |
| | | public void setId(Integer id) { |
| | | this.id = id; |
| | | } |
| | | |
| | | public String getGdKey() { |
| | | return gdKey; |
| | | } |
| | | |
| | | public void setGdKey(String gdKey) { |
| | | this.gdKey = gdKey; |
| | | } |
| | | |
| | | public Integer getUseStatus() { |
| | | return useStatus; |
| | | } |
| | | |
| | | public void setUseStatus(Integer useStatus) { |
| | | this.useStatus = useStatus; |
| | | } |
| | | |
| | | public Integer getStatus() { |
| | | return status; |
| | | } |
| | | |
| | | public void setStatus(Integer status) { |
| | | this.status = status; |
| | | } |
| | | |
| | | public Integer getDelFlag() { |
| | | return delFlag; |
| | | } |
| | | |
| | | public void setDelFlag(Integer delFlag) { |
| | | this.delFlag = delFlag; |
| | | } |
| | | |
| | | public Date getInsertTime() { |
| | | return insertTime; |
| | | } |
| | | |
| | | public void setInsertTime(Date insertTime) { |
| | | this.insertTime = insertTime; |
| | | } |
| | | |
| | | public Integer getInsertUser() { |
| | | return insertUser; |
| | | } |
| | | |
| | | public void setInsertUser(Integer insertUser) { |
| | | this.insertUser = insertUser; |
| | | } |
| | | |
| | | public Date getUpdateTime() { |
| | | return updateTime; |
| | | } |
| | | |
| | | public void setUpdateTime(Date updateTime) { |
| | | this.updateTime = updateTime; |
| | | } |
| | | |
| | | public Integer getUpdateUser() { |
| | | return updateUser; |
| | | } |
| | | |
| | | public void setUpdateUser(Integer updateUser) { |
| | | this.updateUser = updateUser; |
| | | } |
| | | |
| | | @Override |
| | | protected Serializable pkVal() { |
| | | return this.id; |
| | | } |
| | | |
| | | @Override |
| | | public String toString() { |
| | | return "TGaodeAccount{" + |
| | | "id=" + id + |
| | | ", gdKey=" + gdKey + |
| | | ", useStatus=" + useStatus + |
| | | ", status=" + status + |
| | | ", delFlag=" + delFlag + |
| | | ", insertTime=" + insertTime + |
| | | ", insertUser=" + insertUser + |
| | | ", updateTime=" + updateTime + |
| | | ", updateUser=" + updateUser + |
| | | "}"; |
| | | } |
| | | } |
| New file |
| | |
| | | package com.stylefeng.guns.modular.system.service; |
| | | |
| | | import com.baomidou.mybatisplus.plugins.Page; |
| | | import com.baomidou.mybatisplus.service.IService; |
| | | import com.stylefeng.guns.modular.system.model.TGaodeAccount; |
| | | |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | |
| | | /** |
| | | * <p> |
| | | * 高德地图账号 服务类 |
| | | * </p> |
| | | * |
| | | * @author stylefeng |
| | | * @since 2026-09-21 |
| | | */ |
| | | public interface ITGaodeAccountService extends IService<TGaodeAccount> { |
| | | |
| | | /** |
| | | * 根据条件查询高德地图账号列表 |
| | | */ |
| | | List<Map<String, Object>> getAccountList(Page<Map<String, Object>> page, String gdKey, Integer useStatus, Integer status); |
| | | } |
| New file |
| | |
| | | package com.stylefeng.guns.modular.system.service.impl; |
| | | |
| | | import com.baomidou.mybatisplus.plugins.Page; |
| | | import com.baomidou.mybatisplus.service.impl.ServiceImpl; |
| | | import com.stylefeng.guns.modular.system.dao.TGaodeAccountMapper; |
| | | import com.stylefeng.guns.modular.system.model.TGaodeAccount; |
| | | import com.stylefeng.guns.modular.system.service.ITGaodeAccountService; |
| | | import org.springframework.stereotype.Service; |
| | | |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | |
| | | /** |
| | | * <p> |
| | | * 高德地图账号 服务实现类 |
| | | * </p> |
| | | * |
| | | * @author stylefeng |
| | | * @since 2026-09-21 |
| | | */ |
| | | @Service |
| | | public class TGaodeAccountServiceImpl extends ServiceImpl<TGaodeAccountMapper, TGaodeAccount> implements ITGaodeAccountService { |
| | | |
| | | @Override |
| | | public List<Map<String, Object>> getAccountList(Page<Map<String, Object>> page, String gdKey, Integer useStatus, Integer status) { |
| | | return this.baseMapper.getAccountList(page, gdKey, useStatus, status); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.stylefeng.guns.modular.system.util; |
| | | |
| | | import com.alibaba.fastjson.JSON; |
| | | import com.alibaba.fastjson.JSONObject; |
| | | import com.stylefeng.guns.modular.system.dao.GDKeyMapper; |
| | | import org.slf4j.Logger; |
| | | import org.slf4j.LoggerFactory; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.scheduling.annotation.Scheduled; |
| | | import org.springframework.stereotype.Component; |
| | | import org.springframework.web.client.RestTemplate; |
| | | |
| | | import javax.annotation.PostConstruct; |
| | | import java.util.ArrayList; |
| | | import java.util.List; |
| | | import java.util.Set; |
| | | import java.util.concurrent.ConcurrentHashMap; |
| | | import java.util.concurrent.CopyOnWriteArrayList; |
| | | import java.util.concurrent.atomic.AtomicInteger; |
| | | |
| | | /** |
| | | * 高德地图key管理器 |
| | | * <p> |
| | | * 功能: |
| | | * 1. key池管理:从 t_gaode_account 表加载可用key(启用、未删除、未用完),轮询获取; |
| | | * 表为空或全部用尽时降级使用兜底key(原硬编码key)。 |
| | | * 2. 自动切换:调用方检测到key"次数用完"后调用 reportExhausted 上报, |
| | | * 管理器将key移出内存池并置库表 useStatus=3(已用完)。 |
| | | * 3. 每日凌晨重置校验:每天 00:00 将 useStatus=3 的key重置为未使用并放回池, |
| | | * 随后调用高德最轻量接口校验配额是否真正恢复;未恢复的key重新标记已用完, |
| | | * 进入延迟重试队列,每30分钟校验一次直到恢复额度。 |
| | | * 4. 兜底key:当天用完仅内存禁用,次日凌晨自动恢复;不写库(表内无对应记录)。 |
| | | */ |
| | | @Component |
| | | public class GDKeyManager { |
| | | |
| | | private static final Logger logger = LoggerFactory.getLogger(GDKeyManager.class); |
| | | |
| | | /** |
| | | * 兜底key(原硬编码key):管理端/用户端 |
| | | */ |
| | | public static final String FALLBACK_KEY = "39b31fa22955efa8fce9eed487241db9"; |
| | | |
| | | /** |
| | | * 配额校验接口:最轻量的通用Web服务接口,每次仅消耗1次配额 |
| | | */ |
| | | private static final String VERIFY_URL = "https://restapi.amap.com/v3/geocode/geo?output=JSON&address=%E5%8C%97%E4%BA%AC%E5%B8%82&key="; |
| | | |
| | | @Autowired |
| | | private GDKeyMapper gdKeyMapper; |
| | | |
| | | @Autowired |
| | | private RestTemplate restTemplate; |
| | | |
| | | /** |
| | | * 内存可用key池(refreshPool整体替换,保证getKey不会看到clear/addAll中间态) |
| | | */ |
| | | private volatile List<String> availableKeys = new CopyOnWriteArrayList<>(); |
| | | |
| | | /** |
| | | * 轮询下标 |
| | | */ |
| | | private final AtomicInteger index = new AtomicInteger(0); |
| | | |
| | | /** |
| | | * 延迟校验队列:key -> 已重试次数(每日凌晨校验未恢复的key) |
| | | */ |
| | | private final ConcurrentHashMap<String, Integer> pendingVerifyKeys = new ConcurrentHashMap<>(); |
| | | |
| | | /** |
| | | * 兜底key当日是否已用完 |
| | | */ |
| | | private volatile boolean fallbackExhausted = false; |
| | | |
| | | /** |
| | | * 已标记"使用中"(useStatus=2)的key集合,避免热路径每次取用都写库 |
| | | */ |
| | | private final Set<String> inUseKeys = ConcurrentHashMap.newKeySet(); |
| | | |
| | | @PostConstruct |
| | | public void init() { |
| | | refreshPool(); |
| | | } |
| | | |
| | | /** |
| | | * 刷新可用key池 |
| | | */ |
| | | public synchronized void refreshPool() { |
| | | try { |
| | | List<String> keys = gdKeyMapper.selectAvailableKeys(); |
| | | List<String> newPool = new CopyOnWriteArrayList<>(); |
| | | if (keys != null) { |
| | | newPool.addAll(keys); |
| | | } |
| | | // 整体替换,避免clear/addAll中间态导致getKey短暂看到空池误降级兜底key |
| | | availableKeys = newPool; |
| | | logger.info("[GDKeyManager] 高德key池刷新完成,可用key数:{}", newPool.size()); |
| | | } catch (Exception e) { |
| | | // 表不存在(用户端/司机端未建表时)等异常:降级使用兜底key,不影响系统运行 |
| | | logger.warn("[GDKeyManager] 加载高德key失败(t_gaode_account表可能不存在),降级使用兜底key", e); |
| | | availableKeys = new CopyOnWriteArrayList<>(); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 获取一个可用key:池非空轮询池内key;池空使用兜底key(当日未用完时) |
| | | */ |
| | | public String getKey() { |
| | | List<String> pool = availableKeys; |
| | | if (!pool.isEmpty()) { |
| | | String key = pickFromPool(pool); |
| | | markInUse(key); |
| | | return key; |
| | | } |
| | | if (!fallbackExhausted) { |
| | | return FALLBACK_KEY; |
| | | } |
| | | // 兜底key当日也已用完:尝试刷新池(可能有新录入的key) |
| | | refreshPool(); |
| | | pool = availableKeys; |
| | | if (!pool.isEmpty()) { |
| | | String key = pickFromPool(pool); |
| | | markInUse(key); |
| | | return key; |
| | | } |
| | | return FALLBACK_KEY; |
| | | } |
| | | |
| | | /** |
| | | * 从内存池轮询取一个key(使用调用方传入的池快照,避免与定时刷新交叉导致越界) |
| | | */ |
| | | private String pickFromPool(List<String> pool) { |
| | | int i = Math.abs(index.getAndIncrement() % pool.size()); |
| | | return pool.get(i); |
| | | } |
| | | |
| | | /** |
| | | * 将表内key标记为"使用中"(useStatus=2): |
| | | * 仅key首次被取用时写库一次;兜底key不写库;写库失败不影响取key流程。 |
| | | */ |
| | | private void markInUse(String key) { |
| | | if (key == null || FALLBACK_KEY.equals(key) || inUseKeys.contains(key)) { |
| | | return; |
| | | } |
| | | try { |
| | | gdKeyMapper.updateUseStatus(key, 2); |
| | | inUseKeys.add(key); |
| | | logger.info("[GDKeyManager] 高德key[{}]开始使用,已标记为使用中", key); |
| | | } catch (Exception e) { |
| | | // 表不存在等异常:本次不标记,下次取用再尝试,不影响业务 |
| | | logger.warn("[GDKeyManager] 标记高德key[{}]使用中失败(表可能不存在),不影响使用", key); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 定时刷新key池:每5分钟一次,让管理后台新录入的key及时生效 |
| | | */ |
| | | @Scheduled(fixedDelay = 5 * 60 * 1000, initialDelay = 60 * 1000) |
| | | public void scheduledRefreshPool() { |
| | | refreshPool(); |
| | | } |
| | | |
| | | /** |
| | | * 上报某个key次数用完: |
| | | * 表内key -> 移出内存池 + 置库表 useStatus=3; |
| | | * 兜底key -> 仅内存当日禁用(不写库),次日凌晨自动恢复。 |
| | | */ |
| | | public void reportExhausted(String usedKey) { |
| | | if (usedKey == null || usedKey.isEmpty()) { |
| | | return; |
| | | } |
| | | availableKeys.remove(usedKey); |
| | | inUseKeys.remove(usedKey); |
| | | if (FALLBACK_KEY.equals(usedKey)) { |
| | | fallbackExhausted = true; |
| | | logger.warn("[GDKeyManager] 兜底key次数已用完,今日禁用,次日凌晨自动恢复"); |
| | | return; |
| | | } |
| | | try { |
| | | gdKeyMapper.updateUseStatus(usedKey, 3); |
| | | logger.warn("[GDKeyManager] 高德key[{}]次数用完,已标记为已用完并移出key池", usedKey); |
| | | } catch (Exception e) { |
| | | logger.warn("[GDKeyManager] 更新高德key[{}]使用状态失败(表可能不存在),仅内存剔除", usedKey); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 判断Web服务API响应是否为"次数用完/不可用"(依据infocode) |
| | | */ |
| | | public static boolean isWebKeyExhausted(JSONObject resp) { |
| | | if (resp == null) { |
| | | return false; |
| | | } |
| | | String infocode = resp.getString("infocode"); |
| | | if (infocode == null || infocode.isEmpty()) { |
| | | return false; |
| | | } |
| | | switch (infocode) { |
| | | case "10001": // key不正确或过期 |
| | | case "10003": // 超过每日配额 |
| | | case "10004": // 访问过于频繁 |
| | | case "10005": // 超出每秒访问量限制 |
| | | case "10010": // 权限不足 |
| | | case "10012": // 服务未开放/权限不足 |
| | | case "10019": // 使用配额已用尽 |
| | | case "10020": // 并发超限 |
| | | case "10044": // 签名错误/未开通 |
| | | return true; |
| | | default: |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 判断猎鹰API响应是否为"次数用完"(依据errcode/errmsg) |
| | | */ |
| | | public static boolean isFalconExhausted(JSONObject resp) { |
| | | if (resp == null) { |
| | | return false; |
| | | } |
| | | int errcode = resp.getIntValue("errcode"); |
| | | if (errcode == 10000) { |
| | | return false; |
| | | } |
| | | if (errcode == 20001) { |
| | | return true; // 权限不足 |
| | | } |
| | | String errmsg = resp.getString("errmsg"); |
| | | if (errmsg == null) { |
| | | return false; |
| | | } |
| | | return errmsg.contains("配额") || errmsg.contains("上限") || errmsg.contains("次数") |
| | | || errmsg.contains("用完") || errmsg.contains("超限") |
| | | || errmsg.contains("quota") || errmsg.contains("limit"); |
| | | } |
| | | |
| | | // ==================== 每日凌晨重置 + API校验恢复额度 ==================== |
| | | |
| | | /** |
| | | * 每日凌晨 00:00 自动执行: |
| | | * 1. 将 useStatus=3(已用完)的key重置为未使用并放回内存池; |
| | | * 2. 调用高德API校验配额是否真正恢复; |
| | | * 3. 未恢复的key重新标记已用完,进入延迟重试队列,直到恢复额度。 |
| | | */ |
| | | @Scheduled(cron = "0 0 0 * * ?") |
| | | public void dailyResetAndVerify() { |
| | | logger.info("[GDKeyManager] 开始每日高德key配额重置校验"); |
| | | // 兜底key当日禁用标记次日自动恢复 |
| | | fallbackExhausted = false; |
| | | List<String> exhaustedKeys; |
| | | try { |
| | | exhaustedKeys = gdKeyMapper.selectExhaustedKeys(); |
| | | } catch (Exception e) { |
| | | logger.warn("[GDKeyManager] 查询已用完key失败(表可能不存在),跳过重置校验", e); |
| | | return; |
| | | } |
| | | if (exhaustedKeys == null || exhaustedKeys.isEmpty()) { |
| | | logger.info("[GDKeyManager] 无已用完的key,跳过重置校验"); |
| | | return; |
| | | } |
| | | for (String gdKey : exhaustedKeys) { |
| | | // 1. 先重置为未使用 |
| | | try { |
| | | gdKeyMapper.updateUseStatus(gdKey, 1); |
| | | } catch (Exception e) { |
| | | logger.warn("[GDKeyManager] 重置key[{}]使用状态失败", gdKey, e); |
| | | } |
| | | // 2. 调用API校验是否真正恢复额度 |
| | | boolean recovered = verifyQuota(gdKey); |
| | | if (recovered) { |
| | | if (!availableKeys.contains(gdKey)) { |
| | | availableKeys.add(gdKey); |
| | | } |
| | | pendingVerifyKeys.remove(gdKey); |
| | | logger.info("[GDKeyManager] 高德key[{}]配额已恢复,重新加入key池", gdKey); |
| | | } else { |
| | | // 未恢复:重新标记已用完,进入延迟重试队列 |
| | | try { |
| | | gdKeyMapper.updateUseStatus(gdKey, 3); |
| | | } catch (Exception e) { |
| | | logger.warn("[GDKeyManager] 回写key[{}]已用完状态失败", gdKey, e); |
| | | } |
| | | availableKeys.remove(gdKey); |
| | | pendingVerifyKeys.put(gdKey, 0); |
| | | logger.warn("[GDKeyManager] 高德key[{}]配额尚未恢复,进入延迟重试队列", gdKey); |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 延迟重试任务:每30分钟校验一次待恢复的key,直到恢复额度为止 |
| | | */ |
| | | @Scheduled(fixedDelay = 30 * 60 * 1000, initialDelay = 30 * 60 * 1000) |
| | | public void retryPendingKeys() { |
| | | if (pendingVerifyKeys.isEmpty()) { |
| | | return; |
| | | } |
| | | for (String gdKey : new ArrayList<>(pendingVerifyKeys.keySet())) { |
| | | boolean recovered = verifyQuota(gdKey); |
| | | if (recovered) { |
| | | try { |
| | | gdKeyMapper.updateUseStatus(gdKey, 1); |
| | | } catch (Exception e) { |
| | | logger.warn("[GDKeyManager] 恢复key[{}]使用状态失败", gdKey, e); |
| | | } |
| | | if (!availableKeys.contains(gdKey)) { |
| | | availableKeys.add(gdKey); |
| | | } |
| | | pendingVerifyKeys.remove(gdKey); |
| | | logger.info("[GDKeyManager] 高德key[{}]延迟校验通过,配额已恢复,重新加入key池", gdKey); |
| | | } else { |
| | | int count = pendingVerifyKeys.merge(gdKey, 1, Integer::sum); |
| | | logger.warn("[GDKeyManager] 高德key[{}]配额仍未恢复(已校验{}次),继续等待", gdKey, count); |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 调用高德最轻量接口校验配额是否恢复 |
| | | * |
| | | * @return true=已恢复(或无法判断,保守放行);false=明确未恢复(配额类错误) |
| | | */ |
| | | private boolean verifyQuota(String gdKey) { |
| | | try { |
| | | String resp = restTemplate.getForObject(VERIFY_URL + gdKey, String.class); |
| | | JSONObject json = JSON.parseObject(resp); |
| | | String status = json.getString("status"); |
| | | String infocode = json.getString("infocode"); |
| | | if ("1".equals(status) && "10000".equals(infocode)) { |
| | | return true; |
| | | } |
| | | // 权限类错误:该key未开通Web服务校验权限(如猎鹰专用key),无法判断配额,按已恢复处理 |
| | | if ("10044".equals(infocode) || "10012".equals(infocode) |
| | | || "10002".equals(infocode) || "10010".equals(infocode)) { |
| | | logger.info("[GDKeyManager] 高德key[{}]无Web服务校验权限(infocode={}),按已恢复处理", gdKey, infocode); |
| | | return true; |
| | | } |
| | | // 配额类错误:明确未恢复 |
| | | if ("10001".equals(infocode) || "10003".equals(infocode) || "10019".equals(infocode)) { |
| | | return false; |
| | | } |
| | | // 其他错误(限流/网络等):保守放行,真实调用失败时会被自动切换机制再次标记 |
| | | return true; |
| | | } catch (Exception e) { |
| | | logger.warn("[GDKeyManager] 校验key[{}]配额异常,按已恢复处理", gdKey, e); |
| | | return true; |
| | | } |
| | | } |
| | | } |
| | |
| | | @Autowired |
| | | private IGDInterfaceService gdInterfaceService; |
| | | |
| | | private String key = "ca723c3c8a78962a7751129ae035dc9d"; |
| | | @Autowired |
| | | private GDKeyManager gdKeyManager; |
| | | |
| | | private JSONArray jsonArray = new JSONArray(); |
| | | |
| | | /** |
| | | * 执行GET请求:key次数用完时自动切换可用key重试 |
| | | */ |
| | | private String doGetWithRetry(String baseUrl) { |
| | | String lastResp = null; |
| | | int maxAttempts = 5; |
| | | for (int i = 0; i < maxAttempts; i++) { |
| | | String key = gdKeyManager.getKey(); |
| | | String url = baseUrl + (baseUrl.contains("?") ? "&" : "?") + "key=" + key; |
| | | String resp = restTemplate.getForObject(url, String.class); |
| | | lastResp = resp; |
| | | JSONObject jsonObject; |
| | | try { |
| | | jsonObject = JSON.parseObject(resp); |
| | | } catch (Exception e) { |
| | | return resp; |
| | | } |
| | | if (GDKeyManager.isWebKeyExhausted(jsonObject)) { |
| | | gdKeyManager.reportExhausted(key); |
| | | continue; |
| | | } |
| | | return resp; |
| | | } |
| | | return lastResp; |
| | | } |
| | | |
| | | /** |
| | | * 执行POST请求(JSON体):key次数用完时自动切换可用key重试 |
| | | */ |
| | | private String doPostJsonWithRetry(String baseUrl, String jsonBody) { |
| | | String lastResp = null; |
| | | int maxAttempts = 5; |
| | | HttpHeaders headers = new HttpHeaders(); |
| | | MediaType type = MediaType.parseMediaType("application/json; charset=UTF-8"); |
| | | headers.setContentType(type); |
| | | headers.add("Accept", MediaType.APPLICATION_JSON.toString()); |
| | | for (int i = 0; i < maxAttempts; i++) { |
| | | String key = gdKeyManager.getKey(); |
| | | String url = baseUrl + (baseUrl.contains("?") ? "&" : "?") + "key=" + key; |
| | | HttpEntity<String> formEntity = new HttpEntity<>(jsonBody, headers); |
| | | String resp = restTemplate.postForObject(url, formEntity, String.class); |
| | | lastResp = resp; |
| | | JSONObject jsonObject; |
| | | try { |
| | | jsonObject = JSON.parseObject(resp); |
| | | } catch (Exception e) { |
| | | return resp; |
| | | } |
| | | if (GDKeyManager.isWebKeyExhausted(jsonObject)) { |
| | | gdKeyManager.reportExhausted(key); |
| | | continue; |
| | | } |
| | | return resp; |
| | | } |
| | | return lastResp; |
| | | } |
| | | |
| | | /** |
| | | * 执行PATCH请求(JSON体):key次数用完时自动切换可用key重试 |
| | | */ |
| | | private String doPatchJsonWithRetry(String baseUrl, String jsonBody) { |
| | | String lastResp = null; |
| | | int maxAttempts = 5; |
| | | HttpHeaders headers = new HttpHeaders(); |
| | | MediaType type = MediaType.parseMediaType("application/json; charset=UTF-8"); |
| | | headers.setContentType(type); |
| | | headers.add("Accept", MediaType.APPLICATION_JSON.toString()); |
| | | for (int i = 0; i < maxAttempts; i++) { |
| | | String key = gdKeyManager.getKey(); |
| | | String url = baseUrl + (baseUrl.contains("?") ? "&" : "?") + "key=" + key; |
| | | HttpEntity<String> formEntity = new HttpEntity<>(jsonBody, headers); |
| | | String resp = restTemplate.patchForObject(url, formEntity, String.class); |
| | | lastResp = resp; |
| | | JSONObject jsonObject; |
| | | try { |
| | | jsonObject = JSON.parseObject(resp); |
| | | } catch (Exception e) { |
| | | return resp; |
| | | } |
| | | if (GDKeyManager.isWebKeyExhausted(jsonObject)) { |
| | | gdKeyManager.reportExhausted(key); |
| | | continue; |
| | | } |
| | | return resp; |
| | | } |
| | | return lastResp; |
| | | } |
| | | |
| | | |
| | | @Bean |
| | |
| | | json.put("alert_condition", "enter;leave"); |
| | | json.put("desc", id); |
| | | |
| | | HttpHeaders headers = new HttpHeaders(); |
| | | MediaType type = MediaType.parseMediaType("application/json; charset=UTF-8"); |
| | | headers.setContentType(type); |
| | | headers.add("Accept", MediaType.APPLICATION_JSON.toString()); |
| | | HttpEntity<String> formEntity = new HttpEntity<>(json.toString(), headers); |
| | | |
| | | String url = "https://restapi.amap.com/v4/geofence/meta?key=" + key; |
| | | String js = restTemplate.postForObject(url, formEntity, String.class); |
| | | String js = doPostJsonWithRetry("https://restapi.amap.com/v4/geofence/meta", json.toString()); |
| | | JSONObject jsonObject = JSON.parseObject(js); |
| | | JSONObject data = jsonObject.getJSONObject("data"); |
| | | String status = data.getString("status"); |
| | |
| | | * @return |
| | | */ |
| | | public String findElectricFenc(String gid){ |
| | | String url = "https://restapi.amap.com/v4/geofence/meta?key=" + key + "&gid=" + gid; |
| | | String forObject = restTemplate.getForObject(url, String.class); |
| | | String url = "https://restapi.amap.com/v4/geofence/meta?gid=" + gid; |
| | | String forObject = doGetWithRetry(url); |
| | | JSONObject jsonObject = JSON.parseObject(forObject); |
| | | JSONObject data = jsonObject.getJSONObject("data"); |
| | | JSONArray rs_list = data.getJSONArray("rs_list"); |
| | |
| | | * 查询以创建的所有电子围栏 |
| | | */ |
| | | public String findElectricFenc(String center, String radius, String points){ |
| | | String url = "https://restapi.amap.com/v4/geofence/meta?key=" + key; |
| | | String forObject = restTemplate.getForObject(url, String.class); |
| | | String url = "https://restapi.amap.com/v4/geofence/meta"; |
| | | String forObject = doGetWithRetry(url); |
| | | JSONObject jsonObject = JSON.parseObject(forObject); |
| | | JSONObject data = jsonObject.getJSONObject("data"); |
| | | |
| | |
| | | if(page == 1){ |
| | | jsonArray = new JSONArray(); |
| | | } |
| | | String url = "https://restapi.amap.com/v4/geofence/meta?key=" + key + "&page_no=" + page + "&page_size=" + 30; |
| | | String forObject = restTemplate.getForObject(url, String.class); |
| | | String url = "https://restapi.amap.com/v4/geofence/meta?page_no=" + page + "&page_size=" + 30; |
| | | String forObject = doGetWithRetry(url); |
| | | JSONObject jsonObject = JSON.parseObject(forObject); |
| | | JSONObject data = jsonObject.getJSONObject("data"); |
| | | JSONArray rs_list = data.getJSONArray("rs_list"); |
| | |
| | | json.put("alert_condition", "enter;leave"); |
| | | json.put("desc", id + "围栏更新描述"); |
| | | |
| | | HttpHeaders headers = new HttpHeaders(); |
| | | MediaType type = MediaType.parseMediaType("application/json; charset=UTF-8"); |
| | | headers.setContentType(type); |
| | | headers.add("Accept", MediaType.APPLICATION_JSON.toString()); |
| | | HttpEntity<String> formEntity = new HttpEntity<>(json.toString(), headers); |
| | | |
| | | String url = "https://restapi.amap.com/v4/geofence/meta?key=" + key + "&gid=" + gid; |
| | | String s = restTemplate.patchForObject(url, formEntity, String.class); |
| | | String url = "https://restapi.amap.com/v4/geofence/meta?gid=" + gid; |
| | | String s = doPatchJsonWithRetry(url, json.toString()); |
| | | System.out.println(s); |
| | | |
| | | gdInterfaceService.saveData("https://restapi.amap.com/v4/geofence/meta", "更新电子围栏"); |
| | |
| | | diu = "B78F99DD-2496-4A43-843B-172CDA2D9798"; |
| | | } |
| | | locations = locations + "," + (new Date().getTime() / 1000); |
| | | String url = "https://restapi.amap.com/v4/geofence/status?key=" + key + "&diu=" + diu + "&locations=" + locations; |
| | | String forObject = restTemplate.getForObject(url, String.class); |
| | | String url = "https://restapi.amap.com/v4/geofence/status?diu=" + diu + "&locations=" + locations; |
| | | String forObject = doGetWithRetry(url); |
| | | JSONObject jsonObject = JSON.parseObject(forObject); |
| | | List<String> ids = new ArrayList<>(); |
| | | JSONObject data = jsonObject.getJSONObject("data"); |
| | |
| | | * @return |
| | | */ |
| | | public boolean deleteElectricFenc(String gid){ |
| | | restTemplate.delete("https://restapi.amap.com/v4/geofence/meta?key={1}&gid={2}", key, gid); |
| | | restTemplate.delete("https://restapi.amap.com/v4/geofence/meta?key={1}&gid={2}", gdKeyManager.getKey(), gid); |
| | | System.out.println("删除:" ); |
| | | |
| | | gdInterfaceService.saveData("https://restapi.amap.com/v4/geofence/meta", "删除电子围栏"); |
| | |
| | | * @return |
| | | */ |
| | | public Map<String, String> getDistance(String origins, String destination, Integer type){ |
| | | String url = "https://restapi.amap.com/v3/distance?key=" + key + "&origins=" + origins + "&destination=" + destination + |
| | | String url = "https://restapi.amap.com/v3/distance?origins=" + origins + "&destination=" + destination + |
| | | "&type=" + type; |
| | | String forObject = restTemplate.getForObject(url, String.class); |
| | | String forObject = doGetWithRetry(url); |
| | | JSONObject jsonObject = JSON.parseObject(forObject); |
| | | String status = jsonObject.getString("status"); |
| | | |
| | |
| | | * @return |
| | | */ |
| | | public Map<String, String> getDriving(String origins, String destination, String waypoints){ |
| | | String url = "https://restapi.amap.com/v3/direction/driving?key=" + key + "&origin=" + origins + "&destination=" + destination + |
| | | String url = "https://restapi.amap.com/v3/direction/driving?origin=" + origins + "&destination=" + destination + |
| | | "&waypoints=" + waypoints + "&extensions=base&strategy=0"; |
| | | String forObject = restTemplate.getForObject(url, String.class); |
| | | String forObject = doGetWithRetry(url); |
| | | JSONObject jsonObject = JSON.parseObject(forObject); |
| | | String status = jsonObject.getString("status"); |
| | | |
| | |
| | | @Component |
| | | public class GDMapGeocodingUtil { |
| | | |
| | | private String key = "ca723c3c8a78962a7751129ae035dc9d"; |
| | | |
| | | @Autowired |
| | | private RestTemplate restTemplate; |
| | | |
| | | @Autowired |
| | | private IGDInterfaceService gdInterfaceService; |
| | | |
| | | @Autowired |
| | | private GDKeyManager gdKeyManager; |
| | | |
| | | /** |
| | | * 执行GET请求:key次数用完时自动切换可用key重试 |
| | | */ |
| | | private String doGetWithRetry(String baseUrl) { |
| | | String lastResp = null; |
| | | int maxAttempts = 5; |
| | | for (int i = 0; i < maxAttempts; i++) { |
| | | String key = gdKeyManager.getKey(); |
| | | String url = baseUrl + (baseUrl.contains("?") ? "&" : "?") + "key=" + key; |
| | | String resp = restTemplate.getForObject(url, String.class); |
| | | lastResp = resp; |
| | | JSONObject jsonObject; |
| | | try { |
| | | jsonObject = JSON.parseObject(resp); |
| | | } catch (Exception e) { |
| | | return resp; |
| | | } |
| | | if (GDKeyManager.isWebKeyExhausted(jsonObject)) { |
| | | gdKeyManager.reportExhausted(key); |
| | | continue; |
| | | } |
| | | return resp; |
| | | } |
| | | return lastResp; |
| | | } |
| | | |
| | | |
| | | /** |
| | |
| | | return map; |
| | | } |
| | | |
| | | String url = "https://restapi.amap.com/v3/geocode/geo?key=" + key + "&output=JSON"; |
| | | String url = "https://restapi.amap.com/v3/geocode/geo?output=JSON"; |
| | | url += "&address=" + province + (ToolUtil.isNotEmpty(city) ? city : "") + (ToolUtil.isNotEmpty(county) ? county : "") + (ToolUtil.isNotEmpty(address) ? address : ""); |
| | | String forObject = restTemplate.getForObject(url, String.class); |
| | | String forObject = doGetWithRetry(url); |
| | | JSONObject jsonObject = JSON.parseObject(forObject); |
| | | String status = jsonObject.getString("status"); |
| | | List<String> list = new ArrayList<>(); |
| | |
| | | |
| | | public Map<String, Object> geocoding(String address){ |
| | | Map<String, Object> map = new HashMap<>(); |
| | | String url = "https://restapi.amap.com/v3/geocode/geo?key=" + key + "&output=JSON&address=" + address; |
| | | String forObject = restTemplate.getForObject(url, String.class); |
| | | String url = "https://restapi.amap.com/v3/geocode/geo?output=JSON&address=" + address; |
| | | String forObject = doGetWithRetry(url); |
| | | JSONObject jsonObject = JSON.parseObject(forObject); |
| | | String status = jsonObject.getString("status"); |
| | | List<String> list = new ArrayList<>(); |
| | |
| | | * @throws Exception |
| | | */ |
| | | public Map<String, String> geocode(String lon, String lan) throws Exception{ |
| | | String url = "https://restapi.amap.com/v3/geocode/regeo?key=" + key + "&location=" + lon + "," + lan; |
| | | String forObject = restTemplate.getForObject(url, String.class); |
| | | String url = "https://restapi.amap.com/v3/geocode/regeo?location=" + lon + "," + lan; |
| | | String forObject = doGetWithRetry(url); |
| | | JSONObject jsonObject = JSON.parseObject(forObject); |
| | | Map<String, String> map = new HashMap<>(); |
| | | |
| | |
| | | package com.stylefeng.guns.modular.system.util; |
| | | import com.alibaba.fastjson.JSON; |
| | | import com.alibaba.fastjson.JSONArray; |
| | | import com.alibaba.fastjson.JSONObject; |
| | | import com.stylefeng.guns.core.util.SpringContextHolder; |
| | | import org.apache.http.HttpEntity; |
| | | import org.apache.http.client.config.RequestConfig; |
| | | import org.apache.http.client.methods.CloseableHttpResponse; |
| | |
| | | } |
| | | |
| | | /** |
| | | * 获取GDKeyManager(非Spring环境下返回null,如main测试) |
| | | */ |
| | | private static GDKeyManager getKeyManager() { |
| | | try { |
| | | return SpringContextHolder.getBean(GDKeyManager.class); |
| | | } catch (Exception e) { |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 根据地址查询经纬度 |
| | | * key次数用完时自动切换可用key重试 |
| | | * |
| | | * @param address |
| | | * @return |
| | | */ |
| | | public static JSONObject getLngAndLat(String address) { |
| | | JSONObject positionObj = new JSONObject(); |
| | | GDKeyManager keyManager = getKeyManager(); |
| | | |
| | | try { |
| | | // 拼接请求高德的url |
| | | String url = "http://restapi.amap.com/v3/geocode/geo?address=" + address + "&output=JSON&key=" + "ca723c3c8a78962a7751129ae035dc9d"; |
| | | // 请求高德接口 |
| | | String result = sendHttpGet(url); |
| | | JSONObject resultJOSN = JSONObject.parseObject(result); |
| | | System.out.println("高德接口返回原始数据:"); |
| | | System.out.println(resultJOSN); |
| | | JSONArray geocodesArray = resultJOSN.getJSONArray("geocodes"); |
| | | if (geocodesArray.size() > 0) { |
| | | String position = geocodesArray.getJSONObject(0).getString("location"); |
| | | String[] lngAndLat = position.split(","); |
| | | String longitude = lngAndLat[0]; |
| | | String latitude = lngAndLat[1]; |
| | | positionObj.put("longitude", longitude); |
| | | positionObj.put("latitude", latitude); |
| | | int maxAttempts = 5; |
| | | for (int i = 0; i < maxAttempts; i++) { |
| | | String key = keyManager != null ? keyManager.getKey() : GDKeyManager.FALLBACK_KEY; |
| | | try { |
| | | // 拼接请求高德的url |
| | | String url = "http://restapi.amap.com/v3/geocode/geo?address=" + address + "&output=JSON&key=" + key; |
| | | // 请求高德接口 |
| | | String result = sendHttpGet(url); |
| | | JSONObject resultJOSN = JSON.parseObject(result); |
| | | System.out.println("高德接口返回原始数据:"); |
| | | System.out.println(resultJOSN); |
| | | if (GDKeyManager.isWebKeyExhausted(resultJOSN)) { |
| | | if (keyManager != null) { |
| | | keyManager.reportExhausted(key); |
| | | } |
| | | continue; |
| | | } |
| | | JSONArray geocodesArray = resultJOSN.getJSONArray("geocodes"); |
| | | if (geocodesArray != null && geocodesArray.size() > 0) { |
| | | String position = geocodesArray.getJSONObject(0).getString("location"); |
| | | String[] lngAndLat = position.split(","); |
| | | String longitude = lngAndLat[0]; |
| | | String latitude = lngAndLat[1]; |
| | | positionObj.put("longitude", longitude); |
| | | positionObj.put("latitude", latitude); |
| | | } |
| | | break; |
| | | } catch (Exception e) { |
| | | e.printStackTrace(); |
| | | } |
| | | geocodesArray.getJSONObject(0).getString("location"); |
| | | |
| | | } catch (Exception e) { |
| | | e.printStackTrace(); |
| | | } |
| | | return positionObj; |
| | | } |
| | |
| | | |
| | | #spring: |
| | | # datasource: |
| | | # url: jdbc:mysql://127.0.0.1:3306/fbtravel?autoReconnect=true&useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=CONVERT_TO_NULL&useSSL=false&serverTimezone=GMT%2B8 |
| | | # url: jdbc:mysql://192.168.110.80:3306/fbtravel?autoReconnect=true&useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=CONVERT_TO_NULL&useSSL=false&serverTimezone=GMT%2B8 |
| | | # username: root |
| | | # password: 123456 |
| | | # db-name: oktravel #用来搜集数据库的所有表 |
| | |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <script type="text/javascript" src="https://webapi.amap.com/maps?v=1.4.15&key=c59c0ec8058a8305009eb315584e07d1"></script> |
| | | <script src="https://webapi.amap.com/maps?v=1.4.15&key=c59c0ec8058a8305009eb315584e07d1&plugin=AMap.MouseTool"></script> |
| | | <script type="text/javascript"> |
| | | window._AMapSecurityConfig = { |
| | | securityJsCode: "f4241876b072db561dc69b2857032738", |
| | | }; |
| | | </script> |
| | | <script type="text/javascript" src="https://webapi.amap.com/maps?v=1.4.15&key=fb70a54f977723c9cac44bc903936746"></script> |
| | | <script src="https://webapi.amap.com/maps?v=1.4.15&key=fb70a54f977723c9cac44bc903936746&plugin=AMap.MouseTool"></script> |
| | | <script type="text/javascript" src='https://a.amap.com/jsapi_demos/static/citys.js'></script> |
| | | <script src="${ctxPath}/static/home.js"></script> |
| | | <script src="${ctxPath}/static/js/jquery.sparkline 2.1.2.js"></script> |
| New file |
| | |
| | | @layout("/common/_container.html"){ |
| | | <div class="row"> |
| | | <div class="col-sm-12"> |
| | | <div class="ibox float-e-margins"> |
| | | <div class="ibox-title"> |
| | | <h5>高德地图账号管理</h5> |
| | | </div> |
| | | <div class="ibox-content"> |
| | | <div class="row row-lg"> |
| | | <div class="col-sm-12"> |
| | | <div class="row"> |
| | | <div class="col-sm-3"> |
| | | <#NameCon id="gdKey" name="高德key" /> |
| | | </div> |
| | | <div class="col-sm-3"> |
| | | <#SelectCon id="useStatus" name="使用状态" > |
| | | <option value="">全部</option> |
| | | <option value="1">未使用</option> |
| | | <option value="2">使用中</option> |
| | | <option value="3">已用完</option> |
| | | </#SelectCon> |
| | | </div> |
| | | <div class="col-sm-3"> |
| | | <#SelectCon id="status" name="状态" > |
| | | <option value="">全部</option> |
| | | <option value="1">启用</option> |
| | | <option value="2">禁用</option> |
| | | </#SelectCon> |
| | | </div> |
| | | <div class="col-sm-3"> |
| | | <#button name="搜索" icon="fa-search" clickFun="TGaodeAccount.search()"/> |
| | | <#button name="重置" icon="fa-trash" clickFun="TGaodeAccount.resetSearch()" space="true"/> |
| | | </div> |
| | | </div> |
| | | <div class="hidden-xs" id="TGaodeAccountTableToolbar" role="group"> |
| | | @if(shiro.hasPermission("/tGaodeAccount/add")){ |
| | | <#button name="添加" icon="fa-plus" clickFun="TGaodeAccount.openAddTGaodeAccount()"/> |
| | | @} |
| | | @if(shiro.hasPermission("/tGaodeAccount/update")){ |
| | | <#button name="编辑" icon="fa-edit" clickFun="TGaodeAccount.openTGaodeAccountDetail()" space="true"/> |
| | | @} |
| | | @if(shiro.hasPermission("/tGaodeAccount/delete")){ |
| | | <#button name="删除" icon="fa-remove" clickFun="TGaodeAccount.delete()" space="true"/> |
| | | @} |
| | | @if(shiro.hasPermission("/tGaodeAccount/opt")){ |
| | | <#button name="启用" icon="fa-check-circle" clickFun="TGaodeAccount.enable()" space="true"/> |
| | | <#button name="禁用" icon="fa-ban" clickFun="TGaodeAccount.disable()" space="true"/> |
| | | @} |
| | | </div> |
| | | <#table id="TGaodeAccountTable"/> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <script src="${ctxPath}/static/modular/system/tGaodeAccount/tGaodeAccount.js"></script> |
| | | @} |
| New file |
| | |
| | | @layout("/common/_container.html"){ |
| | | <div class="ibox float-e-margins"> |
| | | <div class="ibox-content"> |
| | | <div class="form-horizontal" id="gaodeAccountInfoForm"> |
| | | |
| | | <div class="row"> |
| | | <div class="col-sm-10"> |
| | | <#input id="gdKey" name="高德key"/> |
| | | <#select id="useStatus" name="使用状态:"> |
| | | <option value="1" selected="selected">未使用</option> |
| | | <option value="2">使用中</option> |
| | | <option value="3">已用完</option> |
| | | </#select> |
| | | <#select id="status" name="状态:"> |
| | | <option value="1" selected="selected">启用</option> |
| | | <option value="2">禁用</option> |
| | | </#select> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="row btn-group-m-t"> |
| | | <div class="col-sm-10 col-sm-offset-5"> |
| | | <#button btnCss="info" name="提交" id="ensure" icon="fa-check" clickFun="TGaodeAccountInfoDlg.addSubmit()"/> |
| | | <#button btnCss="danger" name="取消" id="cancel" icon="fa-eraser" clickFun="TGaodeAccountInfoDlg.close()"/> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | </div> |
| | | </div> |
| | | <script src="${ctxPath}/static/modular/system/tGaodeAccount/tGaodeAccount_info.js"></script> |
| | | @} |
| New file |
| | |
| | | @layout("/common/_container.html"){ |
| | | <div class="ibox float-e-margins"> |
| | | <div class="ibox-content"> |
| | | <div class="form-horizontal" id="gaodeAccountInfoForm"> |
| | | <input type="hidden" id="id" name="id" value="${item.id}"> |
| | | <div class="row"> |
| | | <div class="col-sm-10"> |
| | | <#input id="gdKey" name="高德key" value="${item.gdKey}"/> |
| | | <#select id="useStatus" name="使用状态:"> |
| | | <option value="1" ${1 == item.useStatus ? 'selected=selected' : ''}>未使用</option> |
| | | <option value="2" ${2 == item.useStatus ? 'selected=selected' : ''}>使用中</option> |
| | | <option value="3" ${3 == item.useStatus ? 'selected=selected' : ''}>已用完</option> |
| | | </#select> |
| | | <#select id="status" name="状态:"> |
| | | <option value="1" ${1 == item.status ? 'selected=selected' : ''}>启用</option> |
| | | <option value="2" ${2 == item.status ? 'selected=selected' : ''}>禁用</option> |
| | | </#select> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="row btn-group-m-t"> |
| | | <div class="col-sm-10 col-sm-offset-5"> |
| | | <#button btnCss="info" name="提交" id="ensure" icon="fa-check" clickFun="TGaodeAccountInfoDlg.editSubmit()"/> |
| | | <#button btnCss="danger" name="取消" id="cancel" icon="fa-eraser" clickFun="TGaodeAccountInfoDlg.close()"/> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | </div> |
| | | </div> |
| | | <script src="${ctxPath}/static/modular/system/tGaodeAccount/tGaodeAccount_info.js"></script> |
| | | @} |
| | |
| | | |
| | | </div> |
| | | </div> |
| | | <script src="https://webapi.amap.com/maps?v=1.4.15&key=c59c0ec8058a8305009eb315584e07d1&plugin=AMap.MouseTool,AMap.DistrictSearch,AMap.PolyEditor,AMap.RectangleEditor,AMap.CircleEditor"></script> |
| | | <script type="text/javascript"> |
| | | window._AMapSecurityConfig = { |
| | | securityJsCode: "f4241876b072db561dc69b2857032738", |
| | | }; |
| | | </script> |
| | | <script src="https://webapi.amap.com/maps?v=1.4.15&key=fb70a54f977723c9cac44bc903936746&plugin=AMap.MouseTool,AMap.DistrictSearch,AMap.PolyEditor,AMap.RectangleEditor,AMap.CircleEditor"></script> |
| | | <script src="${ctxPath}/static/modular/system/tSite/electricFence.js"></script> |
| | | <script src="${ctxPath}/static/modular/system/tSite/tSite_info.js"></script> |
| | | @} |
| | |
| | | |
| | | </div> |
| | | </div> |
| | | <script src="https://webapi.amap.com/maps?v=1.4.15&key=c59c0ec8058a8305009eb315584e07d1&plugin=AMap.MouseTool,AMap.DistrictSearch,AMap.PolyEditor,AMap.RectangleEditor,AMap.CircleEditor"></script> |
| | | <script type="text/javascript"> |
| | | window._AMapSecurityConfig = { |
| | | securityJsCode: "f4241876b072db561dc69b2857032738", |
| | | }; |
| | | </script> |
| | | <script src="https://webapi.amap.com/maps?v=1.4.15&key=fb70a54f977723c9cac44bc903936746&plugin=AMap.MouseTool,AMap.DistrictSearch,AMap.PolyEditor,AMap.RectangleEditor,AMap.CircleEditor"></script> |
| | | <script src="${ctxPath}/static/modular/system/tSite/electricFence.js"></script> |
| | | <script src="${ctxPath}/static/modular/system/tSite/tSite_info.js"></script> |
| | | @} |
| New file |
| | |
| | | /** |
| | | * 高德地图账号管理初始化 |
| | | */ |
| | | var TGaodeAccount = { |
| | | id: "TGaodeAccountTable", //表格id |
| | | seItem: null, //选中的条目 |
| | | table: null, |
| | | layerIndex: -1 |
| | | }; |
| | | |
| | | /** |
| | | * 初始化表格的列 |
| | | */ |
| | | TGaodeAccount.initColumn = function () { |
| | | return [ |
| | | {field: 'selectItem', radio: true}, |
| | | {title: '主键ID', field: 'id', visible: false, align: 'center', valign: 'middle'}, |
| | | {title: '高德key', field: 'gdKey', visible: true, align: 'center', valign: 'middle',width:'20%', |
| | | formatter: function (value, row) { |
| | | var btn = ""; |
| | | if(row.gdKey != '' && row.gdKey != null) { |
| | | btn = ['<p class="toolTip" style="overflow:hidden;white-space:nowrap;text-overflow:ellipsis;" title="' + row.gdKey + '" onfocus="TUser.tooltip()">' + row.gdKey + '</p>'] |
| | | } |
| | | return btn; |
| | | } |
| | | }, |
| | | {title: '使用状态', field: 'useStatus', visible: true, align: 'center', valign: 'middle', |
| | | formatter: function (value, row) { |
| | | var btn = ""; |
| | | if(row.useStatus != '' && row.useStatus != null) { |
| | | if (row.useStatus == 1){ |
| | | btn = ['<p class="toolTip" style="overflow:hidden;white-space:nowrap;text-overflow:ellipsis;" title="未使用" onfocus="TUser.tooltip()">未使用</p>'] |
| | | } else if (row.useStatus == 2){ |
| | | btn = ['<p class="toolTip" style="overflow:hidden;white-space:nowrap;text-overflow:ellipsis;color: #0d8ddb;" title="使用中" onfocus="TUser.tooltip()">使用中</p>'] |
| | | } else if (row.useStatus == 3){ |
| | | btn = ['<p class="toolTip" style="overflow:hidden;white-space:nowrap;text-overflow:ellipsis;color: red;" title="已用完" onfocus="TUser.tooltip()">已用完</p>'] |
| | | } |
| | | } |
| | | return btn; |
| | | } |
| | | }, |
| | | {title: '状态', field: 'status', visible: true, align: 'center', valign: 'middle', |
| | | formatter: function (value, row) { |
| | | var btn = ""; |
| | | if(row.status != '' && row.status != null) { |
| | | if (row.status == 1){ |
| | | btn = ['<p class="toolTip" style="overflow:hidden;white-space:nowrap;text-overflow:ellipsis;color: #0d8ddb;" title="启用" onfocus="TUser.tooltip()">启用</p>'] |
| | | } else if (row.status == 2){ |
| | | btn = ['<p class="toolTip" style="overflow:hidden;white-space:nowrap;text-overflow:ellipsis;color: red;" title="禁用" onfocus="TUser.tooltip()">禁用</p>'] |
| | | } |
| | | } |
| | | return btn; |
| | | } |
| | | }, |
| | | {title: '删除状态', field: 'delFlag', visible: true, align: 'center', valign: 'middle', |
| | | formatter: function (value, row) { |
| | | var btn = ""; |
| | | if(row.delFlag != '' && row.delFlag != null) { |
| | | if (row.delFlag == 0){ |
| | | btn = ['<p class="toolTip" style="overflow:hidden;white-space:nowrap;text-overflow:ellipsis;" title="未删除" onfocus="TUser.tooltip()">未删除</p>'] |
| | | } else if (row.delFlag == 1){ |
| | | btn = ['<p class="toolTip" style="overflow:hidden;white-space:nowrap;text-overflow:ellipsis;color: red;" title="已删除" onfocus="TUser.tooltip()">已删除</p>'] |
| | | } |
| | | } |
| | | return btn; |
| | | } |
| | | }, |
| | | {title: '添加时间', field: 'insertTime', visible: true, align: 'center', valign: 'middle',width:'12%', |
| | | formatter: function (value, row) { |
| | | var btn = ""; |
| | | if(row.insertTime != '' && row.insertTime != null) { |
| | | var time = row.insertTime.replace(" ",'<br>'); |
| | | btn = ['<p class="toolTip" style="overflow:hidden;white-space:nowrap;text-overflow:ellipsis;" title="' + row.insertTime + '" onfocus="TUser.tooltip()">' + time + '</p>'] |
| | | } |
| | | return btn; |
| | | } |
| | | } |
| | | ]; |
| | | }; |
| | | |
| | | /** |
| | | * 检查是否选中 |
| | | */ |
| | | TGaodeAccount.check = function () { |
| | | var selected = $('#' + this.id).bootstrapTable('getSelections'); |
| | | if(selected.length == 0){ |
| | | Feng.info("请先选中表格中的某一记录!"); |
| | | return false; |
| | | }else{ |
| | | TGaodeAccount.seItem = selected[0]; |
| | | return true; |
| | | } |
| | | }; |
| | | |
| | | /** |
| | | * 点击添加高德地图账号 |
| | | */ |
| | | TGaodeAccount.openAddTGaodeAccount = function () { |
| | | var index = layer.open({ |
| | | type: 2, |
| | | title: '添加高德地图账号', |
| | | area: ['100%', '100%'], //宽高 |
| | | fix: false, //不固定 |
| | | maxmin: true, |
| | | content: Feng.ctxPath + '/tGaodeAccount/tGaodeAccount_add' |
| | | }); |
| | | this.layerIndex = index; |
| | | }; |
| | | |
| | | /** |
| | | * 打开编辑高德地图账号 |
| | | */ |
| | | TGaodeAccount.openTGaodeAccountDetail = function () { |
| | | if (this.check()) { |
| | | var index = layer.open({ |
| | | type: 2, |
| | | title: '编辑高德地图账号', |
| | | area: ['100%', '100%'], //宽高 |
| | | fix: false, //不固定 |
| | | maxmin: true, |
| | | content: Feng.ctxPath + '/tGaodeAccount/tGaodeAccount_update/' + TGaodeAccount.seItem.id |
| | | }); |
| | | this.layerIndex = index; |
| | | } |
| | | }; |
| | | |
| | | /** |
| | | * 删除高德地图账号 |
| | | */ |
| | | TGaodeAccount.delete = function () { |
| | | if (this.check()) { |
| | | var gdKey = TGaodeAccount.seItem.gdKey; |
| | | if (gdKey == "" || gdKey == null || gdKey == undefined) { |
| | | gdKey = "该记录"; |
| | | } else { |
| | | gdKey = "【" + gdKey + "】"; |
| | | } |
| | | swal({ |
| | | title: "您是否确认删除" + gdKey + "?", |
| | | text: "请谨慎操作,删除后不可恢复!", |
| | | type: "warning", |
| | | showCancelButton: true, |
| | | confirmButtonColor: "#DD6B55", |
| | | confirmButtonText: "删除", |
| | | closeOnConfirm: false |
| | | }, function () { |
| | | var ajax = new $ax(Feng.ctxPath + "/tGaodeAccount/delete", function (data) { |
| | | swal("删除成功", "您已经成功删除了" + gdKey + "。", "success"); |
| | | TGaodeAccount.table.refresh(); |
| | | }, function (data) { |
| | | swal("删除失败", data.responseJSON.message + "!", "warning"); |
| | | }); |
| | | ajax.set("tGaodeAccountId",TGaodeAccount.seItem.id); |
| | | ajax.start(); |
| | | }); |
| | | } |
| | | }; |
| | | |
| | | /** |
| | | * 启用 |
| | | */ |
| | | TGaodeAccount.enable = function(){ |
| | | if (this.check()) { |
| | | var gdKey = TGaodeAccount.seItem.gdKey; |
| | | if (gdKey == "" || gdKey == null || gdKey == undefined) { |
| | | gdKey = "该记录"; |
| | | } else { |
| | | gdKey = "【" + gdKey + "】"; |
| | | } |
| | | if (TGaodeAccount.seItem.status == 1) { |
| | | swal("启用失败", "【启用】状态下无需执行此操作", "warning"); |
| | | return; |
| | | } else { |
| | | swal({ |
| | | title: "您是否确认启用" + gdKey + "?", |
| | | text: "请谨慎操作!", |
| | | type: "warning", |
| | | showCancelButton: true, |
| | | confirmButtonColor: "#DD6B55", |
| | | confirmButtonText: "启用", |
| | | closeOnConfirm: false |
| | | }, function () { |
| | | var ajax = new $ax(Feng.ctxPath + "/tGaodeAccount/opt", function (data) { |
| | | swal("启用成功", "您已经成功启用了" + gdKey + "。", "success"); |
| | | TGaodeAccount.table.refresh(); |
| | | }, function (data) { |
| | | swal("启用失败", data.responseJSON.message + "!", "warning"); |
| | | }); |
| | | ajax.set("tGaodeAccountId", TGaodeAccount.seItem.id); |
| | | ajax.set("optType", 1); |
| | | ajax.start(); |
| | | }); |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 禁用 |
| | | */ |
| | | TGaodeAccount.disable = function(){ |
| | | if (this.check()) { |
| | | var gdKey = TGaodeAccount.seItem.gdKey; |
| | | if (gdKey == "" || gdKey == null || gdKey == undefined) { |
| | | gdKey = "该记录"; |
| | | } else { |
| | | gdKey = "【" + gdKey + "】"; |
| | | } |
| | | if (TGaodeAccount.seItem.status == 2) { |
| | | swal("禁用失败", "【禁用】状态下无需执行此操作", "warning"); |
| | | return; |
| | | } else { |
| | | swal({ |
| | | title: "您是否确认禁用" + gdKey + "?", |
| | | text: "请谨慎操作!", |
| | | type: "warning", |
| | | showCancelButton: true, |
| | | confirmButtonColor: "#DD6B55", |
| | | confirmButtonText: "禁用", |
| | | closeOnConfirm: false |
| | | }, function () { |
| | | var ajax = new $ax(Feng.ctxPath + "/tGaodeAccount/opt", function (data) { |
| | | swal("禁用成功", "您已经成功禁用了" + gdKey + "。", "success"); |
| | | TGaodeAccount.table.refresh(); |
| | | }, function (data) { |
| | | swal("禁用失败", data.responseJSON.message + "!", "warning"); |
| | | }); |
| | | ajax.set("tGaodeAccountId", TGaodeAccount.seItem.id); |
| | | ajax.set("optType", 2); |
| | | ajax.start(); |
| | | }); |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 查询高德地图账号列表 |
| | | */ |
| | | TGaodeAccount.search = function () { |
| | | var queryData = {}; |
| | | queryData['gdKey'] = $("#gdKey").val(); |
| | | queryData['useStatus'] = $("#useStatus").val(); |
| | | queryData['status'] = $("#status").val(); |
| | | TGaodeAccount.table.refresh({query: queryData}); |
| | | }; |
| | | TGaodeAccount.resetSearch = function () { |
| | | $("#gdKey").val(""); |
| | | $("#useStatus").val(""); |
| | | $("#status").val(""); |
| | | TGaodeAccount.search(); |
| | | }; |
| | | |
| | | $(function () { |
| | | var defaultColunms = TGaodeAccount.initColumn(); |
| | | var table = new BSTable(TGaodeAccount.id, "/tGaodeAccount/list", defaultColunms); |
| | | table.setPaginationType("server"); |
| | | TGaodeAccount.table = table.init(); |
| | | }); |
| New file |
| | |
| | | /** |
| | | * 初始化高德地图账号详情对话框 |
| | | */ |
| | | var TGaodeAccountInfoDlg = { |
| | | gaodeAccountInfoData : {}, |
| | | validateFields: { |
| | | gdKey: { |
| | | validators: { |
| | | notEmpty: { |
| | | message: '高德key不能为空' |
| | | } |
| | | } |
| | | }, |
| | | useStatus: { |
| | | validators: { |
| | | notEmpty: { |
| | | message: '请先选择使用状态' |
| | | } |
| | | } |
| | | }, |
| | | status: { |
| | | validators: { |
| | | notEmpty: { |
| | | message: '请先选择状态' |
| | | } |
| | | } |
| | | }, |
| | | } |
| | | }; |
| | | |
| | | /** |
| | | * 验证数据是否为空 |
| | | */ |
| | | TGaodeAccountInfoDlg.validate = function () { |
| | | $('#gaodeAccountInfoForm').data("bootstrapValidator").resetForm(); |
| | | $('#gaodeAccountInfoForm').bootstrapValidator('validate'); |
| | | return $("#gaodeAccountInfoForm").data('bootstrapValidator').isValid(); |
| | | }; |
| | | |
| | | |
| | | /** |
| | | * 清除数据 |
| | | */ |
| | | TGaodeAccountInfoDlg.clearData = function() { |
| | | this.gaodeAccountInfoData = {}; |
| | | } |
| | | |
| | | /** |
| | | * 设置对话框中的数据 |
| | | * |
| | | * @param key 数据的名称 |
| | | * @param val 数据的具体值 |
| | | */ |
| | | TGaodeAccountInfoDlg.set = function(key, val) { |
| | | this.gaodeAccountInfoData[key] = (typeof val == "undefined") ? $("#" + key).val() : val; |
| | | return this; |
| | | } |
| | | |
| | | /** |
| | | * 设置对话框中的数据 |
| | | * |
| | | * @param key 数据的名称 |
| | | * @param val 数据的具体值 |
| | | */ |
| | | TGaodeAccountInfoDlg.get = function(key) { |
| | | return $("#" + key).val(); |
| | | } |
| | | |
| | | /** |
| | | * 关闭此对话框 |
| | | */ |
| | | TGaodeAccountInfoDlg.close = function() { |
| | | parent.layer.close(window.parent.TGaodeAccount.layerIndex); |
| | | } |
| | | |
| | | /** |
| | | * 收集数据 |
| | | */ |
| | | TGaodeAccountInfoDlg.collectData = function() { |
| | | this |
| | | .set('id') |
| | | .set('gdKey') |
| | | .set('useStatus') |
| | | .set('status'); |
| | | } |
| | | |
| | | /** |
| | | * 提交添加 |
| | | */ |
| | | TGaodeAccountInfoDlg.addSubmit = function() { |
| | | |
| | | this.clearData(); |
| | | this.collectData(); |
| | | if(!this.validate()){ |
| | | return ; |
| | | } |
| | | //提交信息 |
| | | var ajax = new $ax(Feng.ctxPath + "/tGaodeAccount/add", function(data){ |
| | | Feng.success("添加成功!"); |
| | | window.parent.TGaodeAccount.table.refresh(); |
| | | TGaodeAccountInfoDlg.close(); |
| | | },function(data){ |
| | | Feng.error("添加失败!" + data.responseJSON.message + "!"); |
| | | }); |
| | | ajax.set(this.gaodeAccountInfoData); |
| | | ajax.start(); |
| | | } |
| | | |
| | | /** |
| | | * 提交修改 |
| | | */ |
| | | TGaodeAccountInfoDlg.editSubmit = function() { |
| | | |
| | | this.clearData(); |
| | | this.collectData(); |
| | | if(!this.validate()){ |
| | | return ; |
| | | } |
| | | //提交信息 |
| | | var ajax = new $ax(Feng.ctxPath + "/tGaodeAccount/update", function(data){ |
| | | Feng.success("修改成功!"); |
| | | window.parent.TGaodeAccount.table.refresh(); |
| | | TGaodeAccountInfoDlg.close(); |
| | | },function(data){ |
| | | Feng.error("修改失败!" + data.responseJSON.message + "!"); |
| | | }); |
| | | ajax.set(this.gaodeAccountInfoData); |
| | | ajax.start(); |
| | | } |
| | | |
| | | $(function() { |
| | | Feng.initValidator("gaodeAccountInfoForm", TGaodeAccountInfoDlg.validateFields); |
| | | }); |
| New file |
| | |
| | | package com.stylefeng.guns.modular.system.dao; |
| | | |
| | | import org.apache.ibatis.annotations.Param; |
| | | import org.apache.ibatis.annotations.Select; |
| | | import org.apache.ibatis.annotations.Update; |
| | | |
| | | import java.util.List; |
| | | |
| | | /** |
| | | * 高德地图key管理 Mapper(注解方式,无需XML) |
| | | */ |
| | | public interface GDKeyMapper { |
| | | |
| | | /** |
| | | * 查询可用key(启用、未删除、未用完) |
| | | */ |
| | | @Select("SELECT gdKey FROM t_gaode_account WHERE status = 1 AND delFlag = 0 AND useStatus <> 3") |
| | | List<String> selectAvailableKeys(); |
| | | |
| | | /** |
| | | * 查询已用完的key(用于每日凌晨重置校验) |
| | | */ |
| | | @Select("SELECT gdKey FROM t_gaode_account WHERE status = 1 AND delFlag = 0 AND useStatus = 3") |
| | | List<String> selectExhaustedKeys(); |
| | | |
| | | /** |
| | | * 更新key的使用状态 |
| | | */ |
| | | @Update("UPDATE t_gaode_account SET useStatus = #{useStatus} WHERE gdKey = #{gdKey} AND delFlag = 0") |
| | | int updateUseStatus(@Param("gdKey") String gdKey, @Param("useStatus") Integer useStatus); |
| | | } |
| | |
| | | |
| | | /** |
| | | * 高德猎鹰服务 |
| | | * key次数用完时自动切换可用key(切换后自动重建serverId) |
| | | */ |
| | | @Component |
| | | public class GDFalconUtil implements ApplicationRunner { |
| | | |
| | | private String key = "ca723c3c8a78962a7751129ae035dc9d"; |
| | | |
| | | @Autowired |
| | | private RestTemplate restTemplate; |
| | | |
| | | @Autowired |
| | | private GDKeyManager gdKeyManager; |
| | | |
| | | private static String serverId = null; |
| | | |
| | | /** |
| | | * 当前serverId对应的key(key切换后需重建serverId) |
| | | */ |
| | | private static volatile String currentKey = null; |
| | | |
| | | |
| | | public String getServerId() { |
| | |
| | | } |
| | | |
| | | |
| | | |
| | | /** |
| | | * 初始化创建服务 |
| | | */ |
| | | public void init(){ |
| | | if(serverId == null){ |
| | | JSONArray jsonArray = this.selectServer(); |
| | | if(null == jsonArray){ |
| | | serverId = this.createServer("server"); |
| | | }else{ |
| | | serverId = jsonArray.getJSONObject(0).getString("sid"); |
| | | if (gdKeyManager == null || restTemplate == null) { |
| | | // 非容器实例(如 GunsApplication.main 中 new 出来的对象),由容器内bean完成初始化 |
| | | return; |
| | | } |
| | | ensureServerId(); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 参数构造器:key与sid切换后需按最新值重新构造 |
| | | */ |
| | | private interface ParamsBuilder { |
| | | MultiValueMap<String, Object> build(String key, String sid); |
| | | } |
| | | |
| | | /** |
| | | * 确保serverId与当前使用的key对应: |
| | | * key切换后自动查询/重建serverId;key用完自动切换可用key重试 |
| | | */ |
| | | private synchronized String ensureServerId(){ |
| | | int maxAttempts = 5; |
| | | String lastKey = gdKeyManager.getKey(); |
| | | for (int i = 0; i < maxAttempts; i++) { |
| | | String gdKey = gdKeyManager.getKey(); |
| | | lastKey = gdKey; |
| | | if (serverId != null && gdKey.equals(currentKey)) { |
| | | return gdKey; |
| | | } |
| | | // 1. 查询该key下的服务列表 |
| | | JSONArray jsonArray = null; |
| | | try { |
| | | String url = "https://tsapi.amap.com/v1/track/service/list?key=" + gdKey; |
| | | String forObject = restTemplate.getForObject(url, String.class); |
| | | JSONObject jsonObject = JSON.parseObject(forObject); |
| | | if (GDKeyManager.isFalconExhausted(jsonObject)) { |
| | | gdKeyManager.reportExhausted(gdKey); |
| | | serverId = null; |
| | | continue; |
| | | } |
| | | if (jsonObject.getIntValue("errcode") == 10000) { |
| | | JSONObject data = jsonObject.getJSONObject("data"); |
| | | if (null != data) { |
| | | jsonArray = data.getJSONArray("results"); |
| | | } |
| | | } |
| | | } catch (Exception e) { |
| | | loggerWarn("查询猎鹰服务列表异常", e); |
| | | } |
| | | if (jsonArray != null && jsonArray.size() > 0) { |
| | | serverId = jsonArray.getJSONObject(0).getString("sid"); |
| | | currentKey = gdKey; |
| | | return gdKey; |
| | | } |
| | | // 2. 无服务:创建新服务 |
| | | String sid = createServerInternal("server", gdKey); |
| | | if (sid != null) { |
| | | serverId = sid; |
| | | currentKey = gdKey; |
| | | return gdKey; |
| | | } |
| | | // 3. 创建失败(可能key用完):切换key重试 |
| | | gdKeyManager.reportExhausted(gdKey); |
| | | serverId = null; |
| | | } |
| | | return lastKey; |
| | | } |
| | | |
| | | private void loggerWarn(String msg, Exception e) { |
| | | System.err.println("[GDFalconUtil] " + msg + ":" + e.getMessage()); |
| | | } |
| | | |
| | | /** |
| | | * 执行猎鹰POST表单请求:key次数用完时自动切换key并重建serverId重试 |
| | | */ |
| | | private String doFalconPost(String url, ParamsBuilder builder) { |
| | | String lastResp = null; |
| | | int maxAttempts = 5; |
| | | for (int i = 0; i < maxAttempts; i++) { |
| | | String gdKey = ensureServerId(); |
| | | String sid = serverId; |
| | | MultiValueMap<String, Object> params = builder.build(gdKey, sid); |
| | | HttpHeaders headers = new HttpHeaders(); |
| | | headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); |
| | | HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(params, headers); |
| | | String resp = restTemplate.postForObject(url, requestEntity, String.class); |
| | | lastResp = resp; |
| | | JSONObject jsonObject; |
| | | try { |
| | | jsonObject = JSON.parseObject(resp); |
| | | } catch (Exception e) { |
| | | return resp; |
| | | } |
| | | if (GDKeyManager.isFalconExhausted(jsonObject)) { |
| | | gdKeyManager.reportExhausted(gdKey); |
| | | serverId = null; |
| | | continue; |
| | | } |
| | | return resp; |
| | | } |
| | | return lastResp; |
| | | } |
| | | |
| | | /** |
| | | * 执行猎鹰GET请求:key次数用完时自动切换可用key重试 |
| | | */ |
| | | private String doFalconGet(String baseUrl) { |
| | | String lastResp = null; |
| | | int maxAttempts = 5; |
| | | for (int i = 0; i < maxAttempts; i++) { |
| | | String gdKey = gdKeyManager.getKey(); |
| | | String url = baseUrl + (baseUrl.contains("?") ? "&" : "?") + "key=" + gdKey; |
| | | String resp = restTemplate.getForObject(url, String.class); |
| | | lastResp = resp; |
| | | JSONObject jsonObject; |
| | | try { |
| | | jsonObject = JSON.parseObject(resp); |
| | | } catch (Exception e) { |
| | | return resp; |
| | | } |
| | | if (GDKeyManager.isFalconExhausted(jsonObject)) { |
| | | gdKeyManager.reportExhausted(gdKey); |
| | | continue; |
| | | } |
| | | return resp; |
| | | } |
| | | return lastResp; |
| | | } |
| | | |
| | | |
| | |
| | | * @return |
| | | */ |
| | | public String createServer(String name){ |
| | | return createServerInternal(name, gdKeyManager.getKey()); |
| | | } |
| | | |
| | | /** |
| | | * 创建新的服务(内部直连,供ensureServerId使用) |
| | | */ |
| | | private String createServerInternal(String name, String key){ |
| | | String url = "https://tsapi.amap.com/v1/track/service/add"; |
| | | //调用推送 |
| | | HttpHeaders headers = new HttpHeaders(); |
| | |
| | | |
| | | |
| | | /** |
| | | * 查询服务 |
| | | * @return |
| | | */ |
| | | private JSONArray selectServer(){ |
| | | String url = "https://tsapi.amap.com/v1/track/service/list?key=" + key; |
| | | String forObject = restTemplate.getForObject(url, String.class); |
| | | JSONObject jsonObject = JSON.parseObject(forObject); |
| | | if(jsonObject.getIntValue("errcode") == 10000){ |
| | | JSONObject data = jsonObject.getJSONObject("data"); |
| | | if(null != data){ |
| | | return data.getJSONArray("results"); |
| | | } |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | |
| | | /** |
| | | * 创建终端数据 |
| | | * @param name |
| | | * @return |
| | |
| | | if(tid != null){ |
| | | return tid; |
| | | } |
| | | String url = "https://tsapi.amap.com/v1/track/terminal/add"; |
| | | //调用推送 |
| | | HttpHeaders headers = new HttpHeaders(); |
| | | // 以表单的方式提交 |
| | | headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); |
| | | //将请求头部和参数合成一个请求 |
| | | MultiValueMap<String, Object> params = new LinkedMultiValueMap<>(); |
| | | params.add("key", key); |
| | | params.add("sid", serverId); |
| | | params.add("name", name); |
| | | HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(params, headers); |
| | | String s = restTemplate.postForObject(url, requestEntity , String.class); |
| | | String s = doFalconPost("https://tsapi.amap.com/v1/track/terminal/add", (key, sid) -> { |
| | | MultiValueMap<String, Object> params = new LinkedMultiValueMap<>(); |
| | | params.add("key", key); |
| | | params.add("sid", sid); |
| | | params.add("name", name); |
| | | return params; |
| | | }); |
| | | JSONObject jsonObject = JSON.parseObject(s); |
| | | if(jsonObject.getIntValue("errcode") == 10000){ |
| | | JSONObject data = jsonObject.getJSONObject("data"); |
| | |
| | | * @return |
| | | */ |
| | | public String selectTerminal(String name){ |
| | | String url = "https://tsapi.amap.com/v1/track/terminal/list?key=" + key + "&name=" + name; |
| | | String forObject = restTemplate.getForObject(url, String.class); |
| | | String forObject = doFalconGet("https://tsapi.amap.com/v1/track/terminal/list?name=" + name); |
| | | JSONObject jsonObject = JSON.parseObject(forObject); |
| | | if(jsonObject.getIntValue("errcode") == 10000){ |
| | | JSONObject data = jsonObject.getJSONObject("data"); |
| | |
| | | * @return |
| | | */ |
| | | public String createTrack(String tid){ |
| | | String url = "https://tsapi.amap.com/v1/track/trace/add"; |
| | | //调用推送 |
| | | HttpHeaders headers = new HttpHeaders(); |
| | | // 以表单的方式提交 |
| | | headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); |
| | | //将请求头部和参数合成一个请求 |
| | | MultiValueMap<String, Object> params = new LinkedMultiValueMap<>(); |
| | | params.add("key", key); |
| | | params.add("sid", serverId); |
| | | params.add("tid", tid); |
| | | HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(params, headers); |
| | | String s = restTemplate.postForObject(url, requestEntity , String.class); |
| | | String s = doFalconPost("https://tsapi.amap.com/v1/track/trace/add", (key, sid) -> { |
| | | MultiValueMap<String, Object> params = new LinkedMultiValueMap<>(); |
| | | params.add("key", key); |
| | | params.add("sid", sid); |
| | | params.add("tid", tid); |
| | | return params; |
| | | }); |
| | | JSONObject jsonObject = JSON.parseObject(s); |
| | | if(jsonObject.getIntValue("errcode") == 10000){ |
| | | return jsonObject.getString("data"); |
| | |
| | | * @param trid 轨迹id |
| | | */ |
| | | public void deleteTrack(String tid, String trid){ |
| | | String url = "https://tsapi.amap.com/v1/track/trace/delete"; |
| | | //调用推送 |
| | | HttpHeaders headers = new HttpHeaders(); |
| | | // 以表单的方式提交 |
| | | headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); |
| | | //将请求头部和参数合成一个请求 |
| | | MultiValueMap<String, Object> params = new LinkedMultiValueMap<>(); |
| | | params.add("key", key); |
| | | params.add("sid", serverId); |
| | | params.add("tid", tid); |
| | | params.add("trid", trid); |
| | | HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(params, headers); |
| | | String s = restTemplate.postForObject(url, requestEntity , String.class); |
| | | String s = doFalconPost("https://tsapi.amap.com/v1/track/trace/delete", (key, sid) -> { |
| | | MultiValueMap<String, Object> params = new LinkedMultiValueMap<>(); |
| | | params.add("key", key); |
| | | params.add("sid", sid); |
| | | params.add("tid", tid); |
| | | params.add("trid", trid); |
| | | return params; |
| | | }); |
| | | JSONObject jsonObject = JSON.parseObject(s); |
| | | if(jsonObject.getIntValue("errcode") != 10000){ |
| | | System.err.println(jsonObject.getString("errmsg")); |
| | | } |
| | | } |
| | | |
| | | |
| | | // |
| | | // public Map<String, Object> queryTrsearch(){ |
| | | // |
| | | // } |
| | | } |
| New file |
| | |
| | | package com.stylefeng.guns.modular.system.util; |
| | | |
| | | import com.alibaba.fastjson.JSON; |
| | | import com.alibaba.fastjson.JSONObject; |
| | | import com.stylefeng.guns.modular.system.dao.GDKeyMapper; |
| | | import org.slf4j.Logger; |
| | | import org.slf4j.LoggerFactory; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.scheduling.annotation.Scheduled; |
| | | import org.springframework.stereotype.Component; |
| | | import org.springframework.web.client.RestTemplate; |
| | | |
| | | import javax.annotation.PostConstruct; |
| | | import java.util.ArrayList; |
| | | import java.util.List; |
| | | import java.util.Set; |
| | | import java.util.concurrent.ConcurrentHashMap; |
| | | import java.util.concurrent.CopyOnWriteArrayList; |
| | | import java.util.concurrent.atomic.AtomicInteger; |
| | | |
| | | /** |
| | | * 高德地图key管理器 |
| | | * <p> |
| | | * 功能: |
| | | * 1. key池管理:从 t_gaode_account 表加载可用key(启用、未删除、未用完),轮询获取; |
| | | * 表为空或全部用尽时降级使用兜底key(原硬编码key)。 |
| | | * 2. 自动切换:调用方检测到key"次数用完"后调用 reportExhausted 上报, |
| | | * 管理器将key移出内存池并置库表 useStatus=3(已用完)。 |
| | | * 3. 每日凌晨重置校验:每天 00:00 将 useStatus=3 的key重置为未使用并放回池, |
| | | * 随后调用高德最轻量接口校验配额是否真正恢复;未恢复的key重新标记已用完, |
| | | * 进入延迟重试队列,每30分钟校验一次直到恢复额度。 |
| | | * 4. 兜底key:当天用完仅内存禁用,次日凌晨自动恢复;不写库(表内无对应记录)。 |
| | | */ |
| | | @Component |
| | | public class GDKeyManager { |
| | | |
| | | private static final Logger logger = LoggerFactory.getLogger(GDKeyManager.class); |
| | | |
| | | /** |
| | | * 兜底key(原硬编码key):管理端/用户端 |
| | | */ |
| | | public static final String FALLBACK_KEY = "39b31fa22955efa8fce9eed487241db9"; |
| | | |
| | | /** |
| | | * 配额校验接口:最轻量的通用Web服务接口,每次仅消耗1次配额 |
| | | */ |
| | | private static final String VERIFY_URL = "https://restapi.amap.com/v3/geocode/geo?output=JSON&address=%E5%8C%97%E4%BA%AC%E5%B8%82&key="; |
| | | |
| | | @Autowired |
| | | private GDKeyMapper gdKeyMapper; |
| | | |
| | | @Autowired |
| | | private RestTemplate restTemplate; |
| | | |
| | | /** |
| | | * 内存可用key池(refreshPool整体替换,保证getKey不会看到clear/addAll中间态) |
| | | */ |
| | | private volatile List<String> availableKeys = new CopyOnWriteArrayList<>(); |
| | | |
| | | /** |
| | | * 轮询下标 |
| | | */ |
| | | private final AtomicInteger index = new AtomicInteger(0); |
| | | |
| | | /** |
| | | * 延迟校验队列:key -> 已重试次数(每日凌晨校验未恢复的key) |
| | | */ |
| | | private final ConcurrentHashMap<String, Integer> pendingVerifyKeys = new ConcurrentHashMap<>(); |
| | | |
| | | /** |
| | | * 兜底key当日是否已用完 |
| | | */ |
| | | private volatile boolean fallbackExhausted = false; |
| | | |
| | | /** |
| | | * 已标记"使用中"(useStatus=2)的key集合,避免热路径每次取用都写库 |
| | | */ |
| | | private final Set<String> inUseKeys = ConcurrentHashMap.newKeySet(); |
| | | |
| | | @PostConstruct |
| | | public void init() { |
| | | refreshPool(); |
| | | } |
| | | |
| | | /** |
| | | * 刷新可用key池 |
| | | */ |
| | | public synchronized void refreshPool() { |
| | | try { |
| | | List<String> keys = gdKeyMapper.selectAvailableKeys(); |
| | | List<String> newPool = new CopyOnWriteArrayList<>(); |
| | | if (keys != null) { |
| | | newPool.addAll(keys); |
| | | } |
| | | // 整体替换,避免clear/addAll中间态导致getKey短暂看到空池误降级兜底key |
| | | availableKeys = newPool; |
| | | logger.info("[GDKeyManager] 高德key池刷新完成,可用key数:{}", newPool.size()); |
| | | } catch (Exception e) { |
| | | // 表不存在(用户端/司机端未建表时)等异常:降级使用兜底key,不影响系统运行 |
| | | logger.warn("[GDKeyManager] 加载高德key失败(t_gaode_account表可能不存在),降级使用兜底key", e); |
| | | availableKeys = new CopyOnWriteArrayList<>(); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 获取一个可用key:池非空轮询池内key;池空使用兜底key(当日未用完时) |
| | | */ |
| | | public String getKey() { |
| | | List<String> pool = availableKeys; |
| | | if (!pool.isEmpty()) { |
| | | String key = pickFromPool(pool); |
| | | markInUse(key); |
| | | return key; |
| | | } |
| | | if (!fallbackExhausted) { |
| | | return FALLBACK_KEY; |
| | | } |
| | | // 兜底key当日也已用完:尝试刷新池(可能有新录入的key) |
| | | refreshPool(); |
| | | pool = availableKeys; |
| | | if (!pool.isEmpty()) { |
| | | String key = pickFromPool(pool); |
| | | markInUse(key); |
| | | return key; |
| | | } |
| | | return FALLBACK_KEY; |
| | | } |
| | | |
| | | /** |
| | | * 从内存池轮询取一个key(使用调用方传入的池快照,避免与定时刷新交叉导致越界) |
| | | */ |
| | | private String pickFromPool(List<String> pool) { |
| | | int i = Math.abs(index.getAndIncrement() % pool.size()); |
| | | return pool.get(i); |
| | | } |
| | | |
| | | /** |
| | | * 将表内key标记为"使用中"(useStatus=2): |
| | | * 仅key首次被取用时写库一次;兜底key不写库;写库失败不影响取key流程。 |
| | | */ |
| | | private void markInUse(String key) { |
| | | if (key == null || FALLBACK_KEY.equals(key) || inUseKeys.contains(key)) { |
| | | return; |
| | | } |
| | | try { |
| | | gdKeyMapper.updateUseStatus(key, 2); |
| | | inUseKeys.add(key); |
| | | logger.info("[GDKeyManager] 高德key[{}]开始使用,已标记为使用中", key); |
| | | } catch (Exception e) { |
| | | // 表不存在等异常:本次不标记,下次取用再尝试,不影响业务 |
| | | logger.warn("[GDKeyManager] 标记高德key[{}]使用中失败(表可能不存在),不影响使用", key); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 定时刷新key池:每5分钟一次,让管理后台新录入的key及时生效 |
| | | */ |
| | | @Scheduled(fixedDelay = 5 * 60 * 1000, initialDelay = 60 * 1000) |
| | | public void scheduledRefreshPool() { |
| | | refreshPool(); |
| | | } |
| | | |
| | | /** |
| | | * 上报某个key次数用完: |
| | | * 表内key -> 移出内存池 + 置库表 useStatus=3; |
| | | * 兜底key -> 仅内存当日禁用(不写库),次日凌晨自动恢复。 |
| | | */ |
| | | public void reportExhausted(String usedKey) { |
| | | if (usedKey == null || usedKey.isEmpty()) { |
| | | return; |
| | | } |
| | | availableKeys.remove(usedKey); |
| | | inUseKeys.remove(usedKey); |
| | | if (FALLBACK_KEY.equals(usedKey)) { |
| | | fallbackExhausted = true; |
| | | logger.warn("[GDKeyManager] 兜底key次数已用完,今日禁用,次日凌晨自动恢复"); |
| | | return; |
| | | } |
| | | try { |
| | | gdKeyMapper.updateUseStatus(usedKey, 3); |
| | | logger.warn("[GDKeyManager] 高德key[{}]次数用完,已标记为已用完并移出key池", usedKey); |
| | | } catch (Exception e) { |
| | | logger.warn("[GDKeyManager] 更新高德key[{}]使用状态失败(表可能不存在),仅内存剔除", usedKey); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 判断Web服务API响应是否为"次数用完/不可用"(依据infocode) |
| | | */ |
| | | public static boolean isWebKeyExhausted(JSONObject resp) { |
| | | if (resp == null) { |
| | | return false; |
| | | } |
| | | String infocode = resp.getString("infocode"); |
| | | if (infocode == null || infocode.isEmpty()) { |
| | | return false; |
| | | } |
| | | switch (infocode) { |
| | | case "10001": // key不正确或过期 |
| | | case "10003": // 超过每日配额 |
| | | case "10004": // 访问过于频繁 |
| | | case "10005": // 超出每秒访问量限制 |
| | | case "10010": // 权限不足 |
| | | case "10012": // 服务未开放/权限不足 |
| | | case "10019": // 使用配额已用尽 |
| | | case "10020": // 并发超限 |
| | | case "10044": // 签名错误/未开通 |
| | | return true; |
| | | default: |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 判断猎鹰API响应是否为"次数用完"(依据errcode/errmsg) |
| | | */ |
| | | public static boolean isFalconExhausted(JSONObject resp) { |
| | | if (resp == null) { |
| | | return false; |
| | | } |
| | | int errcode = resp.getIntValue("errcode"); |
| | | if (errcode == 10000) { |
| | | return false; |
| | | } |
| | | if (errcode == 20001) { |
| | | return true; // 权限不足 |
| | | } |
| | | String errmsg = resp.getString("errmsg"); |
| | | if (errmsg == null) { |
| | | return false; |
| | | } |
| | | return errmsg.contains("配额") || errmsg.contains("上限") || errmsg.contains("次数") |
| | | || errmsg.contains("用完") || errmsg.contains("超限") |
| | | || errmsg.contains("quota") || errmsg.contains("limit"); |
| | | } |
| | | |
| | | // ==================== 每日凌晨重置 + API校验恢复额度 ==================== |
| | | |
| | | /** |
| | | * 每日凌晨 00:00 自动执行: |
| | | * 1. 将 useStatus=3(已用完)的key重置为未使用并放回内存池; |
| | | * 2. 调用高德API校验配额是否真正恢复; |
| | | * 3. 未恢复的key重新标记已用完,进入延迟重试队列,直到恢复额度。 |
| | | */ |
| | | @Scheduled(cron = "0 0 0 * * ?") |
| | | public void dailyResetAndVerify() { |
| | | logger.info("[GDKeyManager] 开始每日高德key配额重置校验"); |
| | | // 兜底key当日禁用标记次日自动恢复 |
| | | fallbackExhausted = false; |
| | | List<String> exhaustedKeys; |
| | | try { |
| | | exhaustedKeys = gdKeyMapper.selectExhaustedKeys(); |
| | | } catch (Exception e) { |
| | | logger.warn("[GDKeyManager] 查询已用完key失败(表可能不存在),跳过重置校验", e); |
| | | return; |
| | | } |
| | | if (exhaustedKeys == null || exhaustedKeys.isEmpty()) { |
| | | logger.info("[GDKeyManager] 无已用完的key,跳过重置校验"); |
| | | return; |
| | | } |
| | | for (String gdKey : exhaustedKeys) { |
| | | // 1. 先重置为未使用 |
| | | try { |
| | | gdKeyMapper.updateUseStatus(gdKey, 1); |
| | | } catch (Exception e) { |
| | | logger.warn("[GDKeyManager] 重置key[{}]使用状态失败", gdKey, e); |
| | | } |
| | | // 2. 调用API校验是否真正恢复额度 |
| | | boolean recovered = verifyQuota(gdKey); |
| | | if (recovered) { |
| | | if (!availableKeys.contains(gdKey)) { |
| | | availableKeys.add(gdKey); |
| | | } |
| | | pendingVerifyKeys.remove(gdKey); |
| | | logger.info("[GDKeyManager] 高德key[{}]配额已恢复,重新加入key池", gdKey); |
| | | } else { |
| | | // 未恢复:重新标记已用完,进入延迟重试队列 |
| | | try { |
| | | gdKeyMapper.updateUseStatus(gdKey, 3); |
| | | } catch (Exception e) { |
| | | logger.warn("[GDKeyManager] 回写key[{}]已用完状态失败", gdKey, e); |
| | | } |
| | | availableKeys.remove(gdKey); |
| | | pendingVerifyKeys.put(gdKey, 0); |
| | | logger.warn("[GDKeyManager] 高德key[{}]配额尚未恢复,进入延迟重试队列", gdKey); |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 延迟重试任务:每30分钟校验一次待恢复的key,直到恢复额度为止 |
| | | */ |
| | | @Scheduled(fixedDelay = 30 * 60 * 1000, initialDelay = 30 * 60 * 1000) |
| | | public void retryPendingKeys() { |
| | | if (pendingVerifyKeys.isEmpty()) { |
| | | return; |
| | | } |
| | | for (String gdKey : new ArrayList<>(pendingVerifyKeys.keySet())) { |
| | | boolean recovered = verifyQuota(gdKey); |
| | | if (recovered) { |
| | | try { |
| | | gdKeyMapper.updateUseStatus(gdKey, 1); |
| | | } catch (Exception e) { |
| | | logger.warn("[GDKeyManager] 恢复key[{}]使用状态失败", gdKey, e); |
| | | } |
| | | if (!availableKeys.contains(gdKey)) { |
| | | availableKeys.add(gdKey); |
| | | } |
| | | pendingVerifyKeys.remove(gdKey); |
| | | logger.info("[GDKeyManager] 高德key[{}]延迟校验通过,配额已恢复,重新加入key池", gdKey); |
| | | } else { |
| | | int count = pendingVerifyKeys.merge(gdKey, 1, Integer::sum); |
| | | logger.warn("[GDKeyManager] 高德key[{}]配额仍未恢复(已校验{}次),继续等待", gdKey, count); |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 调用高德最轻量接口校验配额是否恢复 |
| | | * |
| | | * @return true=已恢复(或无法判断,保守放行);false=明确未恢复(配额类错误) |
| | | */ |
| | | private boolean verifyQuota(String gdKey) { |
| | | try { |
| | | String resp = restTemplate.getForObject(VERIFY_URL + gdKey, String.class); |
| | | JSONObject json = JSON.parseObject(resp); |
| | | String status = json.getString("status"); |
| | | String infocode = json.getString("infocode"); |
| | | if ("1".equals(status) && "10000".equals(infocode)) { |
| | | return true; |
| | | } |
| | | // 权限类错误:该key未开通Web服务校验权限(如猎鹰专用key),无法判断配额,按已恢复处理 |
| | | if ("10044".equals(infocode) || "10012".equals(infocode) |
| | | || "10002".equals(infocode) || "10010".equals(infocode)) { |
| | | logger.info("[GDKeyManager] 高德key[{}]无Web服务校验权限(infocode={}),按已恢复处理", gdKey, infocode); |
| | | return true; |
| | | } |
| | | // 配额类错误:明确未恢复 |
| | | if ("10001".equals(infocode) || "10003".equals(infocode) || "10019".equals(infocode)) { |
| | | return false; |
| | | } |
| | | // 其他错误(限流/网络等):保守放行,真实调用失败时会被自动切换机制再次标记 |
| | | return true; |
| | | } catch (Exception e) { |
| | | logger.warn("[GDKeyManager] 校验key[{}]配额异常,按已恢复处理", gdKey, e); |
| | | return true; |
| | | } |
| | | } |
| | | } |
| | |
| | | @Autowired |
| | | private IGDInterfaceService gdInterfaceService; |
| | | |
| | | private String key = "ca723c3c8a78962a7751129ae035dc9d"; |
| | | @Autowired |
| | | private GDKeyManager gdKeyManager; |
| | | |
| | | private JSONArray jsonArray = new JSONArray(); |
| | | |
| | | /** |
| | | * 执行GET请求:key次数用完时自动切换可用key重试 |
| | | */ |
| | | private String doGetWithRetry(String baseUrl) { |
| | | String lastResp = null; |
| | | int maxAttempts = 5; |
| | | for (int i = 0; i < maxAttempts; i++) { |
| | | String key = gdKeyManager.getKey(); |
| | | String url = baseUrl + (baseUrl.contains("?") ? "&" : "?") + "key=" + key; |
| | | String resp = restTemplate.getForObject(url, String.class); |
| | | lastResp = resp; |
| | | JSONObject jsonObject; |
| | | try { |
| | | jsonObject = JSON.parseObject(resp); |
| | | } catch (Exception e) { |
| | | return resp; |
| | | } |
| | | if (GDKeyManager.isWebKeyExhausted(jsonObject)) { |
| | | gdKeyManager.reportExhausted(key); |
| | | continue; |
| | | } |
| | | return resp; |
| | | } |
| | | return lastResp; |
| | | } |
| | | |
| | | /** |
| | | * 执行POST请求(JSON体):key次数用完时自动切换可用key重试 |
| | | */ |
| | | private String doPostJsonWithRetry(String baseUrl, String jsonBody) { |
| | | String lastResp = null; |
| | | int maxAttempts = 5; |
| | | HttpHeaders headers = new HttpHeaders(); |
| | | MediaType type = MediaType.parseMediaType("application/json; charset=UTF-8"); |
| | | headers.setContentType(type); |
| | | headers.add("Accept", MediaType.APPLICATION_JSON.toString()); |
| | | for (int i = 0; i < maxAttempts; i++) { |
| | | String key = gdKeyManager.getKey(); |
| | | String url = baseUrl + (baseUrl.contains("?") ? "&" : "?") + "key=" + key; |
| | | HttpEntity<String> formEntity = new HttpEntity<>(jsonBody, headers); |
| | | String resp = restTemplate.postForObject(url, formEntity, String.class); |
| | | lastResp = resp; |
| | | JSONObject jsonObject; |
| | | try { |
| | | jsonObject = JSON.parseObject(resp); |
| | | } catch (Exception e) { |
| | | return resp; |
| | | } |
| | | if (GDKeyManager.isWebKeyExhausted(jsonObject)) { |
| | | gdKeyManager.reportExhausted(key); |
| | | continue; |
| | | } |
| | | return resp; |
| | | } |
| | | return lastResp; |
| | | } |
| | | |
| | | /** |
| | | * 执行PATCH请求(JSON体):key次数用完时自动切换可用key重试 |
| | | */ |
| | | private String doPatchJsonWithRetry(String baseUrl, String jsonBody) { |
| | | String lastResp = null; |
| | | int maxAttempts = 5; |
| | | HttpHeaders headers = new HttpHeaders(); |
| | | MediaType type = MediaType.parseMediaType("application/json; charset=UTF-8"); |
| | | headers.setContentType(type); |
| | | headers.add("Accept", MediaType.APPLICATION_JSON.toString()); |
| | | for (int i = 0; i < maxAttempts; i++) { |
| | | String key = gdKeyManager.getKey(); |
| | | String url = baseUrl + (baseUrl.contains("?") ? "&" : "?") + "key=" + key; |
| | | HttpEntity<String> formEntity = new HttpEntity<>(jsonBody, headers); |
| | | String resp = restTemplate.patchForObject(url, formEntity, String.class); |
| | | lastResp = resp; |
| | | JSONObject jsonObject; |
| | | try { |
| | | jsonObject = JSON.parseObject(resp); |
| | | } catch (Exception e) { |
| | | return resp; |
| | | } |
| | | if (GDKeyManager.isWebKeyExhausted(jsonObject)) { |
| | | gdKeyManager.reportExhausted(key); |
| | | continue; |
| | | } |
| | | return resp; |
| | | } |
| | | return lastResp; |
| | | } |
| | | |
| | | |
| | | @Bean |
| | |
| | | json.put("alert_condition", "enter;leave"); |
| | | json.put("desc", id); |
| | | |
| | | HttpHeaders headers = new HttpHeaders(); |
| | | MediaType type = MediaType.parseMediaType("application/json; charset=UTF-8"); |
| | | headers.setContentType(type); |
| | | headers.add("Accept", MediaType.APPLICATION_JSON.toString()); |
| | | HttpEntity<String> formEntity = new HttpEntity<>(json.toString(), headers); |
| | | |
| | | String url = "https://restapi.amap.com/v4/geofence/meta?key=" + key; |
| | | String js = restTemplate.postForObject(url, formEntity, String.class); |
| | | String js = doPostJsonWithRetry("https://restapi.amap.com/v4/geofence/meta", json.toString()); |
| | | JSONObject jsonObject = JSON.parseObject(js); |
| | | JSONObject data = jsonObject.getJSONObject("data"); |
| | | String status = data.getString("status"); |
| | |
| | | * 查询以创建的所有电子围栏 |
| | | */ |
| | | public String findElectricFenc(String center, String radius, String points){ |
| | | String url = "https://restapi.amap.com/v4/geofence/meta?key=" + key; |
| | | String forObject = restTemplate.getForObject(url, String.class); |
| | | String url = "https://restapi.amap.com/v4/geofence/meta"; |
| | | String forObject = doGetWithRetry(url); |
| | | JSONObject jsonObject = JSON.parseObject(forObject); |
| | | JSONObject data = jsonObject.getJSONObject("data"); |
| | | |
| | |
| | | if(page == 1){ |
| | | jsonArray = new JSONArray(); |
| | | } |
| | | String url = "https://restapi.amap.com/v4/geofence/meta?key=" + key + "&page_no=" + page + "&page_size=" + 30; |
| | | String forObject = restTemplate.getForObject(url, String.class); |
| | | String url = "https://restapi.amap.com/v4/geofence/meta?page_no=" + page + "&page_size=" + 30; |
| | | String forObject = doGetWithRetry(url); |
| | | JSONObject jsonObject = JSON.parseObject(forObject); |
| | | JSONObject data = jsonObject.getJSONObject("data"); |
| | | JSONArray rs_list = data.getJSONArray("rs_list"); |
| | |
| | | json.put("alert_condition", "enter;leave"); |
| | | json.put("desc", id + "围栏更新描述"); |
| | | |
| | | HttpHeaders headers = new HttpHeaders(); |
| | | MediaType type = MediaType.parseMediaType("application/json; charset=UTF-8"); |
| | | headers.setContentType(type); |
| | | headers.add("Accept", MediaType.APPLICATION_JSON.toString()); |
| | | HttpEntity<String> formEntity = new HttpEntity<>(json.toString(), headers); |
| | | |
| | | String url = "https://restapi.amap.com/v4/geofence/meta?key=" + key + "&gid=" + gid; |
| | | String s = restTemplate.patchForObject(url, formEntity, String.class); |
| | | String url = "https://restapi.amap.com/v4/geofence/meta?gid=" + gid; |
| | | String s = doPatchJsonWithRetry(url, json.toString()); |
| | | System.out.println(s); |
| | | |
| | | // gdInterfaceService.saveData("https://restapi.amap.com/v4/geofence/meta", "更新电子围栏"); |
| | |
| | | diu = "B78F99DD-2496-4A43-843B-172CDA2D9798"; |
| | | } |
| | | locations = locations + "," + (new Date().getTime() / 1000); |
| | | String url = "https://restapi.amap.com/v4/geofence/status?key=" + key + "&diu=" + diu + "&locations=" + locations; |
| | | String forObject = restTemplate.getForObject(url, String.class); |
| | | String url = "https://restapi.amap.com/v4/geofence/status?diu=" + diu + "&locations=" + locations; |
| | | String forObject = doGetWithRetry(url); |
| | | JSONObject jsonObject = JSON.parseObject(forObject); |
| | | List<String> ids = new ArrayList<>(); |
| | | JSONObject data = jsonObject.getJSONObject("data"); |
| | |
| | | * @return |
| | | */ |
| | | public boolean deleteElectricFenc(String gid){ |
| | | restTemplate.delete("https://restapi.amap.com/v4/geofence/meta?key={1}&gid={2}", key, gid); |
| | | restTemplate.delete("https://restapi.amap.com/v4/geofence/meta?key={1}&gid={2}", gdKeyManager.getKey(), gid); |
| | | System.out.println("删除:" ); |
| | | |
| | | // gdInterfaceService.saveData("https://restapi.amap.com/v4/geofence/meta", "删除电子围栏"); |
| | |
| | | * @return |
| | | */ |
| | | public Map<String, String> getDistance(String origins, String destination, Integer type){ |
| | | String url = "https://restapi.amap.com/v3/distance?key=" + key + "&origins=" + origins + "&destination=" + destination + |
| | | String url = "https://restapi.amap.com/v3/distance?origins=" + origins + "&destination=" + destination + |
| | | "&type=" + type; |
| | | String forObject = restTemplate.getForObject(url, String.class); |
| | | String forObject = doGetWithRetry(url); |
| | | JSONObject jsonObject = JSON.parseObject(forObject); |
| | | String status = jsonObject.getString("status"); |
| | | |
| | |
| | | * @return |
| | | */ |
| | | public Map<String, String> getDriving(String origins, String destination, String waypoints){ |
| | | String url = "https://restapi.amap.com/v3/direction/driving?key=" + key + "&origin=" + origins + "&destination=" + destination + |
| | | String url = "https://restapi.amap.com/v3/direction/driving?origin=" + origins + "&destination=" + destination + |
| | | "&waypoints=" + waypoints + "&extensions=base&strategy=0"; |
| | | String forObject = restTemplate.getForObject(url, String.class); |
| | | String forObject = doGetWithRetry(url); |
| | | JSONObject jsonObject = JSON.parseObject(forObject); |
| | | String status = jsonObject.getString("status"); |
| | | |
| | |
| | | @Component |
| | | public class GDMapGeocodingUtil { |
| | | |
| | | private String key = "ca723c3c8a78962a7751129ae035dc9d"; |
| | | |
| | | @Autowired |
| | | private RestTemplate restTemplate; |
| | | |
| | | @Autowired |
| | | private IGDInterfaceService gdInterfaceService; |
| | | |
| | | @Autowired |
| | | private GDKeyManager gdKeyManager; |
| | | |
| | | /** |
| | | * 执行GET请求:key次数用完时自动切换可用key重试 |
| | | */ |
| | | private String doGetWithRetry(String baseUrl) { |
| | | String lastResp = null; |
| | | int maxAttempts = 5; |
| | | for (int i = 0; i < maxAttempts; i++) { |
| | | String key = gdKeyManager.getKey(); |
| | | String url = baseUrl + (baseUrl.contains("?") ? "&" : "?") + "key=" + key; |
| | | String resp = restTemplate.getForObject(url, String.class); |
| | | lastResp = resp; |
| | | JSONObject jsonObject; |
| | | try { |
| | | jsonObject = JSON.parseObject(resp); |
| | | } catch (Exception e) { |
| | | return resp; |
| | | } |
| | | if (GDKeyManager.isWebKeyExhausted(jsonObject)) { |
| | | gdKeyManager.reportExhausted(key); |
| | | continue; |
| | | } |
| | | return resp; |
| | | } |
| | | return lastResp; |
| | | } |
| | | |
| | | |
| | | /** |
| | |
| | | return map; |
| | | } |
| | | |
| | | String url = "https://restapi.amap.com/v3/geocode/geo?key=" + key + "&output=JSON"; |
| | | String url = "https://restapi.amap.com/v3/geocode/geo?output=JSON"; |
| | | url += "&address=" + province + (ToolUtil.isNotEmpty(city) ? city : "") + (ToolUtil.isNotEmpty(county) ? county : "") + (ToolUtil.isNotEmpty(address) ? address : ""); |
| | | String forObject = restTemplate.getForObject(url, String.class); |
| | | String forObject = doGetWithRetry(url); |
| | | JSONObject jsonObject = JSON.parseObject(forObject); |
| | | String status = jsonObject.getString("status"); |
| | | List<String> list = new ArrayList<>(); |
| | |
| | | |
| | | public Map<String, Object> geocoding(String address){ |
| | | Map<String, Object> map = new HashMap<>(); |
| | | String url = "https://restapi.amap.com/v3/geocode/geo?key=" + key + "&output=JSON&address=" + address; |
| | | String forObject = restTemplate.getForObject(url, String.class); |
| | | String url = "https://restapi.amap.com/v3/geocode/geo?output=JSON&address=" + address; |
| | | String forObject = doGetWithRetry(url); |
| | | JSONObject jsonObject = JSON.parseObject(forObject); |
| | | String status = jsonObject.getString("status"); |
| | | List<String> list = new ArrayList<>(); |
| | |
| | | * @throws Exception |
| | | */ |
| | | public Map<String, String> geocode(String lon, String lan) throws Exception{ |
| | | String url = "https://restapi.amap.com/v3/geocode/regeo?key=" + key + "&location=" + lon + "," + lan; |
| | | String forObject = restTemplate.getForObject(url, String.class); |
| | | String url = "https://restapi.amap.com/v3/geocode/regeo?location=" + lon + "," + lan; |
| | | String forObject = doGetWithRetry(url); |
| | | JSONObject jsonObject = JSON.parseObject(forObject); |
| | | Map<String, String> map = new HashMap<>(); |
| | | |
| New file |
| | |
| | | package com.stylefeng.guns.modular.crossCity.server.impl; |
| | | |
| | | import org.junit.Test; |
| | | |
| | | import static org.junit.Assert.assertFalse; |
| | | import static org.junit.Assert.assertTrue; |
| | | |
| | | public class SiteServiceImplTest { |
| | | |
| | | private static final String SQUARE = "0,0;10,0;10,10;0,10"; |
| | | |
| | | @Test |
| | | public void shouldRecognizePointInsidePolygon() { |
| | | assertTrue(SiteServiceImpl.isPointInElectronicFence("5,5", SQUARE)); |
| | | } |
| | | |
| | | @Test |
| | | public void shouldRecognizePointOutsidePolygon() { |
| | | assertFalse(SiteServiceImpl.isPointInElectronicFence("11,5", SQUARE)); |
| | | } |
| | | |
| | | @Test |
| | | public void shouldIncludePointOnEdgeAndVertex() { |
| | | assertTrue(SiteServiceImpl.isPointInElectronicFence("10,5", SQUARE)); |
| | | assertTrue(SiteServiceImpl.isPointInElectronicFence("0,0", SQUARE)); |
| | | } |
| | | |
| | | @Test |
| | | public void shouldRecognizeConcavePolygon() { |
| | | String concavePolygon = "0,0;10,0;10,10;5,5;0,10"; |
| | | assertTrue(SiteServiceImpl.isPointInElectronicFence("5,4", concavePolygon)); |
| | | assertFalse(SiteServiceImpl.isPointInElectronicFence("5,8", concavePolygon)); |
| | | } |
| | | |
| | | @Test |
| | | public void shouldRecognizeAnyOfMultiplePolygons() { |
| | | String multiplePolygons = SQUARE + "_20,20;30,20;30,30;20,30"; |
| | | assertTrue(SiteServiceImpl.isPointInElectronicFence("25,25", multiplePolygons)); |
| | | assertFalse(SiteServiceImpl.isPointInElectronicFence("15,15", multiplePolygons)); |
| | | } |
| | | |
| | | @Test |
| | | public void shouldHandleWhitespaceAndInvalidCoordinates() { |
| | | assertTrue(SiteServiceImpl.isPointInElectronicFence(" 5 , 5 ", " 0,0 ; 10,0 ; 10,10 ; 0,10 ")); |
| | | assertFalse(SiteServiceImpl.isPointInElectronicFence("invalid", SQUARE)); |
| | | assertFalse(SiteServiceImpl.isPointInElectronicFence("5,5", "0,0;10,0")); |
| | | assertFalse(SiteServiceImpl.isPointInElectronicFence("5,5", "0,0;invalid;10,10")); |
| | | assertFalse(SiteServiceImpl.isPointInElectronicFence(null, SQUARE)); |
| | | assertFalse(SiteServiceImpl.isPointInElectronicFence("5,5", null)); |
| | | } |
| | | } |