基于GoFrame的网络管理系统完整技术方案(含PPPoE支持)

一、系统概述

本系统基于GoFrame框架开发,支持PPTP、L2TP、802.1X、PPPoE等主流拨号协议的认证、计费、在线状态管理及区域/套餐管控。核心功能包括:

  • 多协议认证(PPTP/L2TP/802.1X/PPPoE)

  • 用户计费与流量统计

  • 实时会话跟踪与在线状态管理

  • 区域/套餐分级管控

  • NAS设备监控与自动断网

二、数据库设计(MySQL)

完整表结构包含核心业务表及多协议扩展字段,支持全生命周期会话跟踪。



1. 表结构清单

表名 字段 说明
region region_id(主键)、parent_region_id、name、level、description 区域管理(省市区/自定义层级)
user user_id(主键)、username、password_hash、region_id、status、phone、current_session_id、last_online_time 用户基础信息(关联区域,新增当前会话ID和最后在线时间)
package package_id(主键)、region_id、name、price、bandwidth、valid_days、traffic_limit 区域下套餐配置(支持按区域限制带宽/流量)
order order_id(主键)、user_id、package_id、amount、pay_time、effect_time、expire_time、status 订单记录(支付状态/生效时间)
nas_device nas_id(主键)、nas_name、nas_ip、shared_secret、protocol_type(pptp/l2tp/802.1x/pppoe)、pptp_max_connections、l2tp_ipsec_psk、pppoe_max_connections、pppoe_ac_name、pppoe_interface NAS设备信息(支持多协议,新增PPPoE专用字段)
session session_id(主键)、user_id、nas_ip、tunnel_id(PPTP/L2TP隧道ID)、pppoe_ac_name、pppoe_interface、start_time、end_time、bytes_in、bytes_out、status(0=在线,1=离线,2=异常断开,3=手动封禁)、last_activity、connect_reason、disconnect_reason、protocol 会话记录(含多协议隧道ID、PPPoE专用字段及全生命周期状态)


2. 表关系

  • user.region_id→ region.region_id(用户所属区域)。

  • package.region_id→ region.region_id(套餐所属区域)。

  • order.user_id→ user.user_id(订单关联用户)。

  • session.user_id→ user.user_id(会话关联用户)。

  • session.nas_ip→ nas_device.nas_ip(会话关联NAS设备)。



3.SQL创建脚本

-- 创建数据库
CREATE DATABASE IF NOT EXISTS radius_system DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE radius_system;

