基于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.go3. 监控与日志
日志查看:应用日志输出到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:39