游戏日志系统设计与实现

发表于2017-12-12
评论1 8k浏览

作用

游戏临近上线,需要做一个日志系统,记录玩家的行为,用途如下:

  • 监控玩家状态变化,如账号登记,角色创建,上线下线,充值等;
  • 分析玩家行为,如金币钻石消耗在什么系统上了,主要参与了哪些活动和玩法;
  • 帮助分析bug,记录玩家的行为和数据变化,可以回溯bug产生的过程;
  • 方便客服,查询和处理玩家的反馈。

结构设计

首先,用一台公共的服务器左右日志的db服务器,所有游戏中产生的日志,都往这个db中写;

然后,查询系统需要一个后端,与前端交互,来处理查询逻辑,反馈数据;

最后,需要一个前端,提交查询条件,展示查询结果。

实现

日志数据库

在网易无论手游还是端游,基本上都是用mongo,出来之后游戏数据库也就用了mongo,在我看来主要基于两个优点:

  1. 游戏需求多变,mongo直接写json,省去需要建表改表的麻烦;
  2. 对于游戏数据库,无须完成逻辑,只要存数据就好,用不上复杂的SQL语句;

查询系统后端

系统后端直接用了nodejs,主要是基于经验和前段吧,因为之前用nodejs和python写过http服务器,可选的就是这两个了,加上前段用的网页,前后端统一就都用js了。

// dao.js
var MongoClient = require('mongodb').MongoClient;
var StrUtils = require("./StrUtils");
var TIMEOUT = 3000;// 毫秒
function getConnStr(host, port, rpSetName, dbname) {
    return StrUtils.format("mongodb://{0}:{1}/{2}?connectTimeoutMS={3}&replicaSet={4}", host, port, dbname, TIMEOUT, rpSetName);
};
function findDocuments(conditions, db, col, startIndex, rows, callback) {
    db.slaveOk = true;
    var collection = db.collection(col);
    var cursor = collection.find(conditions).sort({tm: -1}).skip(startIndex).limit(rows);
    cursor.toArray(function(err, docs) {
        if (err) {
            console.error(err);
            callback([], 0);
            return;
        }
        cursor.count(false, function(err, count) {
            if (err) {
                console.error(err);
                callback([], 0);
                return;
            }
            callback(docs, count);
        });
    });
}
function findRecord(host, port, rpSetName, dbname, colname, conditions, startIndex, rows, callback) {
    var conn = getConnStr(host, port, rpSetName, dbname, colname);
    MongoClient.connect(conn, function(err, db) {
        if (err) {
            console.error(err);
            callback([], 0);
        } else {
            findDocuments(conditions, db, colname, startIndex, rows, function(docs, cnt) {
                callback(docs, cnt);
                db.close();
            });
        }
    });
}
exports.findRecord = findRecord;

前端

考虑到这个工具会时常更新,多方会用到,用客户端的话,用网页比较合适,更新之后刷新就可以了。

这里有点技巧就是列名需要动态的获取,否则就要在客户端写很多Grid模板了,主要代码如下:

// log.js
function createGrid(colNames, colModel, url) {
    var reader = {
        root: "rows",// 包含实际数据的数组
        page: "page",// 当前页
        total: "total",// 总页数
        records: "records",// 查询出的记录数
        repeatitems: true,// 每行的数据是可以重复的
        cell: "cell",// 当前行所有单元格的数据数组
        id: "id",// 行id
        userdata: "userdata"// 额外参数
    };
    var options = {
        // 请求
        url: url,
        autoencode: true,
        datatype: "json",
        mtype: "GET",
        // 表格显示
        caption: "查询结果",
        colNames: colNames,
        colModel: colModel,
        // 页数
        rowNum: DEFAULT_ROW,
        rowList: [30, 50],
        pager: '#pager',
        page: 1,
        // 排序
        sortable: false,
        sortname: 'accout',
        sortorder: "desc",
        // 尺寸
        height: 'auto',
        width: 'auto',
        shrinkToFit: true,
        autowidth: true,
        // 附加功能
        viewrecords: true,
        rownumbers: true,
        multiselect: false,
        cellEdit: false,
        hidegrid: false,
        // 数据解析
        jsonReader: reader,
        loadComplete: function (jsonData) {
            if (jsonData.error) {
                alert(jsonData.error);
                return;
            }
        }
    };
    var grid = $("#grid");
    grid.jqGrid(options);
}
function getColModel(colNames, colWidth) {
    var colModel = [];
    for (var i = 0; i < colNames.length; i++) {
        var name = colNames[i];
        var width = colWidth[i] || 10;
        colModel.push({name: name, sortable: false, width: width});
    }
    return colModel;
}
function getUrlArgs(getCol, isExport) {
    var acc = $("input#account").val();
    var tp = $("#sel_op").val();
    var args = {
        usr: getCookie(COOKIE_KEY),
        getCol: getCol,
        acc: acc,
        tp: tp
    };
    if (getCol) {
        return args;
    } else {
        args.sid = $("#sel_server").val();
        args.channel = $("#sel_channel").val();
        args.pkg = $("#sel_pkg").val();
        args.fdate = $("#date_picker_from").datepicker('getDate').getTime();
        args.tdate = $("#date_picker_to").datepicker('getDate').getTime();
        args.name = $("input#name").val();
        args.id = $("input#id").val();
        if (isExport) {
            args.page = 1;
            args.rows = 0;
        }
        return args;
    }
}
function onClickQuery() {
    $.jgrid.gridUnload("#grid");
    var url = getURL(HOST, PORT, "/roleinfo", getUrlArgs(true, false));
    $.get(url, function(jsonData){
        if (jsonData.error) {
            alert(jsonData.error);
            return;
        }
        var colNames = jsonData.colNames;
        var colWidth = jsonData.colWidth;
        if (colNames && colNames.length) {
            var colModel = getColModel(colNames, colWidth);
            var url = getURL(HOST, PORT, "/roleinfo", getUrlArgs(false, false));
            createGrid(colNames, colModel, url);
        } else {
            alert("未知操作类型");
        }
    }, "json");
}

