diff --git a/neutrino-proxy-admin/src/views/proxy/domainMapping.vue b/neutrino-proxy-admin/src/views/proxy/domainMapping.vue
index 5e71a596..bd8a6c7c 100644
--- a/neutrino-proxy-admin/src/views/proxy/domainMapping.vue
+++ b/neutrino-proxy-admin/src/views/proxy/domainMapping.vue
@@ -7,10 +7,10 @@
-
+
+
@@ -277,6 +277,7 @@ export default {
listQuery: {
current: 1,
size: 10,
+ domain: null,
importance: undefined,
title: undefined,
type: undefined,
diff --git a/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/controller/DomainController.java b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/controller/DomainController.java
new file mode 100644
index 00000000..4e94c5ea
--- /dev/null
+++ b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/controller/DomainController.java
@@ -0,0 +1,128 @@
+package org.dromara.neutrinoproxy.server.controller;
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.dromara.neutrinoproxy.server.base.page.PageInfo;
+import org.dromara.neutrinoproxy.server.base.page.PageQuery;
+import org.dromara.neutrinoproxy.server.base.proxy.ProxyConfig;
+import org.dromara.neutrinoproxy.server.constant.ExceptionConstant;
+import org.dromara.neutrinoproxy.server.constant.NetworkProtocolEnum;
+import org.dromara.neutrinoproxy.server.controller.req.proxy.*;
+import org.dromara.neutrinoproxy.server.controller.res.proxy.*;
+import org.dromara.neutrinoproxy.server.service.DomainMappingService;
+import org.dromara.neutrinoproxy.server.service.PortMappingService;
+import org.dromara.neutrinoproxy.server.util.ParamCheckUtil;
+import org.noear.solon.annotation.*;
+
+/**
+ * @author: aoshiguchen
+ * @date: 2023/4/2
+ */
+@Slf4j
+@Mapping("/domain")
+@Controller
+public class DomainController {
+ @Inject
+ private ProxyConfig proxyConfig;
+
+ @Inject
+ private DomainMappingService domainMappingService;
+
+ @Get
+ @Mapping("/bind-info")
+ public String bindInfo () {
+ return proxyConfig.getServer().getTcp().getDomainName();
+ }
+
+
+ @Get
+ @Mapping("/page")
+ public PageInfo page(PageQuery pageQuery, PortMappingListReq req) {
+ ParamCheckUtil.checkNotNull(pageQuery, "pageQuery");
+ return domainMappingService.page(pageQuery, req);
+ }
+
+ @Post
+ @Mapping("/modify")
+ public PortMappingCreateRes create(PortMappingCreateReq req) {
+ ParamCheckUtil.checkNotNull(req, "req");
+ ParamCheckUtil.checkNotNull(req.getLicenseId(), "licenseId");
+ ParamCheckUtil.checkNotNull(req.getServerPort(), "serverPort");
+ ParamCheckUtil.checkNotNull(req.getClientPort(), "clientPort");
+ ParamCheckUtil.checkNotEmpty(req.getProtocal(), "protocal");
+ ParamCheckUtil.checkMaxLength(req.getDescription(), 50, "描述", "50");
+ ParamCheckUtil.checkBytesDesc(req.getUpLimitRate(), "upLimitRate");
+ ParamCheckUtil.checkBytesDesc(req.getDownLimitRate(), "downLimitRate");
+ if (StringUtils.isBlank(req.getClientIp())) {
+ // 没传客户端ip,默认为127.0.0.1
+ req.setClientIp("127.0.0.1");
+ }
+ NetworkProtocolEnum networkProtocolEnum = NetworkProtocolEnum.of(req.getProtocal());
+ ParamCheckUtil.checkNotNull(networkProtocolEnum, ExceptionConstant.AN_UNSUPPORTED_PROTOCOL, req.getProtocal());
+ if (networkProtocolEnum != NetworkProtocolEnum.HTTP) {
+ // 目前仅HTTP支持绑定域名
+ req.setSubdomain(null);
+ }
+ req.setProtocal(networkProtocolEnum.getDesc());
+ if (null == req.getProxyResponses()) {
+ req.setProxyResponses(0);
+ }
+ if (null == req.getProxyTimeoutMs()) {
+ req.setProxyTimeoutMs(0L);
+ }
+ if (null == req.getSecurityGroupId()) {
+ req.setSecurityGroupId(0);
+ }
+
+ return domainMappingService.create(req);
+ }
+
+ @Get
+ @Mapping("/one/{id}")
+ public DomainMappingDto one(Integer id) {
+ return domainMappingService.one(id);
+ }
+
+ @Post
+ @Mapping("/update/enable-status")
+ public PortMappingUpdateEnableStatusRes updateEnableStatus(PortMappingUpdateEnableStatusReq req) {
+ ParamCheckUtil.checkNotNull(req, "req");
+ ParamCheckUtil.checkNotNull(req.getId(), "id");
+ ParamCheckUtil.checkNotNull(req.getEnable(), "enable");
+
+ return domainMappingService.updateEnableStatus(req);
+ }
+
+ @Post
+ @Mapping("/delete")
+ public void delete(PortMappingDeleteReq req) {
+ ParamCheckUtil.checkNotNull(req, "req");
+ ParamCheckUtil.checkNotNull(req.getId(), "id");
+
+ domainMappingService.delete(req.getId());
+ }
+
+ /**
+ * 绑定安全组
+ * @param req portMappingId和securityGroupId
+ */
+ @Post
+ @Mapping("/bind/security-group")
+ public void bindSecurityGroup(PortMappingBindSecurityGroupReq req) {
+ domainMappingService.portBindSecurityGroup(req.getId(), req.getSecurityGroupId());
+ }
+
+ /**
+ * 安全组解绑
+ * @param id 端口映射Id
+ */
+ @Post
+ @Mapping("/unbind/security-group")
+ public void unbindSecurityGroup(Integer id) {
+ domainMappingService.portUnbindSecurityGroup(id);
+ }
+
+
+
+
+}
diff --git a/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/controller/DomainNameController.java b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/controller/DomainNameController.java
deleted file mode 100644
index 319a4e5b..00000000
--- a/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/controller/DomainNameController.java
+++ /dev/null
@@ -1,27 +0,0 @@
-package org.dromara.neutrinoproxy.server.controller;
-
-import lombok.extern.slf4j.Slf4j;
-import org.dromara.neutrinoproxy.server.base.proxy.ProxyConfig;
-import org.noear.solon.annotation.Controller;
-import org.noear.solon.annotation.Get;
-import org.noear.solon.annotation.Inject;
-import org.noear.solon.annotation.Mapping;
-
-/**
- * @author: aoshiguchen
- * @date: 2023/4/2
- */
-@Slf4j
-@Mapping("/domain")
-@Controller
-public class DomainNameController {
- @Inject
- private ProxyConfig proxyConfig;
-
- @Get
- @Mapping("/bind-info")
- public String bindInfo () {
- return proxyConfig.getServer().getTcp().getDomainName();
- }
-
-}
diff --git a/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/controller/res/proxy/DomainMappingDto.java b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/controller/res/proxy/DomainMappingDto.java
new file mode 100644
index 00000000..ad05d7fe
--- /dev/null
+++ b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/controller/res/proxy/DomainMappingDto.java
@@ -0,0 +1,31 @@
+package org.dromara.neutrinoproxy.server.controller.res.proxy;
+
+
+import lombok.Data;
+import lombok.experimental.Accessors;
+import org.dromara.neutrinoproxy.server.dal.entity.DomainMappingDO;
+
+/**
+ *
+ * @author xiaojie
+ * @date
+ */
+@Accessors(chain = true)
+@Data
+public class DomainMappingDto extends DomainMappingDO {
+
+ /**
+ * licenseId
+ */
+ private String licenseName;
+
+ /**
+ * 用户ID
+ */
+ private Integer userId;
+
+ /**
+ * 用户名称
+ */
+ private String userName;
+}
diff --git a/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/dal/DomainMappingMapper.java b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/dal/DomainMappingMapper.java
new file mode 100644
index 00000000..bf2c44a3
--- /dev/null
+++ b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/dal/DomainMappingMapper.java
@@ -0,0 +1,112 @@
+package org.dromara.neutrinoproxy.server.dal;
+
+import cn.hutool.core.collection.CollectionUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.core.toolkit.StringUtils;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.dromara.neutrinoproxy.server.constant.EnableStatusEnum;
+import org.dromara.neutrinoproxy.server.controller.req.proxy.PortMappingListReq;
+import org.dromara.neutrinoproxy.server.dal.entity.DomainMappingDO;
+import org.dromara.neutrinoproxy.server.dal.entity.DomainMappingDO;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Set;
+
+/**
+ *
+ * @author: aoshiguchen
+ * @date: 2022/8/8
+ */
+@Mapper
+public interface DomainMappingMapper extends BaseMapper {
+
+ default DomainMappingDO findById(Integer id) {
+ return this.selectById(id);
+ }
+
+ default void updateEnableStatus(Integer id, Integer enable, Date updateTime) {
+ this.update(null, new LambdaUpdateWrapper()
+ .eq(DomainMappingDO::getId, id)
+ .set(DomainMappingDO::getEnable, enable)
+ .set(DomainMappingDO::getUpdateTime, updateTime)
+ );
+ }
+
+ default DomainMappingDO findByDomain(String domain, Set excludeIds) {
+ return this.selectOne(new LambdaQueryWrapper()
+ .eq(DomainMappingDO::getDomain, domain)
+ .notIn(!CollectionUtil.isEmpty(excludeIds), DomainMappingDO::getId, excludeIds)
+ .last("limit 1")
+ );
+ }
+
+ /**
+ * 校验域名是否重复
+ * @param subdomain
+ * @return
+ */
+ default Boolean checkRepeatByDomain(String subdomain, Set excludeIds) {
+ return StringUtils.isEmpty(subdomain)? Boolean.FALSE : this.selectCount(new LambdaQueryWrapper()
+ .eq(DomainMappingDO::getDomain, subdomain)
+ .notIn(!CollectionUtil.isEmpty(excludeIds), DomainMappingDO::getId, excludeIds)
+ ).intValue() > 0;
+ }
+
+ default List findEnableListByLicenseId(Integer licenseId) {
+ return this.selectList(new LambdaQueryWrapper()
+ .eq(DomainMappingDO::getLicenseId, licenseId)
+ .eq(DomainMappingDO::getEnable, EnableStatusEnum.ENABLE.getStatus())
+ );
+ }
+
+ default List findListByServerPort(Integer serverPort) {
+ return this.selectList(new LambdaQueryWrapper()
+ .eq(DomainMappingDO::getServerPort, serverPort)
+ );
+ }
+
+ default List findListByLicenseId(Integer licenseId) {
+ return this.selectList(new LambdaQueryWrapper()
+ .eq(DomainMappingDO::getLicenseId, licenseId)
+ );
+ }
+
+ default DomainMappingDO findByLicenseIdAndServerPort(Integer licenseId, Integer serverPort) {
+ return this.selectOne(new LambdaQueryWrapper()
+ .eq(DomainMappingDO::getLicenseId, licenseId)
+ .eq(DomainMappingDO::getServerPort, serverPort)
+ .last("limit 1")
+ );
+ }
+
+ default void updateOnlineStatus(Integer licenseId,Integer serverPort, Integer isOnline, Date updateTime) {
+ this.update(null, new LambdaUpdateWrapper()
+ .eq(DomainMappingDO::getLicenseId, licenseId)
+ .eq(DomainMappingDO::getServerPort, serverPort)
+ .set(DomainMappingDO::getIsOnline, isOnline)
+ .set(DomainMappingDO::getUpdateTime, updateTime)
+ );
+ }
+
+ default void updateOnlineStatus(Integer licenseId, Integer isOnline, Date updateTime) {
+ this.update(null, new LambdaUpdateWrapper()
+ .eq(DomainMappingDO::getLicenseId, licenseId)
+ .set(DomainMappingDO::getIsOnline, isOnline)
+ .set(DomainMappingDO::getUpdateTime, updateTime)
+ );
+ }
+
+ default void updateOnlineStatus(Integer isOnline, Date updateTime) {
+ this.update(null, new LambdaUpdateWrapper()
+ .set(DomainMappingDO::getIsOnline, isOnline)
+ .set(DomainMappingDO::getUpdateTime, updateTime)
+ );
+ }
+
+ List selectDomainMappingByCondition(IPage page, @Param("req") PortMappingListReq req);
+}
diff --git a/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/dal/entity/DomainMappingDO.java b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/dal/entity/DomainMappingDO.java
new file mode 100644
index 00000000..c87ceb1a
--- /dev/null
+++ b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/dal/entity/DomainMappingDO.java
@@ -0,0 +1,110 @@
+/**
+ * Copyright (c) 2022 aoshiguchen
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+package org.dromara.neutrinoproxy.server.dal.entity;
+
+import cn.hutool.core.bean.BeanUtil;
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.ToString;
+import lombok.experimental.Accessors;
+import org.dromara.neutrinoproxy.server.constant.EnableStatusEnum;
+import org.dromara.neutrinoproxy.server.constant.OnlineStatusEnum;
+import org.dromara.neutrinoproxy.server.controller.res.proxy.DomainMappingDto;
+import org.dromara.neutrinoproxy.server.controller.res.proxy.PortMappingListRes;
+
+import java.util.Date;
+
+/**
+ * 端口映射
+ * @author: aoshiguchen
+ * @date: 2022/8/8
+ */
+@ToString
+@Accessors(chain = true)
+@Data
+@TableName("domain_mapping")
+public class DomainMappingDO {
+ @TableId(type = IdType.AUTO)
+ private Integer id;
+ /**
+ * licenseId
+ */
+ private Integer licenseId;
+ /**
+ * 协议
+ */
+ private String protocal;
+ /**
+ * 域名
+ */
+ private String domain;
+ /**
+ * 描述
+ */
+ private String description;
+ /**
+ * 目标地址(licenseId客户端,ip:port)
+ */
+ private String targetPath;
+ /**
+ * 上传限速
+ */
+ private String upLimitRate;
+ /**
+ * 下载限速
+ */
+ private String downLimitRate;
+
+ /**
+ * 是否在线
+ * {@link OnlineStatusEnum}
+ */
+ @TableField(exist=false)
+ private Integer isOnline;
+
+ /**
+ * 启用状态
+ * {@link EnableStatusEnum}
+ */
+ private Integer enable;
+
+ /**
+ * 安全组Id
+ */
+ private Integer securityGroupId = 0; // 设置为null不生效,不知道为啥
+
+ /**
+ * 创建时间
+ */
+ private Date createTime;
+ /**
+ * 更新时间
+ */
+ private Date updateTime;
+
+ public DomainMappingDto toRes() {
+ return (DomainMappingDto) this;
+ }
+}
diff --git a/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/service/DomainMappingService.java b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/service/DomainMappingService.java
new file mode 100644
index 00000000..9d92a1cb
--- /dev/null
+++ b/neutrino-proxy-server/src/main/java/org/dromara/neutrinoproxy/server/service/DomainMappingService.java
@@ -0,0 +1,441 @@
+package org.dromara.neutrinoproxy.server.service;
+
+import cn.hutool.cache.Cache;
+import cn.hutool.cache.CacheUtil;
+import cn.hutool.core.collection.CollectionUtil;
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
+import com.baomidou.mybatisplus.core.toolkit.StringUtils;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.baomidou.mybatisplus.solon.plugins.pagination.Page;
+import com.google.common.collect.Sets;
+import org.apache.ibatis.solon.annotation.Db;
+import org.dromara.neutrinoproxy.server.base.db.DBInitialize;
+import org.dromara.neutrinoproxy.server.base.page.PageInfo;
+import org.dromara.neutrinoproxy.server.base.page.PageQuery;
+import org.dromara.neutrinoproxy.server.base.proxy.ProxyConfig;
+import org.dromara.neutrinoproxy.server.base.rest.SystemContextHolder;
+import org.dromara.neutrinoproxy.server.constant.EnableStatusEnum;
+import org.dromara.neutrinoproxy.server.constant.ExceptionConstant;
+import org.dromara.neutrinoproxy.server.constant.NetworkProtocolEnum;
+import org.dromara.neutrinoproxy.server.constant.OnlineStatusEnum;
+import org.dromara.neutrinoproxy.server.controller.req.proxy.PortMappingCreateReq;
+import org.dromara.neutrinoproxy.server.controller.req.proxy.PortMappingListReq;
+import org.dromara.neutrinoproxy.server.controller.req.proxy.PortMappingUpdateEnableStatusReq;
+import org.dromara.neutrinoproxy.server.controller.req.proxy.PortMappingUpdateReq;
+import org.dromara.neutrinoproxy.server.controller.res.proxy.*;
+import org.dromara.neutrinoproxy.server.dal.DomainMappingMapper;
+import org.dromara.neutrinoproxy.server.dal.LicenseMapper;
+import org.dromara.neutrinoproxy.server.dal.PortPoolMapper;
+import org.dromara.neutrinoproxy.server.dal.UserMapper;
+import org.dromara.neutrinoproxy.server.dal.entity.*;
+import org.dromara.neutrinoproxy.server.service.bo.FlowLimitBO;
+import org.dromara.neutrinoproxy.server.util.ParamCheckUtil;
+import org.dromara.neutrinoproxy.server.util.ProxyUtil;
+import org.dromara.neutrinoproxy.server.util.StringUtil;
+import org.noear.solon.annotation.Component;
+import org.noear.solon.annotation.Init;
+import org.noear.solon.annotation.Inject;
+import org.noear.solon.core.bean.LifecycleBean;
+import org.noear.solon.core.runtime.NativeDetector;
+
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ *
+ * 域名解析 service
+ * author xiaojie
+ */
+@Component
+public class DomainMappingService implements LifecycleBean {
+ @Db
+ private DomainMappingMapper domainMappingMapper;
+ @Db
+ private LicenseMapper licenseMapper;
+ @Db
+ private UserMapper userMapper;
+ @Db
+ private PortPoolMapper portPoolMapper;
+ @Inject
+ private VisitorChannelService visitorChannelService;
+
+ @Inject
+ private PortPoolService portPoolService;
+ @Inject
+ private ProxyConfig proxyConfig;
+ @Inject
+ private DBInitialize dbInitialize;
+ @Inject
+ private LicenseService licenseService;
+
+ /** 端口到安全组Id的映射 */
+ private final Map mappingDomainToSecurityGroupMap = new ConcurrentHashMap<>();
+ /**
+ * 域名 到 域名解析id的映射
+ */
+ private final Cache domainToDomainMappingIdCache = CacheUtil.newLRUCache(500, 1000 * 60 * 10);
+ // 域名解析id到licenseId
+ private final Cache idToLicenseIdCache = CacheUtil.newLRUCache(500, 1000 * 60 * 10);
+ // 流量限制缓存
+ private final Cache flowLimitCache = CacheUtil.newLRUCache(500, 1000 * 60 * 5);
+
+ public PageInfo page(PageQuery pageQuery, PortMappingListReq req) {
+ if (StringUtils.isNotEmpty(req.getDescription())) {
+ //描述字段为模糊查询,在应用层处理,否则sqlite不支持
+ req.setDescription("%" + req.getDescription() + "%");
+ }
+
+ // 协议名称转换
+ if (StringUtils.isNotBlank(req.getProtocal())) {
+ NetworkProtocolEnum networkProtocolEnum = NetworkProtocolEnum.of(req.getProtocal());
+ req.setProtocal(networkProtocolEnum.getDesc());
+ }
+
+ Page page = new Page<>(pageQuery.getCurrent(), pageQuery.getSize());
+ List list = domainMappingMapper.selectDomainMappingByCondition(page, req);
+ List respList = list.stream().map(DomainMappingDO::toRes).collect(Collectors.toList());
+ if (CollectionUtils.isEmpty(list)) {
+ return PageInfo.of(respList, page.getTotal(), pageQuery.getCurrent(), pageQuery.getSize());
+ }
+
+ Set licenseIds = respList.stream().map(DomainMappingDto::getLicenseId).collect(Collectors.toSet());
+ List licenseList = licenseMapper.findByIds(licenseIds);
+ if (CollectionUtil.isEmpty(licenseList)) {
+ return PageInfo.of(respList, page.getTotal(), pageQuery.getCurrent(), pageQuery.getSize());
+ }
+ Set userIds = licenseList.stream().map(LicenseDO::getUserId).collect(Collectors.toSet());
+ List userList = userMapper.findByIds(userIds);
+ Map licenseMap = licenseList.stream().collect(Collectors.toMap(LicenseDO::getId, Function.identity()));
+ Map userMap = userList.stream().collect(Collectors.toMap(UserDO::getId, Function.identity()));
+
+ respList.forEach(item -> {
+ LicenseDO license = licenseMap.get(item.getLicenseId());
+ if (null == license) {
+ return;
+ }
+ item.setLicenseName(license.getName());
+ item.setUserId(license.getUserId());
+ UserDO user = userMap.get(license.getUserId());
+ if (null == user) {
+ return;
+ }
+ item.setUserName(user.getName());
+// if (StrUtil.isNotBlank(proxyConfig.getServer().getTcp().getDomainName()) && StrUtil.isNotBlank(item.getDomain())) {
+// item.setDomain(item.getDomain() + "." + proxyConfig.getServer().getTcp().getDomainName());
+// }
+ if (NetworkProtocolEnum.HTTP.getDesc().equals(item.getProtocal())) {
+ item.setProtocal("HTTP(S)");
+ }
+ });
+ //sorted [userId asc] [licenseId asc] [createTime asc]
+ respList = respList.stream()
+ .filter(e -> null != e.getUserId())
+ .sorted(Comparator.comparing(DomainMappingDto::getUserId).thenComparing(DomainMappingDto::getLicenseId).thenComparing(DomainMappingDto::getCreateTime)).collect(Collectors.toList());
+ return PageInfo.of(respList, page.getTotal(), pageQuery.getCurrent(), pageQuery.getSize());
+ }
+
+ public PortMappingCreateRes modify(DomainMappingDto req) {
+ LicenseDO licenseDO = licenseMapper.findById(req.getLicenseId());
+ ParamCheckUtil.checkNotNull(licenseDO, ExceptionConstant.LICENSE_NOT_EXIST);
+ if (!SystemContextHolder.isAdmin()) {
+ // 临时处理,如果当前用户不是管理员,则操作userId不能为1
+ ParamCheckUtil.checkExpression(!licenseDO.getUserId().equals(1), ExceptionConstant.NO_PERMISSION_VISIT);
+ }
+ PortPoolDO portPoolDO = portPoolMapper.findByPort(req.getDomain());
+ ParamCheckUtil.checkNotNull(portPoolDO, ExceptionConstant.PORT_NOT_EXIST);
+ ParamCheckUtil.checkExpression(null == domainMappingMapper.findByDomain(req.getDomain(), null), ExceptionConstant.PORT_CANNOT_REPEAT_MAPPING, req.getDomain());
+ ParamCheckUtil.checkExpression(!domainMappingMapper.checkRepeatByDomain(req.getDomain(), null), ExceptionConstant.PORT_MAPPING_SUBDONAME_CONNOT_REPEAT);
+
+ Date now = new Date();
+ PortMappingDO portMappingDO = ;
+ portMappingDO.setLicenseId(req.getLicenseId());
+ portMappingDO.setProtocal(req.getProtocal());
+ portMappingDO.setSubdomain(req.getSubdomain());
+ portMappingDO.setServerPort(req.getServerPort());
+ portMappingDO.setClientIp(req.getClientIp());
+ portMappingDO.setClientPort(req.getClientPort());
+ portMappingDO.setUpLimitRate(req.getUpLimitRate());
+ portMappingDO.setDownLimitRate(req.getDownLimitRate());
+ portMappingDO.setProxyResponses(req.getProxyResponses());
+ portMappingDO.setProxyTimeoutMs(req.getProxyTimeoutMs());
+ portMappingDO.setDescription(req.getDescription());
+ portMappingDO.setIsOnline(OnlineStatusEnum.OFFLINE.getStatus());
+ portMappingDO.setEnable(EnableStatusEnum.ENABLE.getStatus());
+ portMappingDO.setCreateTime(now);
+ portMappingDO.setUpdateTime(now);
+ domainMappingMapper.insert(portMappingDO);
+ // 更新VisitorChannel
+ visitorChannelService.addVisitorChannelByPortMapping(portMappingDO);
+ // 更新域名映射
+ if (NetworkProtocolEnum.isHttp(portMappingDO.getProtocal()) && StrUtil.isNotBlank(proxyConfig.getServer().getTcp().getDomainName()) && StrUtil.isNotBlank(portMappingDO.getSubdomain())) {
+ ProxyUtil.setSubdomainToServerPort(portMappingDO.getSubdomain(), portMappingDO.getServerPort());
+ }
+
+ updateMappingDomainToSecurityGroupMap(portMappingDO.getServerPort(), req.getSecurityGroupId());
+
+ // 更新端口到映射的缓存
+ domainToDomainMappingIdCache.put(req.getServerPort(), portMappingDO.getId());
+ // 更新端口映射到licenseId的缓存
+ idToLicenseIdCache.put(portMappingDO.getId(), portMappingDO.getLicenseId());
+ // 刷新流量限制缓存
+ refreshFlowLimitCache(portMappingDO.getId(), portMappingDO.getUpLimitRate(), portMappingDO.getDownLimitRate());
+
+ return new PortMappingCreateRes();
+ }
+
+
+
+ public PortMappingUpdateEnableStatusRes updateEnableStatus(PortMappingUpdateEnableStatusReq req) {
+ PortMappingDO portMappingDO = domainMappingMapper.findById(req.getId());
+ ParamCheckUtil.checkNotNull(portMappingDO, ExceptionConstant.PORT_MAPPING_NOT_EXIST);
+
+ LicenseDO licenseDO = licenseMapper.findById(portMappingDO.getLicenseId());
+ ParamCheckUtil.checkNotNull(licenseDO, ExceptionConstant.LICENSE_NOT_EXIST);
+ if (!SystemContextHolder.isAdmin()) {
+ ParamCheckUtil.checkExpression(!licenseDO.getUserId().equals(1), ExceptionConstant.NO_PERMISSION_VISIT);
+ }
+
+ domainMappingMapper.updateEnableStatus(req.getId(), req.getEnable(), new Date());
+
+ // 更新VisitorChannel
+ portMappingDO.setEnable(req.getEnable());
+ if (EnableStatusEnum.ENABLE == EnableStatusEnum.of(req.getEnable())) {
+ visitorChannelService.addVisitorChannelByPortMapping(portMappingDO);
+ } else {
+ visitorChannelService.removeVisitorChannelByPortMapping(portMappingDO);
+ }
+
+ return new PortMappingUpdateEnableStatusRes();
+ }
+
+ public void delete(Integer id) {
+ PortMappingDO portMappingDO = domainMappingMapper.findById(id);
+ ParamCheckUtil.checkNotNull(portMappingDO, ExceptionConstant.PORT_MAPPING_NOT_EXIST);
+
+ LicenseDO licenseDO = licenseMapper.findById(portMappingDO.getLicenseId());
+ if (null != licenseDO && !SystemContextHolder.isAdmin()) {
+ // 临时处理,如果当前用户不是管理员,则操作userId不能为1
+ ParamCheckUtil.checkExpression(!licenseDO.getUserId().equals(1), ExceptionConstant.NO_PERMISSION_VISIT);
+ }
+
+ domainMappingMapper.deleteById(id);
+
+ // 更新VisitorChannel
+ visitorChannelService.removeVisitorChannelByPortMapping(portMappingDO);
+ // 更新域名映射
+ if (NetworkProtocolEnum.isHttp(portMappingDO.getProtocal()) && StrUtil.isNotBlank(portMappingDO.getSubdomain())) {
+ ProxyUtil.removeSubdomainToServerPort(portMappingDO.getSubdomain());
+ }
+
+ updateMappingDomainToSecurityGroupMap(portMappingDO.getServerPort(), null);
+
+ // 删除id到licenseId的映射
+ idToLicenseIdCache.remove(id);
+ // 删除流量限制缓存
+ flowLimitCache.remove(id);
+ }
+
+ public void portBindSecurityGroup(Integer portMappingId, Integer groupId) {
+ PortMappingDO mappingDO = domainMappingMapper.findById(portMappingId);
+ if (mappingDO == null) {
+ throw new RuntimeException("指定的端口映射不存在");
+ }
+ mappingDO.setSecurityGroupId(groupId);
+ mappingDO.setUpdateTime(new Date());
+ domainMappingMapper.updateById(mappingDO);
+ updateMappingDomainToSecurityGroupMap(mappingDO.getServerPort(), groupId);
+ }
+
+ public void portUnbindSecurityGroup(Integer portMappingId) {
+ PortMappingDO mappingDO = domainMappingMapper.findById(portMappingId);
+ if (mappingDO == null) {
+ throw new RuntimeException("指定的端口映射不存在");
+ }
+ mappingDO.setSecurityGroupId(0);
+ mappingDO.setUpdateTime(new Date());
+ domainMappingMapper.updateById(mappingDO);
+ updateMappingDomainToSecurityGroupMap(mappingDO.getServerPort(), null);
+ }
+
+ /**
+ * 根据license查询可用的端口映射列表
+ *
+ * @param licenseId
+ * @return
+ */
+ public List findEnableListByLicenseId(Integer licenseId) {
+ return domainMappingMapper.findEnableListByLicenseId(licenseId);
+ }
+
+ public Integer getSecurityGroupIdByMappingPort(Integer port) {
+ return mappingDomainToSecurityGroupMap.get(port);
+ }
+
+
+ /**
+ * 服务端项目停止、启动时,更新在线状态为离线
+ */
+ @Init
+ public void init() {
+ // aot 阶段,不初始化
+ if (NativeDetector.isAotRuntime()) {
+ return;
+ }
+ // 服务刚启动,所以默认所有license都是离线状态。解决服务突然关闭,在线状态来不及更新的问题
+ domainMappingMapper.updateOnlineStatus(OnlineStatusEnum.OFFLINE.getStatus(), new Date());
+
+ List allMappingDOList = domainMappingMapper.selectList(Wrappers.lambdaQuery(DomainMappingDO.class));
+ allMappingDOList.forEach(item -> {
+ Integer securityGroupId = item.getSecurityGroupId();
+ if (securityGroupId != null && securityGroupId > 0) {
+ updateMappingDomainToSecurityGroupMap(item.getDomain(), item.getSecurityGroupId());
+ }
+ // 更新端口到映射的缓存
+ domainToDomainMappingIdCache.put(item.getDomain(), item.getId());
+ // 更新端口映射到licenseId的缓存
+ idToLicenseIdCache.put(item.getId(), item.getLicenseId());
+ // 刷新流量限制缓存
+ refreshFlowLimitCache(item.getId(), item.getUpLimitRate(), item.getDownLimitRate());
+ });
+
+ // 未配置域名,则不需要处理域名映射逻辑
+ if (StrUtil.isBlank(proxyConfig.getServer().getTcp().getDomainName())) {
+ return;
+ }
+ List portMappingDOList = allMappingDOList.stream()
+ .filter(item -> NetworkProtocolEnum.HTTP.getDesc().equals(item.getProtocal()) && item.getDomain() != null)
+ .collect(Collectors.toList());
+ if (CollectionUtil.isEmpty(portMappingDOList)) {
+ return;
+ }
+ portMappingDOList.forEach(item -> {
+ if (StrUtil.isBlank(item.getSubdomain())) {
+ return;
+ }
+ ProxyUtil.setSubdomainToServerPort(item.getSubdomain(), item.getServerPort());
+
+ });
+ }
+
+ /**
+ * 刷新流量限制缓存
+ * @param id
+ * @param upLimitRate
+ * @param downLimitRate
+ */
+ private void refreshFlowLimitCache(Integer id, String upLimitRate, String downLimitRate) {
+ if (null == id) {
+ return;
+ }
+ flowLimitCache.put(id, new FlowLimitBO()
+ .setUpLimitRate(StringUtil.parseBytes(upLimitRate))
+ .setDownLimitRate(StringUtil.parseBytes(downLimitRate))
+ );
+ }
+
+ /**
+ * 获取license的流量限制
+ * @param id
+ * @return
+ */
+ public FlowLimitBO getFlowLimit(Integer id) {
+ FlowLimitBO res = flowLimitCache.get(id);
+ if (null == res) {
+ DomainMappingDO domainMappingDO = domainMappingMapper.findById(id);
+ if (null != domainMappingDO) {
+ refreshFlowLimitCache(id, domainMappingDO.getUpLimitRate(), domainMappingDO.getDownLimitRate());
+ res = flowLimitCache.get(id);
+ }
+ }
+ return res;
+ }
+
+ public Integer getDomainMappingIdByServerPort(Integer serverPort) {
+ if (null == serverPort) {
+ return null;
+ }
+ Integer id = domainToDomainMappingIdCache.get(serverPort);
+ if (null != id) {
+ return id;
+ }
+ List domainMappingDOList = domainMappingMapper.findListByServerPort(serverPort);
+ // 不存在 或者 有多条记录,都不处理
+ if (CollectionUtils.isEmpty(domainMappingDOList) || domainMappingDOList.size() > 1) {
+ return null;
+ }
+ id = domainMappingDOList.get(0).getId();
+ domainToDomainMappingIdCache.put(serverPort, id);
+ return id;
+ }
+
+ public Integer getLicenseIdById(Integer id) {
+ Integer licenseId = idToLicenseIdCache.get(id);
+ if (null == licenseId) {
+ DomainMappingDO domainMappingDO = domainMappingMapper.findById(id);
+ if (null != domainMappingDO) {
+ licenseId = domainMappingDO.getLicenseId();
+ idToLicenseIdCache.put(id, licenseId);
+ }
+ }
+ return licenseId;
+ }
+
+ public FlowLimitBO getFlowLimitByServerPort(Integer serverPort) {
+ Integer id = getPortMappingIdByServerPort(serverPort);
+ if (null == id) {
+ return null;
+ }
+ FlowLimitBO res = getFlowLimit(id);
+ if (null == res || (null == res.getUpLimitRate() && null == res.getDownLimitRate())) {
+ Integer licenseId = getLicenseIdById(id);
+ if (null != licenseId) {
+ res = licenseService.getFlowLimit(licenseId);
+ }
+ }
+ return res;
+ }
+
+ private void updateMappingDomainToSecurityGroupMap(Integer serverPort, Integer securityGroupId) {
+ if (securityGroupId == null || securityGroupId == 0) {
+ mappingDomainToSecurityGroupMap.remove(serverPort);
+ return;
+ }
+ mappingDomainToSecurityGroupMap.put(serverPort, securityGroupId);
+ }
+
+ @Override
+ public void start() throws Throwable {
+
+ }
+
+ /**
+ * 服务端项目停止、启动时,更新在线状态为离线
+ */
+ @Override
+ public void stop() throws Throwable {
+ domainMappingMapper.updateOnlineStatus(OnlineStatusEnum.OFFLINE.getStatus(), new Date());
+ }
+
+ public DomainMappingDto one(Integer id) {
+ DomainMappingDO domainMappingDO = domainMappingMapper.findById(id);
+ if (null == domainMappingDO) {
+ return null;
+ }
+ DomainMappingDto res = (DomainMappingDto)domainMappingDO;
+
+ LicenseDO license = licenseMapper.findById(domainMappingDO.getLicenseId());
+ if (null != license) {
+ res.setLicenseName(license.getName());
+ res.setUserId(license.getUserId());
+ UserDO user = userMapper.findById(license.getUserId());
+ if (null != user) {
+ res.setUserName(user.getName());
+ }
+ }
+ return res;
+ }
+}
diff --git a/neutrino-proxy-server/src/main/resources/mapper/DomainMappingMapper.xml b/neutrino-proxy-server/src/main/resources/mapper/DomainMappingMapper.xml
new file mode 100644
index 00000000..8d169bdd
--- /dev/null
+++ b/neutrino-proxy-server/src/main/resources/mapper/DomainMappingMapper.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/neutrino-proxy-server/src/main/resources/mapper/PortMappingMapper.xml b/neutrino-proxy-server/src/main/resources/mapper/PortMappingMapper.xml
index 22348ef9..72b473d0 100644
--- a/neutrino-proxy-server/src/main/resources/mapper/PortMappingMapper.xml
+++ b/neutrino-proxy-server/src/main/resources/mapper/PortMappingMapper.xml
@@ -2,7 +2,7 @@
-