diff --git a/docker-compose.yaml b/docker-compose.yaml
new file mode 100644
index 0000000..d5143a9
--- /dev/null
+++ b/docker-compose.yaml
@@ -0,0 +1,80 @@
+version: '3.8'
+
+services:
+ # 微信机器人管理系统
+ wechat-robot:
+ build:
+ context: .
+ dockerfile: Dockerfile
+ container_name: wechat-robot
+ restart: unless-stopped
+ ports:
+ - "8080:8080" # 应用端口
+ volumes:
+ - ./configs:/app/configs # 配置文件
+ - ./data:/app/data # 数据文件
+ - ./logs:/app/logs # 日志文件
+ - /var/run/docker.sock:/var/run/docker.sock # Docker socket 用于容器管理
+ # 添加 docker 组的 GID 到容器,999 是常见的 docker 组 GID,但在不同系统可能不同
+ group_add:
+ - "999" # 请确认您系统上的 docker 组 GID
+ environment:
+ - APP_ENV=production
+ - TZ=Asia/Shanghai
+ - DOCKER_HOST=unix:///var/run/docker.sock
+ depends_on:
+ - postgres
+ - redis
+ networks:
+ - wechat-network
+
+ # PostgreSQL 数据库
+ postgres:
+ image: postgres:14-alpine
+ container_name: wechat-postgres
+ restart: unless-stopped
+ environment:
+ POSTGRES_USER: postgres
+ POSTGRES_PASSWORD: password # 生产环境请修改此密码
+ POSTGRES_DB: wechat_demo
+ volumes:
+ - postgres-data:/var/lib/postgresql/data
+ ports:
+ - "5432:5432"
+ networks:
+ - wechat-network
+
+ # Redis 服务
+ redis:
+ image: redis:7-alpine
+ container_name: wechat-redis
+ restart: unless-stopped
+ command: redis-server --requirepass pGhQKwj7DE7FbFL1 # 与配置中的密码一致
+ volumes:
+ - redis-data:/data
+ ports:
+ - "6379:6379"
+ networks:
+ - wechat-network
+
+ # PGAdmin(可选)数据库管理工具
+ pgadmin:
+ image: dpage/pgadmin4
+ container_name: wechat-pgadmin
+ environment:
+ PGADMIN_DEFAULT_EMAIL: admin@example.com
+ PGADMIN_DEFAULT_PASSWORD: admin # 生产环境请修改此密码
+ ports:
+ - "5050:80"
+ depends_on:
+ - postgres
+ networks:
+ - wechat-network
+
+volumes:
+ postgres-data:
+ redis-data:
+
+networks:
+ wechat-network:
+ driver: bridge
\ No newline at end of file
diff --git a/internal/docker/container.go b/internal/docker/container.go
index a9055bc..d308a94 100644
--- a/internal/docker/container.go
+++ b/internal/docker/container.go
@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"io"
+ "net"
"os"
"strconv"
"strings"
@@ -36,6 +37,16 @@ func CreateContainer(ctx context.Context, cfg *config.DockerConfig, name string,
// 检测是否在容器内运行
inContainer := isRunningInContainer()
+ // 如果在容器内运行,获取当前容器的网络设置
+ var currentNetworkName string
+ if inContainer {
+ currentNetworkName = getCurrentContainerNetwork(ctx, cli)
+ if currentNetworkName != "" {
+ // 使用发现的网络替代配置中的网络
+ cfg.Network = currentNetworkName
+ }
+ }
+
// 端口映射 - 将容器的9000端口映射到主机的端口
hostPort := nat.Port("9000/tcp")
exposedPorts := nat.PortSet{
@@ -94,8 +105,31 @@ func CreateContainer(ctx context.Context, cfg *config.DockerConfig, name string,
// 设置网络配置
networkingConfig := &network.NetworkingConfig{}
if cfg.Network != "" {
+ // 首先检查网络类型,只在用户自定义网络上分配固定IP
+ isUserNetwork := false
+ if cfg.Network != "bridge" && cfg.Network != "host" && cfg.Network != "none" {
+ // 检查网络是否存在
+ _, err := cli.NetworkInspect(ctx, cfg.Network, types.NetworkInspectOptions{})
+ if err == nil {
+ isUserNetwork = true
+ }
+ }
+
endpointsConfig := make(map[string]*network.EndpointSettings)
- endpointsConfig[cfg.Network] = &network.EndpointSettings{}
+ endpointSettings := &network.EndpointSettings{}
+
+ // 只在用户自定义网络上尝试分配固定IP
+ if isUserNetwork {
+ // 自动为容器分配一个递增的IP地址
+ nextIP, err := getNextAvailableIPInNetwork(ctx, cli, cfg.Network)
+ if err == nil && nextIP != "" {
+ endpointSettings.IPAMConfig = &network.EndpointIPAMConfig{
+ IPv4Address: nextIP,
+ }
+ }
+ }
+
+ endpointsConfig[cfg.Network] = endpointSettings
networkingConfig.EndpointsConfig = endpointsConfig
}
@@ -116,11 +150,341 @@ func CreateContainer(ctx context.Context, cfg *config.DockerConfig, name string,
return resp.ID, nil
}
+// getNextAvailableIPInNetwork 获取网络中下一个可用的IP地址
+func getNextAvailableIPInNetwork(ctx context.Context, cli *client.Client, networkName string) (string, error) {
+ // 获取网络信息
+ networkResource, err := cli.NetworkInspect(ctx, networkName, types.NetworkInspectOptions{})
+ if err != nil {
+ return "", fmt.Errorf("无法检查网络: %w", err)
+ }
+
+ // 确认网络类型是否为用户自定义网络,并获取子网信息
+ if networkResource.IPAM.Config == nil || len(networkResource.IPAM.Config) == 0 {
+ return "", fmt.Errorf("网络没有IPAM配置")
+ }
+
+ // 获取网络子网
+ subnet := networkResource.IPAM.Config[0].Subnet
+ if subnet == "" {
+ return "", fmt.Errorf("网络子网未定义")
+ }
+
+ // 解析子网
+ _, ipNet, err := net.ParseCIDR(subnet)
+ if err != nil {
+ return "", fmt.Errorf("无法解析子网CIDR: %w", err)
+ }
+
+ // 获取网络中的所有容器
+ containers, err := cli.ContainerList(ctx, types.ContainerListOptions{All: true})
+ if err != nil {
+ return "", fmt.Errorf("无法列出容器: %w", err)
+ }
+
+ // 获取已使用的IP地址
+ usedIPs := make(map[string]bool)
+ for _, c := range containers {
+ for name, network := range c.NetworkSettings.Networks {
+ if name == networkName && network.IPAddress != "" {
+ usedIPs[network.IPAddress] = true
+ }
+ }
+ }
+
+ // 获取网络中的所有IP地址,查找最大IP
+ var maxIP net.IP
+ for ip := range usedIPs {
+ ipAddr := net.ParseIP(ip)
+ if ipAddr == nil {
+ continue
+ }
+
+ // 确保IP在子网内
+ if !ipNet.Contains(ipAddr) {
+ continue
+ }
+
+ // 更新最大IP
+ if maxIP == nil || compareIPs(ipAddr, maxIP) > 0 {
+ maxIP = ipAddr
+ }
+ }
+
+ // 如果网络中没有容器,使用网关地址的下一个IP
+ if maxIP == nil {
+ // 获取网关IP
+ gateway := networkResource.IPAM.Config[0].Gateway
+ if gateway == "" {
+ // 如果没有定义网关,使用子网的第一个可用IP
+ ip := ipNet.IP.To4()
+ if ip == nil {
+ return "", fmt.Errorf("无法获取IPv4地址")
+ }
+
+ // 通常第一个可用IP是网关后的地址(x.x.x.1 + 1 = x.x.x.2)
+ ip[3]++
+ maxIP = ip
+ } else {
+ gwIP := net.ParseIP(gateway)
+ if gwIP == nil {
+ return "", fmt.Errorf("无效的网关地址")
+ }
+
+ // 使用网关IP的下一个地址
+ maxIP = gwIP.To4()
+ if maxIP == nil {
+ return "", fmt.Errorf("无法获取IPv4网关地址")
+ }
+ maxIP[3]++
+ }
+ } else {
+ // 如果有容器,则使用最大IP + 1
+ maxIP = incrementIP(maxIP)
+ }
+
+ // 确保新IP仍然在子网内
+ if !ipNet.Contains(maxIP) {
+ return "", fmt.Errorf("无法分配下一个IP,已达到子网范围上限")
+ }
+
+ return maxIP.String(), nil
+}
+
+// compareIPs 比较两个IP地址的大小
+func compareIPs(a, b net.IP) int {
+ // 转为IPv4格式
+ a4 := a.To4()
+ b4 := b.To4()
+
+ if a4 == nil || b4 == nil {
+ // 如果任一IP不是IPv4,则无法比较
+ return 0
+ }
+
+ // 逐字节比较
+ for i := 0; i < 4; i++ {
+ if a4[i] < b4[i] {
+ return -1
+ } else if a4[i] > b4[i] {
+ return 1
+ }
+ }
+ return 0
+}
+
+// incrementIP 将IP地址递增1
+func incrementIP(ip net.IP) net.IP {
+ // 创建IP的副本
+ newIP := make(net.IP, len(ip))
+ copy(newIP, ip)
+
+ // 确保使用IPv4格式
+ ipv4 := newIP.To4()
+ if ipv4 == nil {
+ return newIP
+ }
+
+ // 递增最后一个字节
+ for i := 3; i >= 0; i-- {
+ ipv4[i]++
+ if ipv4[i] > 0 {
+ // 如果没有溢出,退出循环
+ break
+ }
+ // 如果溢出(变成0),继续递增上一个字节
+ }
+
+ return ipv4
+}
+
+// getNextAvailableIP 获取网络中下一个可用的IP地址
+func getNextAvailableIP(ctx context.Context, cli *client.Client, networkName string) (string, error) {
+ // 获取网络信息
+ networkResource, err := cli.NetworkInspect(ctx, networkName, types.NetworkInspectOptions{})
+ if err != nil {
+ return "", fmt.Errorf("无法检查网络: %w", err)
+ }
+
+ // 确认网络类型是否为bridge,并获取子网信息
+ if networkResource.IPAM.Config == nil || len(networkResource.IPAM.Config) == 0 {
+ return "", fmt.Errorf("网络没有IPAM配置")
+ }
+
+ // 获取网络子网
+ subnet := networkResource.IPAM.Config[0].Subnet
+ if subnet == "" {
+ return "", fmt.Errorf("网络子网未定义")
+ }
+
+ // 解析子网
+ _, ipNet, err := net.ParseCIDR(subnet)
+ if err != nil {
+ return "", fmt.Errorf("无法解析子网CIDR: %w", err)
+ }
+
+ // 获取现有容器的IP地址
+ existingIPs := make(map[string]bool)
+ containers, err := cli.ContainerList(ctx, types.ContainerListOptions{All: true})
+ if err != nil {
+ return "", fmt.Errorf("无法列出容器: %w", err)
+ }
+
+ // 收集所有容器在这个网络中使用的IP
+ for _, c := range containers {
+ if c.NetworkSettings != nil {
+ for name, network := range c.NetworkSettings.Networks {
+ if name == networkName && network.IPAddress != "" {
+ existingIPs[network.IPAddress] = true
+ }
+ }
+ }
+ }
+
+ // 确定最大IP地址
+ var maxIP net.IP
+ for ipStr := range existingIPs {
+ ip := net.ParseIP(ipStr)
+ if ip == nil {
+ continue
+ }
+
+ if !ipNet.Contains(ip) {
+ continue // IP不在子网内
+ }
+
+ if maxIP == nil || compareIPs(ip, maxIP) > 0 {
+ maxIP = ip
+ }
+ }
+
+ // 如果没有找到最大IP,使用子网的第一个可用IP(通常是x.x.x.2,因为x.x.x.1通常是网关)
+ if maxIP == nil {
+ firstIP := getFirstUsableIP(ipNet)
+ if firstIP != nil {
+ return firstIP.String(), nil
+ }
+ return "", fmt.Errorf("无法确定子网的第一个可用IP")
+ }
+
+ // 递增最大IP地址
+ nextIP := getNextIP(maxIP)
+
+ // 确保新IP仍然在子网内
+ if !ipNet.Contains(nextIP) {
+ return "", fmt.Errorf("无法分配下一个IP,已达到子网范围上限")
+ }
+
+ return nextIP.String(), nil
+}
+
+// getNextIP 获取下一个IP地址
+func getNextIP(ip net.IP) net.IP {
+ // 创建IP的副本
+ nextIP := make(net.IP, len(ip))
+ copy(nextIP, ip)
+
+ // 确保使用IPv4格式
+ nextIP = nextIP.To4()
+ if nextIP == nil {
+ return nil
+ }
+
+ // 从最后一个字节开始递增
+ for i := len(nextIP) - 1; i >= 0; i-- {
+ if nextIP[i] < 255 {
+ nextIP[i]++
+ break
+ }
+ nextIP[i] = 0 // 进位
+ }
+
+ return nextIP
+}
+
+// getFirstUsableIP 获取子网中的第一个可用IP
+func getFirstUsableIP(ipNet *net.IPNet) net.IP {
+ // 子网的第一个地址通常是网络地址,不可用
+ // 第二个地址通常是网关地址,也可能不可用
+ // 因此我们从第三个地址开始(通常是x.x.x.2)
+
+ // 获取子网的起始IP
+ ip := ipNet.IP.To4()
+ if ip == nil {
+ return nil
+ }
+
+ // 创建副本
+ firstIP := make(net.IP, len(ip))
+ copy(firstIP, ip)
+
+ // 递增到第三个地址
+ thirdOctet := int(firstIP[2])
+ fourthOctet := int(firstIP[3]) + 2 // 通常是x.x.x.2
+
+ // 处理进位
+ if fourthOctet > 255 {
+ fourthOctet = fourthOctet % 256
+ thirdOctet++
+ if thirdOctet > 255 {
+ // 溢出处理(通常不会发生在正常子网中)
+ return nil
+ }
+ }
+
+ firstIP[2] = byte(thirdOctet)
+ firstIP[3] = byte(fourthOctet)
+
+ return firstIP
+}
+
+// getCurrentContainerNetwork 获取当前容器所在的网络
+func getCurrentContainerNetwork(ctx context.Context, cli *client.Client) string {
+ // 获取当前容器的ID
+ hostname, err := os.Hostname()
+ if err != nil {
+ return ""
+ }
+
+ // 查询容器信息
+ containerInfo, err := cli.ContainerInspect(ctx, hostname)
+ if err != nil {
+ return ""
+ }
+
+ // 遍历容器的网络配置,获取第一个非空网络名称
+ for networkName := range containerInfo.NetworkSettings.Networks {
+ if networkName != "bridge" && networkName != "host" && networkName != "none" {
+ return networkName // 返回自定义网络名称
+ }
+ }
+
+ // 如果只有默认网络,则返回第一个网络名称
+ for networkName := range containerInfo.NetworkSettings.Networks {
+ return networkName // 返回第一个找到的网络名称
+ }
+
+ return ""
+}
+
// isRunningInContainer 检测当前程序是否在容器内运行
func isRunningInContainer() bool {
// 检查环境变量 IS_DOCKER
isDockEnv := os.Getenv("IS_DOCKER")
- return isDockEnv == "true" || isDockEnv == "1" || isDockEnv == "yes"
+ if isDockEnv == "true" || isDockEnv == "1" || isDockEnv == "yes" {
+ return true
+ }
+
+ // 通过检查cgroup文件判断
+ if _, err := os.Stat("/.dockerenv"); err == nil {
+ return true
+ }
+
+ // 检查进程的cgroup信息
+ if data, err := os.ReadFile("/proc/1/cgroup"); err == nil {
+ return strings.Contains(string(data), "docker")
+ }
+
+ return false
}
// findMaxPortForImage 查找指定镜像的容器已使用的最大端口号
@@ -286,7 +650,7 @@ func GetContainerIP(ctx context.Context, containerID string, cfg *config.DockerC
// GetContainerHost 获取容器的访问地址(格式:ip:port)
func GetContainerHost(ctx context.Context, containerID string, cfg *config.DockerConfig) (string, error) {
- // 如果在Docker环境中运行,需要获取容器真实IP
+ // 如果在Docker环境中运行,需要获取容器真实IP或容器名
if isRunningInContainer() {
cli := GetClient()
@@ -295,16 +659,26 @@ func GetContainerHost(ctx context.Context, containerID string, cfg *config.Docke
return "", fmt.Errorf("无法检查容器: %w", err)
}
- // 遍历网络配置找到IP地址
- for networkName, network := range inspect.NetworkSettings.Networks {
+ // 使用容器名作为主机名,用于内部网络通信
+ containerName := inspect.Name
+ if containerName != "" {
+ // 移除前导斜杠
+ if strings.HasPrefix(containerName, "/") {
+ containerName = containerName[1:]
+ }
+ // 使用容器名:9000作为内部访问地址
+ return containerName + ":9000", nil
+ }
+
+ // 备选方案:尝试使用容器IP
+ for _, network := range inspect.NetworkSettings.Networks {
if network.IPAddress != "" {
// 容器内部访问使用IP:9000格式
return network.IPAddress + ":9000", nil
}
- // 防止在没有获取到IP的情况下继续循环
- fmt.Printf("Network %s has no IP address\n", networkName)
}
- return "localhost:9000", fmt.Errorf("未找到容器IP地址")
+
+ return "localhost:9000", fmt.Errorf("未找到容器名或IP地址")
} else {
// 如果不是在Docker环境中运行,需要获取端口映射
cli := GetClient()
diff --git a/internal/view/robot/show.html b/internal/view/robot/show.html
index a796832..0476cf1 100644
--- a/internal/view/robot/show.html
+++ b/internal/view/robot/show.html
@@ -19,7 +19,8 @@
登录
-