另外,前端还有个导出csv的小功能,代码如下:

// export
// 参考:http://jsfiddle.net/pxfunc/aa2t3ntt/1/
function JSONToCSVConvertor(arrData, title) {
    var CSV = '';
    // 表头
    CSV += title + '\r\n\n';
    // 列名
    var thList = [];
    var colNames = "";
    for (var colName in arrData[0]) {
        colNames += colName + ',';
        thList.push(colName);
    }
    colNames = colNames.slice(0, -1);
    CSV += colNames + '\r\n';
    // 数据
    for (var i = 0; i < arrData.length; i++) {
        var data = arrData[i];
        var line = "";
        for (var j = 0; j < thList.length; j++) {
            var key = thList[j];
            line += '"' + data[key] + '",';
        }
        line.slice(0, line.length - 1);
        CSV += line + '\r\n';
    }
    if (CSV === '') {
        alert("Invalid data");
        return;
    }
    // 创建一个标签并自动点击下载,然后删除
    var fileName = title.replace(/ /g,"_");
    var uri = 'data:text/csv;charset=utf-8,\ufeff' + encodeURIComponent(CSV);
    var link = document.createElement("a");
    link.href = uri;
    link.style = "visibility:hidden";
    link.download = fileName + ".csv";
    document.body.appendChild(link);
    link.click();
    document.body.removeChild(link);
}

webserver

同样也用nodejs简单地实现了一个,省去配置Apache或Nginx的麻烦,代码如下:

// webserver.js
var http = require('http');
var url = require('url');
var fs = require('fs');
var path = require('path');
var PORT = 9950;
var mime = {
    "css": "text/css",
    "gif": "image/gif",
    "html": "text/html",
    "ico": "image/x-icon",
    "jpeg": "image/jpeg",
    "jpg": "image/jpeg",
    "js": "text/javascript",
    "json": "application/json",
    "pdf": "application/pdf",
    "png": "image/png",
    "svg": "image/svg+xml",
    "swf": "application/x-shockwave-flash",
    "tiff": "image/tiff",
    "txt": "text/plain",
    "wav": "audio/x-wav",
    "wma": "audio/x-ms-wma",
    "wmv": "video/x-ms-wmv",
    "xml": "text/xml",
    "woff": "application/x-woff",
    "woff2": "application/x-woff2",
    "tff": "application/x-font-truetype",
    "otf": "application/x-font-opentype",
    "eot": "application/vnd.ms-fontobject"
};
var server = http.createServer(function(request, response) {
    var pathname = url.parse(request.url).pathname || "/index.html";
    var realPath = path.join(".", pathname);
    var ext = path.extname(realPath);
    if (!ext) {
        pathname = "/index.html";
        realPath = path.join(".", pathname);
        ext = path.extname(realPath);
    }
    ext = ext ? ext.slice(1) : 'unknown';
    fs.exists(realPath, function(exists) {
        if (exists) {
            fs.readFile(realPath, "binary", function(err, file) {
                if (err) {
                    response.writeHead(500, {'Content-Type': 'text/plain'});
                    response.end(err);
                } else {
                    var contentType = mime[ext] || "text/plain";
                    response.writeHead(200, {'Content-Type': contentType});
                    response.write(file, "binary");
                    response.end();
                }
            });
        } else {
            response.writeHead(404, {'Content-Type': 'text/plain'});
            response.write("This request URL " + pathname + " was not found on this server.");
            response.end();
        }
    });
});
server.listen(PORT);

最后上截图: 

注意:

  1. js跨域问题,处理起来要小心
  2. mongo分页查询skip,当数据过多时,会很慢

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引