-- 区域表
CREATE TABLE IF NOT EXISTS `region` (
  `region_id` INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '区域ID',
  `parent_region_id` INT UNSIGNED DEFAULT 0 COMMENT '父区域ID(0为根区域)',
  `name` VARCHAR(64) NOT NULL COMMENT '区域名称(如"北京市")',
  `level` TINYINT UNSIGNED NOT NULL COMMENT '区域层级(1=省,2=市,3=区)',
  `description` VARCHAR(255) DEFAULT NULL COMMENT '描述',
  PRIMARY KEY (`region_id`),
  INDEX `idx_parent` (`parent_region_id`),
  INDEX `idx_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT '区域管理表';

-- 用户表
CREATE TABLE IF NOT EXISTS `user` (
  `user_id` INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '用户ID',
  `username` VARCHAR(32) NOT NULL UNIQUE COMMENT '用户名(唯一)',
  `password_hash` VARCHAR(64) NOT NULL COMMENT '密码哈希(bcrypt)',
  `region_id` INT UNSIGNED NOT NULL COMMENT '所属区域ID',
  `status` TINYINT UNSIGNED NOT NULL DEFAULT 1 COMMENT '用户状态(0=禁用,1=启用)',
  `phone` VARCHAR(16) DEFAULT NULL COMMENT '联系电话',
  `current_session_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '当前在线会话ID(NULL=未在线)',
  `last_online_time` DATETIME DEFAULT NULL COMMENT '最后一次在线结束时间',
  PRIMARY KEY (`user_id`),
  INDEX `idx_region` (`region_id`),
  INDEX `idx_status` (`status`),
  INDEX `idx_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT '用户信息表';

-- 套餐表
CREATE TABLE IF NOT EXISTS `package` (
  `package_id` INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '套餐ID',
  `region_id` INT UNSIGNED NOT NULL COMMENT '所属区域ID',
  `name` VARCHAR(64) NOT NULL COMMENT '套餐名称(如"畅享100M")',
  `price` DECIMAL(10,2) NOT NULL COMMENT '套餐价格(元)',
  `bandwidth` INT UNSIGNED NOT NULL COMMENT '带宽(Mbps)',
  `valid_days` TINYINT UNSIGNED NOT NULL COMMENT '有效期(天)',
  `traffic_limit` DECIMAL(10,2) DEFAULT NULL COMMENT '流量限制(GB,NULL=无限制)',
  PRIMARY KEY (`package_id`),
  INDEX `idx_region` (`region_id`),
  INDEX `idx_bandwidth` (`bandwidth`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT '套餐配置表';

-- 订单表
CREATE TABLE IF NOT EXISTS `order` (
  `order_id` INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '订单ID',
  `user_id` INT UNSIGNED NOT NULL COMMENT '用户ID',
  `package_id` INT UNSIGNED NOT NULL COMMENT '套餐ID',
  `amount` DECIMAL(10,2) NOT NULL COMMENT '订单金额(元)',
  `pay_time` DATETIME DEFAULT NULL COMMENT '支付时间(NULL=未支付)',
  `effect_time` DATETIME NOT NULL COMMENT '生效时间',
  `expire_time` DATETIME NOT NULL COMMENT '过期时间',
  `status` TINYINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '订单状态(0=未支付,1=已支付,2=已过期)',
  PRIMARY KEY (`order_id`),
  INDEX `idx_user` (`user_id`),
  INDEX `idx_package` (`package_id`),
  INDEX `idx_effect` (`effect_time`),
  INDEX `idx_expire` (`expire_time`),
  INDEX `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT '订单记录表';

-- NAS设备表
CREATE TABLE IF NOT EXISTS `nas_device` (
  `nas_id` INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'NAS设备ID',
  `nas_name` VARCHAR(64) NOT NULL COMMENT '设备名称(如"北京主NAS")',
  `nas_ip` VARCHAR(16) NOT NULL UNIQUE COMMENT '设备IP(唯一)',
  `shared_secret` VARCHAR(64) NOT NULL COMMENT 'RADIUS共享密钥',
  `protocol_type` ENUM('pptp','l2tp','802.1x','pppoe') NOT NULL COMMENT '支持协议类型',
  `pptp_max_connections` SMALLINT UNSIGNED DEFAULT 100 COMMENT 'PPTP最大并发连接数',
  `l2tp_ipsec_psk` VARCHAR(64) DEFAULT NULL COMMENT 'L2TP/IPSec预共享密钥',
  `pppoe_max_connections` SMALLINT UNSIGNED DEFAULT 100 COMMENT 'PPPoE最大并发连接数',
  `pppoe_ac_name` VARCHAR(64) DEFAULT NULL COMMENT 'PPPoE接入集中器名称',
  `pppoe_interface` VARCHAR(16) DEFAULT NULL COMMENT 'PPPoE接口名(如"eth0.100")',
  `status` TINYINT UNSIGNED NOT NULL DEFAULT 1 COMMENT '设备状态(0=离线,1=在线)',
  PRIMARY KEY (`nas_id`),
  INDEX `idx_ip` (`nas_ip`),
  INDEX `idx_protocol` (`protocol_type`),
  INDEX `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT 'NAS设备信息表';

-- 会话表
CREATE TABLE IF NOT EXISTS `session` (
  `session_id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '会话ID',
  `user_id` INT UNSIGNED NOT NULL COMMENT '用户ID',
  `nas_ip` VARCHAR(16) NOT NULL COMMENT 'NAS设备IP',
  `tunnel_id` VARCHAR(64) DEFAULT NULL COMMENT 'PPTP/L2TP隧道ID',
  `pppoe_ac_name` VARCHAR(64) DEFAULT NULL COMMENT 'PPPoE接入集中器名称',
  `pppoe_interface` VARCHAR(16) DEFAULT NULL COMMENT 'PPPoE接口名',
  `start_time` DATETIME NOT NULL COMMENT '会话开始时间',
  `end_time` DATETIME DEFAULT NULL COMMENT '会话结束时间(NULL=在线)',
  `bytes_in` BIGINT UNSIGNED DEFAULT 0 COMMENT '上行流量(字节)',
  `bytes_out` BIGINT UNSIGNED DEFAULT 0 COMMENT '下行流量(字节)',
  `status` TINYINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '会话状态(0=在线,1=离线,2=异常断开,3=手动封禁)',
  `last_activity` DATETIME NOT NULL COMMENT '最后活动时间(心跳更新)',
  `connect_reason` VARCHAR(255) DEFAULT NULL COMMENT '连接原因(如"认证通过")',
  `disconnect_reason` VARCHAR(255) DEFAULT NULL COMMENT '断开原因(如"套餐到期")',
  `protocol` ENUM('pptp','l2tp','802.1x','pppoe') NOT NULL COMMENT '连接协议',
  PRIMARY KEY (`session_id`),
  INDEX `idx_user` (`user_id`),
  INDEX `idx_nas` (`nas_ip`),
  INDEX `idx_status` (`status`),
  INDEX `idx_protocol` (`protocol`),
  INDEX `idx_start` (`start_time`),
  INDEX `idx_end` (`end_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT '会话记录表';


三、核心模块实现(GoFrame)



1. 数据库初始化(模型层)

使用GoFrame的gdb模块定义模型,映射数据库表结构。

(1) 用户模型(model/user.go)
package model

import (
    "gitee.com/johng/gf/v2/database/gdb"
    "gitee.com/johng/gf/v2/frame/g"
)

// User 用户模型
type User struct {
    gdb.Model
    Username         string    `json:"username"`         // 用户名
    PasswordHash     string    `json:"-"`                // 密码哈希(bcrypt)
    RegionID         uint      `json:"region_id"`        // 所属区域ID
    Status           uint8     `json:"status"`           // 用户状态(0=禁用,1=启用)
    Phone            string    `json:"phone"`            // 联系电话
    CurrentSessionID *uint64   `json:"current_session_id"` // 当前在线会话ID(NULL=未在线)
    LastOnlineTime   *gtime.Time `json:"last_online_time"` // 最后一次在线结束时间
}

// TableName 用户表名
func (u User) TableName() string {
    return "user"
}


(2) NAS设备模型(model/nas_device.go)
package model

import (
    "gitee.com/johng/gf/v2/database/gdb"
    "gitee.com/johng/gf/v2/frame/g"
    "time"
)

// NasDevice NAS设备模型
type NasDevice struct {
    gdb.Model
    NasName           string    `json:"nas_name"`            // 设备名称
    NasIP             string    `json:"nas_ip"`              // 设备IP(唯一)
    SharedSecret      string    `json:"-"`                   // RADIUS共享密钥
    ProtocolType      string    `json:"protocol_type"`       // 支持协议(pptp/l2tp/802.1x/pppoe)
    PptpMaxConnections uint16    `json:"pptp_max_connections"` // PPTP最大连接数
    L2tpIpsecPsk      string    `json:"l2tp_ipsec_psk"`      // L2TP/IPSec预共享密钥
    PppoeMaxConnections uint16 `json:"pppoe_max_connections"` // PPPoE最大连接数
    PppoeAcName       string    `json:"pppoe_ac_name"`       // PPPoE接入集中器名称
    PppoeInterface    string    `json:"pppoe_interface"`     // PPPoE接口名
    Status            uint8     `json:"status"`              // 设备状态(0=离线,1=在线)
}

// TableName NAS设备表名
func (n NasDevice) TableName() string {
    return "nas_device"
}


(3) 会话模型(model/session.go)
package model

import (
    "gitee.com/johng/gf/v2/database/gdb"
    "gitee.com/johng/gf/v2/frame/g"
    "time"
)

// Session 会话模型
type Session struct {
    gdb.Model
    UserID            uint      `json:"user_id"`             // 用户ID
    NasIP             string    `json:"nas_ip"`              // NAS设备IP
    TunnelID          string    `json:"tunnel_id"`           // PPTP/L2TP隧道ID
    PppoeAcName       string    `json:"pppoe_ac_name"`       // PPPoE接入集中器名称
    PppoeInterface    string    `json:"pppoe_interface"`     // PPPoE接口名
    StartTime         time.Time `json:"start_time"`          // 会话开始时间
    EndTime           *time.Time `json:"end_time"`            // 会话结束时间(NULL=在线)
    BytesIn           uint64    `json:"bytes_in"`            // 上行流量(字节)
    BytesOut          uint64    `json:"bytes_out"`           // 下行流量(字节)
    Status            uint8     `json:"status"`              // 会话状态(0=在线,1=离线,2=异常断开,3=手动封禁)
    LastActivity      time.Time `json:"last_activity"`       // 最后活动时间(心跳更新)
    ConnectReason     string    `json:"connect_reason"`      // 连接原因
    DisconnectReason  string    `json:"disconnect_reason"`   // 断开原因
    Protocol          string    `json:"protocol"`            // 连接协议(pptp/l2tp/802.1x/pppoe)
}

// TableName 会话表名
func (s Session) TableName() string {
    return "session"
}


2. RADIUS服务模块(认证/计费

基于 github.com/layeh.com/radius 实现,支持多协议认证与计费。



(1) 服务启动(server.go)
package radius

import (
    "context"
    "net"
    "strconv"
    "time"

    "github.com/gogf/gf/v2/frame/g"
    "github.com/gogf/gf/v2/os/gctx"
    "github.com/layeh.com/radius"
    "github.com/layeh.com/radius/rfc2865"
    "github.com/layeh.com/radius/rfc2866"
    "radius-system/app/model"
    "radius-system/app/pkg/auth"
)

const (
    RADIUS_AUTH_PORT  = 1812
    RADIUS_ACCT_PORT  = 1813
    RADIUS_COA_PORT   = 3799
    RADIUS_SECRET     = "your_shared_secret"
    AUTH_TIMEOUT      = 5 * time.Second
    ACCT_TIMEOUT      = 10 * time.Second
)

var (
    ctx = gctx.New()
)

// Start 启动RADIUS服务(认证+计费+CoA)
func Start() {
    go authServer()
    go acctServer()
    go coaServer()
}

// 认证服务器(处理Access-Request)
func authServer() {
    server := radius.NewServer()
    server.Secret = map[string]string{":"+strconv.Itoa(RADIUS_AUTH_PORT): RADIUS_SECRET}
    server.Timeout = AUTH_TIMEOUT
    server.Handler = authHandler
    if err := server.ListenAndServe("udp", ":"+strconv.Itoa(RADIUS_AUTH_PORT)); err != nil {
        g.Log().Fatal(ctx, "认证服务器启动失败:", err)
    }
}

// 计费服务器(处理Accounting-Start/Interim-Update/Stop)
func acctServer() {
    server := radius.NewServer()
    server.Secret = map[string]string{":"+strconv.Itoa(RADIUS_ACCT_PORT): RADIUS_SECRET}
    server.Timeout = ACCT_TIMEOUT
    server.Handler = acctHandler
    if err := server.ListenAndServe("udp", ":"+strconv.Itoa(RADIUS_ACCT_PORT)); err != nil {
        g.Log().Fatal(ctx, "计费服务器启动失败:", err)
    }
}

// CoA服务器(处理Change of Authorization)
func coaServer() {
    server := radius.NewServer()
    server.Secret = map[string]string{":"+strconv.Itoa(RADIUS_COA_PORT): RADIUS_SECRET}
    server.Handler = coaHandler
    if err := server.ListenAndServe("udp", ":"+strconv.Itoa(RADIUS_COA_PORT)); err != nil {
        g.Log().Fatal(ctx, "CoA服务器启动失败:", err)
    }
}


(2) 认证处理(auth_handler.go)
package radius

import (
    "context"
    "crypto/md5"
    "encoding/hex"
    "fmt"
    "net"
    "strings"
    "time"

    "gitee.com/johng/gf/v2/frame/g"
    "gitee.com/johng/gf/v2/os/gtime"
    "gitee.com/johng/gf/v2/util/guid"
    "github.com/layeh.com/radius/rfc2865"
    "radius-system/app/model"
    "radius-system/app/pkg/auth"
)

// authHandler 处理RADIUS认证请求
func authHandler(packet *radius.Packet, conn net.Conn) {
    username := rfc2865.UserName_Get(packet)
    password := rfc2865.UserPassword_Get(packet)
    nasIP := packet.SourceIP().String()

    // 1. 查询用户状态(启用且存在)
    user := &model.User{}
    if err := g.Model("user").Where("username = ? AND status = 1", username).Scan(user); err != nil {
        g.Log().Error(ctx, "用户查询失败:", err)
        return // 返回Access-Reject
    }

    // 2. 验证密码(bcrypt)
    if !auth.VerifyPassword(password, user.PasswordHash) {
        g.Log().Warn(ctx, "密码错误:", username)
        return
    }

    // 3. 检查有效订单(未过期且已支付)
    order := &model.Order{}
    if err := g.Model("order").
        Where("user_id = ? AND status = 1 AND expire_time > NOW()", user.Id).
        Order("expire_time DESC").
        First(order); err != nil || order.Id == 0 {
        g.Log().Warn(ctx, "无有效套餐:", username)
        return
    }

    // 4. 检查流量超限(Redis缓存)
    redisKey := g.Cache().Key("user_traffic", user.Id)
    bytesUsed, err := g.Cache().Get(ctx, redisKey)
    if err != nil && !gerror.HasError(err, "not found") {
        g.Log().Error(ctx, "获取流量失败:", err)
        return
    }
    if order.TrafficLimit > 0 {
        trafficLimit := int64(order.TrafficLimit) * 1024 * 1024 * 1024 // GB转字节
        if bytesUsed != nil && bytesUsed.(int64) >= trafficLimit {
            g.Log().Warn(ctx, "流量超限:", username)
            return
        }
    }

    // 5. 查询NAS设备信息
    nas := &model.NasDevice{}
    if err := g.Model("nas_device").Where("nas_ip = ?", nasIP).Scan(nas); err != nil {
        g.Log().Error(ctx, "NAS设备查询失败:", err)
        return
    }

    // 6. 协议类型识别(PPTP/L2TP/802.1X/PPPoE)
    protocol := rfc2865.PPPProtocolType_Get(packet)
    switch protocol {
    case rfc2865.PPPProtocolPPP: // PPP协议(PPTP/L2TP/PPPoE)
        vendorType := rfc2865.VendorSpecificType_Get(packet)
        if vendorType == 0x00000100 { // PPPoE Vendor-Specific类型(示例)
            protocol = "pppoe"
        } else if nas.ProtocolType == "pptp" {
            protocol = "pptp"
        } else if nas.ProtocolType == "l2tp" {
            protocol = "l2tp"
        }
    case rfc2865.PPPProtocolEthernet: // 802.1X(Ethernet)
        protocol = "802.1x"
    default:
        g.Log().Warn(ctx, "不支持的协议类型:", protocol)
        return
    }

    // 7. 协议认证(调用工厂方法)
    authHandler := auth.Factory{}.GetHandler(protocol)
    if !authHandler.Authenticate(packet, user, nas) {
        g.Log().Warn(ctx, "协议认证失败:", username, "协议:", protocol)
        return
    }

    // 8. 分配IP并生成Access-Accept
    framedIP := allocateFramedIP(nasIP)
    accessAccept := radius.New(radius.CodeAccessAccept, []byte(RADIUS_SECRET))
    rfc2865.FramedIPAddress_Set(accessAccept, net.ParseIP(framedIP))
    rfc2865.SessionTimeout_Set(accessAccept, int32(order.ValidDays*86400)) // 套餐总秒数

    // 发送响应
    if _, err := conn.Write(accessAccept.Bytes()); err != nil {
        g.Log().Error(ctx, "发送认证响应失败:", err)
        return
    }
    g.Log().Info(ctx, "认证通过:", username, "NAS:", nasIP, "IP:", framedIP, "协议:", protocol)

    // 9. 记录会话(异步)
    go createSession(user.Id, nasIP, protocol, framedIP, order.Id)
}

// 创建会话记录
func createSession(userID uint, nasIP, protocol, framedIP string, orderID uint) {
    session := &model.Session{
        UserID:         userID,
        NasIP:          nasIP,
        Protocol:       protocol,
        StartTime:      gtime.Now(),
        Status:         0, // 在线
        LastActivity:   gtime.Now(),
        ConnectReason:  "认证通过",
        DisconnectReason: "",
    }
    if protocol == "pptp" || protocol == "l2tp" {
        session.TunnelID = extractTunnelID(protocol, packet) // 需传入packet参数(示例简化)
    } else if protocol == "pppoe" {
        session.PppoeAcName = getPPPoEACName(nasIP) // 从NAS获取AC名称(示例简化)
        session.PppoeInterface = getPPPoEInterface(nasIP) // 从NAS获取接口名(示例简化)
    }
    if _, err := g.Model("session").Insert(session); err != nil {
        g.Log().Error(context.Background(), "记录会话失败:", err)
        return
    }
    // 更新用户当前会话ID
    _, _ = g.Model("user").Where("id = ?", userID).Update(g.Map{
        "current_session_id": session.SessionID,
    })
}


(3) PPPoE认证处理器(pkg/auth/pppoe.go)
package auth

import (
    "context"
    "gitee.com/johng/gf/v2/frame/g"
    "github.com/layeh.com/radius/rfc2865"
    "radius-system/app/model"
)

// PPPoEAuth PPPoE认证处理器
type PPPoEAuth struct{}

// Authenticate 实现PPPoE认证(基于PPP协议)
func (p *PPPoEAuth) Authenticate(packet *radius.Packet, user *model.User, nas *model.NasDevice) bool {
    // 1. 验证PPP协议类型
    protocol := rfc2865.PPPProtocolType_Get(packet)
    if protocol != rfc2865.PPPProtocolPPP {
        g.Log().Warn(context.Background(), "PPPoE要求PPP协议类型")
        return false
    }

    // 2. 解析PPPoE特有属性(AC名称)
    acName := rfc2865.VendorSpecific_Get(packet, 0x00000100) // 示例Vendor-Specific类型
    if len(acName) == 0 {
        g.Log().Warn(context.Background(), "PPPoE未携带AC名称")
        return false
    }

    // 3. 验证AC名称与NAS配置一致
    if string(acName) != nas.PppoeAcName {
        g.Log().Warn(context.Background(), "PPPoE AC名称不匹配(NAS配置:", nas.PppoeAcName, ")")
        return false
    }

    // 4. 验证PPP认证(CHAP/MS-CHAPv2)
    if !verifyCHAP(user.Username, rfc2865.CHAPPassword_Get(packet)) {
        g.Log().Warn(context.Background(), "PPPoE CHAP验证失败")
        return false
    }

    // 5. 检查NAS的PPPoE最大连接数
    connCount, err := getPppoeConnectionCount(nas.NasIP)
    if err != nil || connCount >= nas.PppoeMaxConnections {
        g.Log().Warn(context.Background(), "PPPoE连接数超限(当前:", connCount, ",最大:", nas.PppoeMaxConnections, ")")
        return false
    }
    return true
}

// verifyCHAP 验证CHAP密码(示例)
func verifyCHAP(username, chapPass string) bool {
    // 实际逻辑:查询数据库存储的CHAP挑战-响应值(需实现)
    return true
}

// getPppoeConnectionCount 获取NAS当前PPPoE连接数(示例)
func getPppoeConnectionCount(nasIP string) (int, error) {
    // 实际逻辑:通过SNMP/API查询NAS的PPPoE连接数(如`pppd`进程数)
    return 0, nil
}


3. 会话管理(service/session.go)

package service

import (
    "context"
    "time"

    "gitee.com/johng/gf/v2/frame/g"
    "gitee.com/johng/gf/v2/os/gtime"
    "gitee.com/johng/gf/v2/database/gdb"
    "radius-system/app/model"
)

type SessionService struct {
    ctx context.Context
}

func NewSessionService() *SessionService {
    return &SessionService{ctx: gctx.New()}
}

// CreateSession 创建会话记录
func (s *SessionService) CreateSession(userID uint, nasIP, protocol, framedIP string, orderID uint) error {
    session := &model.Session{
        UserID:         userID,
        NasIP:          nasIP,
        Protocol:       protocol,
        StartTime:      gtime.Now(),
        Status:         0,
        LastActivity:   gtime.Now(),
        ConnectReason:  "认证通过",
        DisconnectReason: "",
    }
    if protocol == "pptp" || protocol == "l2tp" {
        session.TunnelID = extractTunnelID(protocol, framedIP) // 示例简化
    } else if protocol == "pppoe" {
        session.PppoeAcName = "NAS-AC-01" // 示例值,实际从NAS获取
        session.PppoeInterface = "eth0.100" // 示例值,实际从NAS获取
    }
    if _, err := g.Model("session").Insert(session); err != nil {
        return err
    }
    // 更新用户当前会话ID
    _, err := g.Model("user").Where("id = ?", userID).Update(g.Map{
        "current_session_id": session.SessionID,
    })
    return err
}

// UpdateSessionActivity 更新会话最后活动时间
func (s *SessionService) UpdateSessionActivity(sessionID uint) error {
    _, err := g.Model("session").Where("session_id = ?", sessionID).Update(g.Map{
        "last_activity": gtime.Now(),
    })
    return err
}

// MarkSessionOffline 标记会话离线
func (s *SessionService) MarkSessionOffline(sessionID uint, disconnectReason string) error {
    session := &model.Session{}
    if err := g.Model("session").Where("session_id = ?", sessionID).Scan(session); err != nil {
        return err
    }
    _, err := g.Model("session").Where("session_id = ?", sessionID).Update(g.Map{
        "end_time":         gtime.Now(),
        "status":           1,
        "disconnect_reason": disconnectReason,
    })
    if err != nil {
        return err
    }
    // 更新用户当前会话ID为NULL
    _, err = g.Model("user").Where("id = ?", session.UserID).Update(g.Map{
        "current_session_id": nil,
    })
    return err
}

// GetOnlineSessions 获取所有在线会话
func (s *SessionService) GetOnlineSessions() ([]*model.Session, error) {
    sessions := []*model.Session{}
    err := g.Model("session").Where("status = 0").All(&sessions)
    return sessions, err
}


4. 定时任务(expire_check.go & heartbeat_check.go)

// app/task/expire_check.go(过期检查任务)
package task

import (
    "context"
    "time"

    "gitee.com/johng/gf/v2/frame/g"
    "gitee.com/johng/gf/v2/os/gcron"
    "gitee.com/johng/gf/v2/os/gtime"
    "radius-system/app/model"
    "radius-system/app/pkg/radius"
    "radius-system/app/service"
)

// StartExpireCheckTask 启动过期检查任务(每5分钟)
func StartExpireCheckTask() {
    spec := "0 */5 * * * *"
    _, err := gcron.Add(spec, func(ctx context.Background()) {
        now := gtime.Now()
        g.Log().Info(ctx, "开始执行过期用户检查任务...")

        // 查询已过期订单
        orders, err := model.Model("order").
            Where("status = 1 AND expire_time <= ?", now.Format("Y-m-d H:i:s")).
            All()
        if err != nil {
            g.Log().Error(ctx, "查询过期订单失败:", err)
            return
        }

        // 处理每个过期订单
        for _, order := range orders {
            // 断开用户所有会话
            sessions, err := model.Model("session").
                Where("user_id = ? AND status = 0", order.UserId).
                All()
            if err != nil {
                g.Log().Error(ctx, "查询用户会话失败:", err)
                continue
            }

            // 发送CoA终止请求
            for _, sess := range sessions {
                if err := radius.SendCoATerminate(sess.NasIP, sess.SessionID, 49); err != nil {
                    g.Log().Error(ctx, "CoA终止失败:", err)
                    go func(s *model.Session) {
                        time.Sleep(5 * time.Second)
                        radius.SendCoATerminate(s.NasIP, s.SessionID, 49)
                    }(sess)
                }
                // 更新会话状态
                model.Model("session").Where("session_id = ?", sess.SessionID).Update(g.Map{"status": 1})
            }

            // 清理用户缓存
            service.NewCacheService().DeleteUserCache(order.UserId)
        }

        g.Log().Info(ctx, "过期用户检查完成,处理订单数:", len(orders))
    })
    if err != nil {
        g.Log().Fatal(ctx, "启动过期检查任务失败:", err)
    }
}

// app/task/heartbeat_check.go(心跳检测任务)
package task

import (
    "context"
    "time"

    "gitee.com/johng/gf/v2/frame/g"
    "gitee.com/johng/gf/v2/os/gcron"
    "gitee.com/johng/gf/v2/os/gtime"
    "radius-system/app/model"
    "radius-system/app/service"
)

// StartHeartbeatCheckTask 启动心跳检测任务(每2分钟)
func StartHeartbeatCheckTask() {
    spec := "0 */2 * * * *"
    _, err := gcron.Add(spec, func(ctx context.Background()) {
        g.Log().Info(ctx, "开始执行心跳检测任务...")

        // 查询所有在线会话
        sessions, err := service.NewSessionService().GetOnlineSessions()
        if err != nil {
            g.Log().Error(ctx, "查询在线会话失败:", err)
            return
        }

        // 检查每个会话的最后活动时间
        now := gtime.Now()
        for _, sess := range sessions {
            timeout := sess.LastActivity.Add(5 * time.Minute)
            if now.After(timeout) {
                g.Log().Warn(ctx, "会话超时未活动,标记为异常断开:", sess.SessionID)
                if err := service.NewSessionService().MarkSessionOffline(sess.SessionID, "心跳超时(5分钟)"); err != nil {
                    g.Log().Error(ctx, "标记异常断开失败:", err)
                }
            }
        }

        g.Log().Info(ctx, "心跳检测完成,处理超时会话数:", len(sessions))
    })
    if err != nil {
        g.Log().Fatal(ctx, "启动心跳检测任务失败:", err)
    }
}


5. HTTP API接口(api/user/online.go)

package api

import (
    "context"
    "net/http"

    "gitee.com/johng/gf/v2/frame/g"
    "gitee.com/johng/gf/v2/net/ghttp"
    "gitee.com/johng/gf/v2/os/gtime"
    "radius-system/app/model"
    "radius-system/app/service"
)

type UserOnlineAPI struct {
    ghttp.API
}

// Get 用户个人在线状态查询
func (a *UserOnlineAPI) Get(r *ghttp.Request) {
    userID := r.GetQueryUint("user_id")
    if userID == 0 {
        r.Response.WriteJsonExit(g.Map{"code": 400, "msg": "用户ID必填"})
    }

    // 查询用户当前会话ID
    user := &model.User{}
    if err := g.Model("user").Where("id = ?", userID).Scan(user); err != nil {
        r.Response.WriteJsonExit(g.Map{"code": 404, "msg": "用户不存在"})
    }
    if user.CurrentSessionID == nil {
        r.Response.WriteJson(g.Map{"code": 200, "msg": "用户未在线"})
        return
    }

    // 查询会话详情
    session := &model.Session{}
    if err := g.Model("session").Where("session_id = ?", user.CurrentSessionID).Scan(session); err != nil {
        r.Response.WriteJsonExit(g.Map{"code": 500, "msg": "查询会话失败:" + err.Error()})
    }
    if session.Status != 0 {
        r.Response.WriteJson(g.Map{"code": 200, "msg": "用户已离线"})
        return
    }

    // 计算在线时长
    duration := gtime.Now().Sub(session.StartTime)
    r.Response.WriteJson(g.Map{
        "code":           200,
        "online":         true,
        "session_id":     session.SessionID,
        "start_time":     session.StartTime.Format("Y-m-d H:i:s"),
        "current_duration": duration.String(),
        "bytes_used":     g.Map{"input": session.BytesIn, "output": session.BytesOut},
        "protocol":       session.Protocol,
        "pppoe_ac_name":  session.PppoeAcName,
        "pppoe_interface": session.PppoeInterface,
    })
}


四、部署与运维


1. 环境要求

  • 操作系统:CentOS 7+/Ubuntu 20.04+

  • 数据库:MySQL 5.7+/PostgreSQL 12+

  • 缓存:Redis 5.0+

  • Go版本:1.18+



2. 部署步骤


1.安装依赖:
# 安装MySQL/Redis
sudo apt-get install mysql-server redis-server

# 安装Go 1.18+
wget https://dl.google.com/go/go1.18.linux-amd64.tar.gz
sudo tar -C /usr/local -xzf go1.18.linux-amd64.tar.gz
echo 'export PATH=$PATH:/usr/local/go/bin' >> ~/.bashrc
source ~/.bashrc


2.配置数据库:
CREATE DATABASE radius_system DEFAULT CHARSET utf8mb4;
USE radius_system;
-- 执行上述SQL创建脚本


3.配置Go应用:

复制config.toml.example为config.toml,修改数据库、Redis、RADIUS参数:

[database]
dsn = "user:password@tcp(127.0.0.1:3306)/radius_system?charset=utf8mb4&parseTime=True"

[redis]
addr = "127.0.0.1:6379"
password = ""
db = 0

[radius]
secret = "your_shared_secret"
auth_port = 1812
acct_port = 1813
coa_port = 3799


4.启动服务:
# 启动HTTP API服务(端口8000)
go run app/main.go -s api -p 8000

# 启动RADIUS服务(认证/计费/CoA)
go run app/radius/server.go


3. 监控与日志
  • 日志查看:应用日志输出到stdout(可通过glog配置文件路径)。

  • 性能监控:使用Prometheus+Grafana监控QPS、缓存命中率、NAS在线率。

  • 告警:通过gcron定时检查服务状态,异常时发送邮件/企业微信告警。



五、总结

本方案完整覆盖多协议认证(PPTP/L2TP/802.1X/PPPoE)、用户计费、在线状态管理、区域/套餐管控等核心功能,通过:

  • 数据库扩展:支持PPPoE专用字段(AC名称、接口名)。

  • RADIUS协议适配:识别PPPoE协议类型,验证AC名称和PPP认证。

  • 会话全生命周期管理:记录PPPoE会话的连接建立、在线保持、异常断开。

  • 定时任务与监控:确保过期用户自动断网,NAS状态实时监控。

系统基于GoFrame框架开发,具备高内聚、低耦合、易扩展的特性,适用于中小型企业/运营商的网络管理场景。

作者:yzhang  创建时间:2025-08-29 10:00
最后编辑:yzhang  更新时间:2025-08-29 10:39