puzhibing
5 天以前 1f9454af3356f628c25c3e2e492869163731da56
修改bug
13个文件已添加
7个文件已修改
1598 ■■■■■ 已修改文件
ManagementFBTravel/guns-admin/src/main/java/com/stylefeng/guns/modular/system/controller/general/TGaodeAccountController.java 164 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ManagementFBTravel/guns-admin/src/main/java/com/stylefeng/guns/modular/system/dao/GDKeyMapper.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ManagementFBTravel/guns-admin/src/main/java/com/stylefeng/guns/modular/system/dao/TGaodeAccountMapper.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ManagementFBTravel/guns-admin/src/main/java/com/stylefeng/guns/modular/system/dao/mapping/TGaodeAccountMapper.xml 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ManagementFBTravel/guns-admin/src/main/java/com/stylefeng/guns/modular/system/model/TGaodeAccount.java 153 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ManagementFBTravel/guns-admin/src/main/java/com/stylefeng/guns/modular/system/service/ITGaodeAccountService.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ManagementFBTravel/guns-admin/src/main/java/com/stylefeng/guns/modular/system/service/impl/TGaodeAccountServiceImpl.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ManagementFBTravel/guns-admin/src/main/java/com/stylefeng/guns/modular/system/util/GDKeyManager.java 349 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ManagementFBTravel/guns-admin/src/main/java/com/stylefeng/guns/modular/system/util/GDMapElectricFenceUtil.java 136 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ManagementFBTravel/guns-admin/src/main/java/com/stylefeng/guns/modular/system/util/GDMapGeocodingUtil.java 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ManagementFBTravel/guns-admin/src/main/java/com/stylefeng/guns/modular/system/util/GaoDeMapUtil.java 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ManagementFBTravel/guns-admin/src/main/resources/application.yml 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ManagementFBTravel/guns-admin/src/main/webapp/WEB-INF/view/home.html 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ManagementFBTravel/guns-admin/src/main/webapp/WEB-INF/view/system/tGaodeAccount/tGaodeAccount.html 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ManagementFBTravel/guns-admin/src/main/webapp/WEB-INF/view/system/tGaodeAccount/tGaodeAccount_add.html 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ManagementFBTravel/guns-admin/src/main/webapp/WEB-INF/view/system/tGaodeAccount/tGaodeAccount_edit.html 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ManagementFBTravel/guns-admin/src/main/webapp/WEB-INF/view/system/tSite/tSite_add.html 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ManagementFBTravel/guns-admin/src/main/webapp/WEB-INF/view/system/tSite/tSite_edit.html 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ManagementFBTravel/guns-admin/src/main/webapp/static/modular/system/tGaodeAccount/tGaodeAccount.js 257 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ManagementFBTravel/guns-admin/src/main/webapp/static/modular/system/tGaodeAccount/tGaodeAccount_info.js 133 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ManagementFBTravel/guns-admin/src/main/java/com/stylefeng/guns/modular/system/controller/general/TGaodeAccountController.java
New file
@@ -0,0 +1,164 @@
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;
    }
}
ManagementFBTravel/guns-admin/src/main/java/com/stylefeng/guns/modular/system/dao/GDKeyMapper.java
New file
@@ -0,0 +1,31 @@
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);
}
ManagementFBTravel/guns-admin/src/main/java/com/stylefeng/guns/modular/system/dao/TGaodeAccountMapper.java
New file
@@ -0,0 +1,28 @@
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);
}
ManagementFBTravel/guns-admin/src/main/java/com/stylefeng/guns/modular/system/dao/mapping/TGaodeAccountMapper.xml
New file
@@ -0,0 +1,41 @@
<?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>
ManagementFBTravel/guns-admin/src/main/java/com/stylefeng/guns/modular/system/model/TGaodeAccount.java
New file
@@ -0,0 +1,153 @@
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 +
        "}";
    }
}
ManagementFBTravel/guns-admin/src/main/java/com/stylefeng/guns/modular/system/service/ITGaodeAccountService.java
New file
@@ -0,0 +1,24 @@
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);
}
ManagementFBTravel/guns-admin/src/main/java/com/stylefeng/guns/modular/system/service/impl/TGaodeAccountServiceImpl.java
New file
@@ -0,0 +1,28 @@
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);
    }
}
ManagementFBTravel/guns-admin/src/main/java/com/stylefeng/guns/modular/system/util/GDKeyManager.java
New file
@@ -0,0 +1,349 @@
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;
        }
    }
}
ManagementFBTravel/guns-admin/src/main/java/com/stylefeng/guns/modular/system/util/GDMapElectricFenceUtil.java
@@ -34,9 +34,98 @@
    @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
@@ -82,14 +171,7 @@
            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");
@@ -132,8 +214,8 @@
     * @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");
@@ -149,8 +231,8 @@
     * 查询以创建的所有电子围栏
     */
    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");
@@ -186,8 +268,8 @@
        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");
@@ -275,14 +357,8 @@
        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", "更新电子围栏");
@@ -304,8 +380,8 @@
            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");
@@ -334,7 +410,7 @@
     * @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", "删除电子围栏");
@@ -351,9 +427,9 @@
     * @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");
@@ -381,9 +457,9 @@
     * @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");
ManagementFBTravel/guns-admin/src/main/java/com/stylefeng/guns/modular/system/util/GDMapGeocodingUtil.java
@@ -20,13 +20,40 @@
@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;
    }
    /**
@@ -55,9 +82,9 @@
            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<>();
@@ -80,8 +107,8 @@
    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<>();
@@ -110,8 +137,8 @@
     * @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<>();
ManagementFBTravel/guns-admin/src/main/java/com/stylefeng/guns/modular/system/util/GaoDeMapUtil.java
@@ -1,6 +1,8 @@
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;
@@ -25,35 +27,57 @@
    }
    /**
     * 获取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;
    }
ManagementFBTravel/guns-admin/src/main/resources/application.yml
@@ -66,7 +66,7 @@
#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 #用来搜集数据库的所有表
ManagementFBTravel/guns-admin/src/main/webapp/WEB-INF/view/home.html
@@ -256,8 +256,13 @@
        </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>
ManagementFBTravel/guns-admin/src/main/webapp/WEB-INF/view/system/tGaodeAccount/tGaodeAccount.html
New file
@@ -0,0 +1,58 @@
@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>
@}
ManagementFBTravel/guns-admin/src/main/webapp/WEB-INF/view/system/tGaodeAccount/tGaodeAccount_add.html
New file
@@ -0,0 +1,32 @@
@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>
@}
ManagementFBTravel/guns-admin/src/main/webapp/WEB-INF/view/system/tGaodeAccount/tGaodeAccount_edit.html
New file
@@ -0,0 +1,32 @@
@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>
@}
ManagementFBTravel/guns-admin/src/main/webapp/WEB-INF/view/system/tSite/tSite_add.html
@@ -164,7 +164,12 @@
    </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>
@}
ManagementFBTravel/guns-admin/src/main/webapp/WEB-INF/view/system/tSite/tSite_edit.html
@@ -187,7 +187,12 @@
    </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>
@}
ManagementFBTravel/guns-admin/src/main/webapp/static/modular/system/tGaodeAccount/tGaodeAccount.js
New file
@@ -0,0 +1,257 @@
/**
 * 高德地图账号管理初始化
 */
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();
});
ManagementFBTravel/guns-admin/src/main/webapp/static/modular/system/tGaodeAccount/tGaodeAccount_info.js
New file
@@ -0,0 +1,133 @@
/**
 * 初始化高德地图账号详情对话框
 */
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);
});