手搭我的世界基岩版服务器后台网站(LiteloaderBDS-SQLite-Spring Boot-Vue)Java后端+RESTful API;借助Three.js实现三维可视化展览交互界面

项目是刚刚完成的,于是趁热打铁把文档也写了。在这里分享出来,也方便以后回顾

目录

项目介绍

整体设计架构图

网站界面预览图

技术选型和原因

搭建步骤

库表设计

插件说明

后端说明

前端说明

部署说明

完整代码

插件代码

后端代码

前端代码

项目总结


项目介绍

本项目旨在为我的世界基岩版私服搭建一个可视化的后台管理系统,通过 LiteloaderBDS 插件实时收集游戏内数据,并将其存储在轻量级数据库 SQLite 中。后端采用 Spring Boot 和 MyBatis 技术栈实现 RESTful API,前端采用 Vue 框架、Element-UI Plus 组件库以及 Three.js WebGL 库实现三维可视化界面

整体设计架构图

手搭我的世界基岩版服务器后台网站(LiteloaderBDS-SQLite-Spring Boot-Vue)Java后端+RESTful API;借助Three.js实现三维可视化展览交互界面_第1张图片

网站界面预览图

主页:

手搭我的世界基岩版服务器后台网站(LiteloaderBDS-SQLite-Spring Boot-Vue)Java后端+RESTful API;借助Three.js实现三维可视化展览交互界面_第2张图片

方块地图:

手搭我的世界基岩版服务器后台网站(LiteloaderBDS-SQLite-Spring Boot-Vue)Java后端+RESTful API;借助Three.js实现三维可视化展览交互界面_第3张图片

 手搭我的世界基岩版服务器后台网站(LiteloaderBDS-SQLite-Spring Boot-Vue)Java后端+RESTful API;借助Three.js实现三维可视化展览交互界面_第4张图片

数据总表:

手搭我的世界基岩版服务器后台网站(LiteloaderBDS-SQLite-Spring Boot-Vue)Java后端+RESTful API;借助Three.js实现三维可视化展览交互界面_第5张图片

 

技术选型和原因

本项目采用的部分技术栈:

  • LiteloaderBDS:跨语言 BDS 插件加载器(适用于我的世界基岩版服务器)
  • SQLite:轻量级数据库,减少部署难度
  • MyBatis:持久层框架
  • Spring Boot:简化 Spring 应用的初始搭建及开发
  • Spring MVC:基于 Java 的 Web 框架,支持 RESTful API 设计
  • Vue:渐进式 JavaScript 框架,构建用户界面
  • Element-UI Plus:基于 Vue 的组件库
  • Three.js:WebGL库,实现三维可视化

搭建步骤

  1. 购买云服务器实例
  2. 安装部署 BDS
  3. 安装部署 LiteloaderBDS
  4. 编写 LiteloaderBDS 脚本插件(将数据存入 SQLite 数据库)
  5. 插件测试
  6. 插件部署
  7. 使用 IntelliJ IDEA、Hbuilder X,分别创建 Spring Boot 和 Vue 项目,编写后端和前端
  8. 网站测试
  9. 网站部署

库表设计

  • 位置表
    • 在下面表中,出现重复位置的概率很大,因此设计了位置表,节省占用空间
    • 在未来,位置表中可以添加访问次数这样的列,用于统计玩家活跃地区
    • 位置表的 x,y,z 和维度 id 上了索引,便于查找
  • 容器表
    • 包括玩家背包、末影箱和地图上的容器方块
  • 玩家表
    • 玩家有位置和容器
  • 历史位置表
    • 历史位置有玩家和位置
  • 容器方块表
    • 容器方块有容器和位置
  • 破坏放置表
    • 破坏放置有玩家和位置
  • 攻击实体表
    • 攻击实体有玩家和位置

插件说明

BB_Data.js 的代码内容分为四大部分:事件监听、定时任务、辅助函数、创表语句

其中,增删改查逻辑集中在事件监听、定时任务和辅助函数部分

被监听的事件:

  • 玩家进入世界
  • 玩家离开世界
  • 玩家打开容器
  • 玩家关闭容器
  • 玩家发送消息
  • 玩家破坏放置
  • 玩家攻击实体

由于 SQLite 对并发修改支持不佳,代码中的 SQL 执行语句偶尔会出现异常;但总的来说,这仅仅会导致很小一部分行为没有被记录,所以我没有加锁来改善这一问题(加锁影响性能)

有一些测试用的打印语句,可以删掉

后端说明

后端采用了传统的 Spring Boot + MyBatis 技术栈

相对于持久层设计,简化了数据模型(去除了所有的外键部分),便于前端拿取数据后直接使用

前端说明

风格为选项式 API,单页面应用(SPA),面向组件设计,解耦较好

多种布局样式,包括传统、绝对位置和 Flex 布局

使用了路由管理

其中一个 svg 图标(ChatGPT),直接封装为组件使用了,在代码中省略

部署说明

后端打包成 JAR 文件,在服务器用命令行执行

前端打包成静态资源,上传到服务器的 Nginx 服务目录,启动 Nginx

完整代码

插件代码

BB_Data:

/// 

// TODO 删除过早的(根据时间戳)数据

let session;

mc.listen('onServerStarted', () => {
    session = initDB();
});

mc.listen('onJoin', (player) => {
    let preSelectPlayer = session.prepare('SELECT COUNT(*) as count FROM player_table WHERE xuid = ?;');
    let preInsertPlayer = session.prepare('INSERT INTO player_table (xuid, name, bag_uuid, enc_uuid) VALUES (?, ?, ?, ?);');
    let preInsertCtr = session.prepare('INSERT INTO ctr_table (uuid, name, content, latest_timestamp) VALUES (?, ?, ?, ?);');
    let currentTimestamp = Date.now();

    // 1. 插入新玩家(如果不存在)
    preSelectPlayer.bind([player.xuid]);
    const playerResult = preSelectPlayer.reexec().fetch();
    if (playerResult.count === 0) {
        // 插入新玩家
        let bagUUID = generateUUID();
        let encUUID = generateUUID();
        preInsertPlayer.bind([player.xuid, player.name, bagUUID, encUUID]);
        preInsertPlayer.reexec();
        // 插入新容器
        let containers = [
            {uuid: bagUUID, name: 'bag'},
            {uuid: encUUID, name: 'ender_chest'}
        ];

        containers.forEach((container) => {
            preInsertCtr.bind([
                container.uuid,
                container.name,
                '{}',
                currentTimestamp
            ]);
            preInsertCtr.reexec();
            preInsertCtr.clear();
        });
        log(`向玩家表中插入了 ${player.name}`);
    }

    // 2. 更新玩家
    updatePlayer(player);

    // 3. 插入消息
    const messageContent = JSON.stringify({text: `${player.name} 进入游戏`});
    insertMsg(player, 'join', messageContent);
});

setInterval(() => {
    mc.getOnlinePlayers().forEach((player) => {
        let preInsertHistoryPos = session.prepare('INSERT INTO history_pos_table (xuid, pos_id, timestamp) VALUES (?, ?, ?);');
        let currentTimestamp = Date.now();

        // 1. 更新玩家,并获得玩家位置id
        const newPosId = updatePlayer(player);

        // 2. 添加历史位置
        preInsertHistoryPos.bind([player.xuid, newPosId, currentTimestamp]);
        preInsertHistoryPos.reexec();
    });
}, 2 * 1000);

mc.listen('onOpenContainer', (player, block) => {
    if (!block.hasContainer()) {
        return;
    }
    let preInsertCtr = session.prepare('INSERT INTO ctr_table (uuid, name) VALUES (?, ?);');
    let preInsertCtrBlock = session.prepare('INSERT INTO ctr_block_table (uuid, pos_id, ctr_uuid) VALUES (?, ?, ?);');
    let preUpdateCtr = session.prepare('UPDATE ctr_table SET content = ?, latest_timestamp = ? WHERE uuid = ?;');
    let preUpdateCtrBlock = session.prepare('UPDATE ctr_block_table SET latest_timestamp = ? WHERE uuid = ?;');
    let ctrContent = ctrContentJSON(block.getContainer());
    let currentTimestamp = Date.now();

    const newPosId = insertPos(block.pos);

    // 1. 查询或插入新容器方块
    let {ctrBlockUuid, ctrUuid} = getCtrBlockAndCtrUUID(newPosId) || {};
    if (!ctrBlockUuid || !ctrUuid) {
        // 生成新的容器方块和容器 UUID
        ctrBlockUuid = generateUUID();
        ctrUuid = generateUUID();
        // init
        preInsertCtr.bind([ctrUuid, block.getContainer().type]);
        preInsertCtr.reexec();
        preInsertCtrBlock.bind([ctrBlockUuid, newPosId, ctrUuid]);
        preInsertCtrBlock.reexec();
    }

    // 2. 添加容器记录到 ctr_table
    preUpdateCtr.bind([ctrContent, currentTimestamp, ctrUuid]);
    preUpdateCtr.reexec();

    // 3. 添加容器记录到 ctr_block_table
    preUpdateCtrBlock.bind([currentTimestamp, ctrBlockUuid]);
    preUpdateCtrBlock.reexec();

    // 4. 插入消息
    const messageContent = JSON.stringify({
        text: `${player.name} 打开容器`,
        pos_id: newPosId
    });
    insertMsg(player, 'open_ctr', messageContent);
});

mc.listen('onCloseContainer', (player, block) => {//只能监听到箱子和木桶的关闭
    if (!block.hasContainer()) {
        return;
    }
    let preUpdateCtr = session.prepare('UPDATE ctr_table SET content = ?, latest_timestamp = ? WHERE uuid = ?;');
    let preUpdateCtrBlock = session.prepare('UPDATE ctr_block_table SET latest_timestamp = ? WHERE uuid = ?;');
    let ctrContent = ctrContentJSON(block.getContainer());
    let currentTimestamp = Date.now();

    const newPosId = insertPos(block.pos);

    // 1. 获取容器方块和容器 UUID
    let {ctrBlockUuid, ctrUuid} = getCtrBlockAndCtrUUID(newPosId) || {};
    if (!ctrBlockUuid || !ctrUuid) {
        colorLog('red', `${player.name} 关闭了未记录的箱子或木桶`);
        return;
    }

    // 2. 添加容器记录到 ctr_table
    preUpdateCtr.bind([ctrContent, currentTimestamp, ctrUuid]);
    preUpdateCtr.reexec();

    // 3. 添加容器记录到 ctr_block_table
    preUpdateCtrBlock.bind([currentTimestamp, ctrBlockUuid]);
    preUpdateCtrBlock.reexec();

    // 4. 插入消息
    const messageContent = JSON.stringify({
        text: `${player.name} 关闭容器`,
        pos_id: newPosId
    });
    insertMsg(player, 'close_ctr', messageContent);
});

mc.listen('onDestroyBlock', (player, block) => {
    colorLog('dk_yellow', `destroy_block:${player.name},${block.name}`)
    let preInsertDestruction = session.prepare('INSERT INTO block_change_table (xuid, pos_id, type, name, timestamp) VALUES (?, ?, ?, ?, ?);');
    let currentTimestamp = Date.now();

    // 1. 插入位置
    const newPosId = insertPos(block.pos);

    // 2. 插入破坏
    preInsertDestruction.bind([player.xuid, newPosId, 'destroy', block.name, currentTimestamp]);
    preInsertDestruction.reexec();

    // 3. 删除容器
    if (block.hasContainer()) {
        // 获取容器方块和容器 UUID
        let {ctrBlockUuid, ctrUuid} = getCtrBlockAndCtrUUID(newPosId) || {};
        if (!ctrBlockUuid || !ctrUuid) {
            return;
        }

        // 删除容器方块和容器
        let preDeleteCtrBlock = session.prepare('DELETE FROM ctr_block_table WHERE uuid = ?;');
        let preDeleteCtr = session.prepare('DELETE FROM ctr_table WHERE uuid = ?;');

        preDeleteCtrBlock.bind([ctrBlockUuid]);
        preDeleteCtrBlock.reexec();

        preDeleteCtr.bind([ctrUuid]);
        preDeleteCtr.reexec();
    }
});

mc.listen('afterPlaceBlock', (player, block) => {
    let preInsertPlacement = session.prepare('INSERT INTO block_change_table (xuid, pos_id, type, name, timestamp) VALUES (?, ?, ?, ?, ?);');
    let currentTimestamp = Date.now();

    // 1. 插入位置
    const newPosId = insertPos(block.pos);

    // 2. 插入添加记录
    preInsertPlacement.bind([player.xuid, newPosId, 'place', block.name, currentTimestamp]);
    preInsertPlacement.reexec();

    // 3. 添加容器
    if (block.hasContainer()) {
        let preInsertCtr = session.prepare('INSERT INTO ctr_table (uuid, name, content, latest_timestamp) VALUES (?, ?, ?, ?);');
        let preInsertCtrBlock = session.prepare('INSERT INTO ctr_block_table (uuid, pos_id, ctr_uuid, latest_timestamp) VALUES (?, ?, ?, ?);');
        let containerContent = ctrContentJSON(block.getContainer());

        // 创建容器的 UUID
        const ctrUuid = generateUUID();
        const ctrBlockUuid = generateUUID();

        // 添加容器记录到 ctr_table
        preInsertCtr.bind([ctrBlockUuid, block.getContainer().type, containerContent, currentTimestamp]);
        preInsertCtr.reexec();

        // 添加容器记录到 ctr_block_table
        preInsertCtrBlock.bind([ctrBlockUuid, newPosId, ctrUuid, currentTimestamp]);
        preInsertCtrBlock.reexec();
    }
});

mc.listen('onAttackEntity', (player, entity, damage) => {
    colorLog('dk_yellow', `attack:${player.name},${entity.name},${damage}`)
    let preInsertAttackEntity = session.prepare('INSERT INTO attack_entity_table (xuid, pos_id, damage, name, timestamp) VALUES (?, ?, ?, ?, ?);');
    let entName = entity.name ? entity.name : 'null';
    let damageNum = damage ? damage : 0;
    let currentTimestamp = Date.now();

    // 1. 插入位置
    const newPosId = insertPos(entity.blockPos);

    // 2. 插入攻击实体
    preInsertAttackEntity.bind([player.xuid, newPosId, damageNum, entName, currentTimestamp]);
    preInsertAttackEntity.reexec();
});

mc.listen('onChat', (player, msg) => {
    // 1. 插入消息
    const messageContent = JSON.stringify({
        text: `${player.name} 发送消息`,
        message: msg
    });
    insertMsg(player, 'chat', messageContent);
});

mc.listen('onLeft', (player) => {
    // 1. 更新玩家
    let newPosId = updatePlayer(player);

    // 2. 插入消息
    const messageContent = JSON.stringify({
        text: `${player.name} 离开游戏`,
        pos_id: newPosId
    });
    insertMsg(player, 'left', messageContent);
});

// 辅助函数:生成 UUID
function generateUUID() {
    return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
        let r = Math.random() * 16 | 0,
            v = c === 'x' ? r : (r & 0x3 | 0x8);
        return v.toString(16);
    });
}

// 获取容器内容 JSON
function ctrContentJSON(ctr) {
    if (ctr.isEmpty()) {
        return '{}';
    }
    let itemsArray = [];
    ctr.getAllItems().forEach((item) => {
        if (item.id !== 0) {
            let itemObj = {
                name: item.name,
                count: item.count,
            };
            itemsArray.push(itemObj);
        }
    });
    let contentJSON = JSON.stringify(itemsArray);
    return contentJSON;
}

// 插入新位置(如果不存在),并返回位置 id
function insertPos(blockPos) {
    let preSelectPos = session.prepare('SELECT id FROM pos_table WHERE x = ? AND y = ? AND z = ? AND dim_id = ?;');
    let preInsertPos = session.prepare('INSERT OR IGNORE INTO pos_table (x, y, z, dim_id) VALUES (?, ?, ?, ?);');
    let {x: newX, y: newY, z: newZ, dimid: newDimId} = blockPos;

    // 1. 插入新位置(如果不存在)
    preInsertPos.bind([newX, newY, newZ, newDimId]);
    preInsertPos.reexec();

    // 2. 查询新位置的 id
    preSelectPos.bind([newX, newY, newZ, newDimId]);
    const result = preSelectPos.reexec().fetch();
    return Object.values(result)[0];
}

// 更新玩家的位置和容器,并返回玩家位置 id
function updatePlayer(player) {
    let preUpdatePlayerPos = session.prepare('UPDATE player_table SET pos_id = ?, latest_timestamp = ? WHERE xuid = ?;');
    let preUpdatePlayerBagCtr = session.prepare('UPDATE ctr_table SET content = ?, latest_timestamp = ? WHERE uuid IN (SELECT bag_uuid FROM player_table WHERE xuid = ?);');
    let preUpdatePlayerEncCtr = session.prepare('UPDATE ctr_table SET content = ?, latest_timestamp = ? WHERE uuid IN (SELECT enc_uuid FROM player_table WHERE xuid = ?);');
    let bagContent = ctrContentJSON(player.getInventory());
    let encContent = ctrContentJSON(player.getEnderChest());
    let currentTimestamp = Date.now();
    // 1. 插入位置
    let newPosId = insertPos(player.blockPos);

    // 2. 更新玩家的 pos_id
    preUpdatePlayerPos.bind([newPosId, currentTimestamp, player.xuid]);
    preUpdatePlayerPos.reexec();

    // 3. 更新玩家背包容器和末影容器的内容以及时间戳
    preUpdatePlayerBagCtr.bind([bagContent, currentTimestamp, player.xuid]);
    preUpdatePlayerBagCtr.reexec();
    preUpdatePlayerEncCtr.bind([encContent, currentTimestamp, player.xuid]);
    preUpdatePlayerEncCtr.reexec();

    return newPosId;
}

function insertMsg(player, type, content) {
    let preInsertMsg = session.prepare('INSERT INTO msg_table (uuid, type, content, timestamp) VALUES (?, ?, ?, ?);');

    preInsertMsg.bind([generateUUID(), type, content, Date.now()]);
    preInsertMsg.reexec();
}

// 获取容器方块和容器的 UUID
function getCtrBlockAndCtrUUID(pos_id) {
    let preSelectCtrBlock = session.prepare('SELECT uuid, ctr_uuid FROM ctr_block_table WHERE pos_id = ?;');

    preSelectCtrBlock.bind([pos_id]);
    const ctr_block_result = preSelectCtrBlock.reexec().fetch();
    if (ctr_block_result && ctr_block_result.uuid && ctr_block_result.ctr_uuid) {
        return {ctrBlockUuid: Object.values(ctr_block_result)[0], ctrUuid: Object.values(ctr_block_result)[1]};
    } else {
        return null;
    }
}

function initDB() {//初始化数据库
    const dirPath = 'plugins/BB_Data';
    if (!file.exists(dirPath)) {
        colorLog('dk_yellow', `检测到数据库目录./${dirPath}不存在, 现将自动创建`);
        file.mkdir(dirPath);
    }
    const session = new DBSession('sqlite', {path: `./${dirPath}/dat.db`});
    session.exec(//位置表
        'CREATE TABLE pos_table (\\n' +
        '   id INTEGER PRIMARY KEY AUTOINCREMENT,\\n' +
        '   x INTEGER,\\n' +
        '   y INTEGER,\\n' +
        '   z INTEGER,\\n' +
        '   dim_id INTEGER\\n' +//维度id
        ');'
    );
    session.exec('CREATE UNIQUE INDEX idx_pos ON pos_table(x, y, z, dim_id);');
    session.exec(//容器表
        'CREATE TABLE ctr_table (\\n' +
        '   uuid TEXT PRIMARY KEY,\\n' +
        '   name TEXT,\\n' +//容器名字
        '   content TEXT,\\n' +//容器内容JSON
        '   latest_timestamp INTEGER\\n' +//最后更新时间戳
        ');'
    );
    session.exec(//消息表
        'CREATE TABLE msg_table (\\n' +
        '   uuid TEXT,\\n' +
        '   type TEXT,\\n' +//消息类型
        '   content TEXT,\\n' +//消息内容JSON
        '   timestamp INTEGER,\\n' +//时间戳
        '   PRIMARY KEY (uuid, timestamp)\\n' +
        ');'
    );
    session.exec(//玩家表
        'CREATE TABLE player_table (\\n' +
        '   xuid INTEGER PRIMARY KEY,\\n' +
        '   name TEXT,\\n' +
        '   pos_id INTEGER, -- 玩家位置id\\n' +
        '   bag_uuid INTEGER, -- 背包容器\\n' +
        '   enc_uuid INTEGER, -- 末影容器\\n' +
        '   latest_timestamp INTEGER, -- 最后更新时间戳\\n' +
        '   FOREIGN KEY (pos_id) REFERENCES pos_table(id),\\n' +
        '   FOREIGN KEY (bag_uuid) REFERENCES ctr_table(uuid),\\n' +
        '   FOREIGN KEY (enc_uuid) REFERENCES ctr_table(uuid)\\n' +
        ');'
    );
    session.exec(//历史位置表
        'CREATE TABLE history_pos_table (\\n' +
        '   xuid INTEGER, -- 玩家\\n' +
        '   pos_id INTEGER, -- 玩家位置id\\n' +
        '   timestamp INTEGER, -- 时间戳\\n' +
        '   PRIMARY KEY (xuid, timestamp),\\n' +
        '   FOREIGN KEY (xuid) REFERENCES player_table(xuid),\\n' +
        '   FOREIGN KEY (pos_id) REFERENCES pos_table(id)\\n' +
        ');'
    );
    session.exec(//容器方块表
        'CREATE TABLE ctr_block_table (\\n' +
        '   uuid TEXT PRIMARY KEY,\\n' +
        '   pos_id INTEGER, -- 容器位置id\\n' +
        '   ctr_uuid INTEGER, -- 容器\\n' +
        '   latest_timestamp INTEGER, -- 最后更新时间戳\\n' +
        '   FOREIGN KEY (pos_id) REFERENCES pos_table(id),\\n' +
        '   FOREIGN KEY (ctr_uuid) REFERENCES ctr_table(uuid)\\n' +
        ');'
    );
    session.exec(//破坏放置表
        'CREATE TABLE block_change_table (\\n' +
        '   xuid INTEGER, -- 玩家\\n' +
        '   pos_id INTEGER, -- 方块位置id\\n' +
        '   type TEXT, -- 动作类型\\n' +
        '   name TEXT, -- 方块名字\\n' +
        '   timestamp INTEGER, -- 时间戳\\n' +
        '   PRIMARY KEY (xuid, timestamp),\\n' +
        '   FOREIGN KEY (xuid) REFERENCES player_table(xuid),\\n' +
        '   FOREIGN KEY (pos_id) REFERENCES pos_table(id)\\n' +
        ');'
    );
    session.exec(//攻击实体表
        'CREATE TABLE attack_entity_table (\\n' +
        '   xuid INTEGER, -- 玩家\\n' +
        '   pos_id INTEGER, -- 实体位置id\\n' +
        '   damage INTEGER, -- 伤害\\n' +
        '   name TEXT, -- 实体名字\\n' +
        '   timestamp INTEGER, -- 时间戳\\n' +
        '   PRIMARY KEY (xuid, timestamp),\\n' +
        '   FOREIGN KEY (xuid) REFERENCES player_table(xuid),\\n' +
        '   FOREIGN KEY (pos_id) REFERENCES pos_table(id)\\n' +
        ');'
    );
    let dbFile = new File(`./${dirPath}/dat.db`, file.ReadMode);
    colorLog('green', `[数据记录]数据库连接完成,当前大小${dbFile.size / 1024}K`);
    dbFile.close();
    return session;
}

ll.registerPlugin('BB_Data', 'BB数据记录', [2, 0, 0, Version.Release], {});

后端代码

省略了配置的部分

Entity:

@Data
public class AttackEntityPos {
    private String playerName;
    private String entityName;
    private long damage;
    private long x;
    private long y;
    private long z;
    private byte dimId;
    private long timestamp;
}
@Data
public class BlockChangePos {
    private String playerName;
    private String blockName;
    private String act;
    private long x;
    private long y;
    private long z;
    private byte dimId;
    private long timestamp;
}
@Data
public class ContainerPos {
    private String containerName;
    private String content;
    private long x;
    private long y;
    private long z;
    private byte dimId;
    private long latestTimestamp;
}
@Data
public class Message {
    private String type;
    private String content;
    private long timestamp;
}
@Data
public class Player {
    private String playerName;
    private String bagItems;
    private String enderItems;
    private long latestTimestamp;
}
@Data
public class PlayerHistoryPos {
    private String playerName;
    private long x;
    private long y;
    private long z;
    private byte dimId;
    private long timestamp;
}

Mapper:

@Mapper
public interface AttackEntityPosMapper {
    @Select("")
    int getTotalCount(@Param("playerName") String playerName);

    @Select("")
    List findAll(int start, int limit, @Param("playerName") String playerName);
}
@Mapper
public interface BlockChangePosMapper {
    @Select("")
    int getTotalCount(@Param("playerName") String playerName);

    @Select("")
    List findAll(int start, int limit, @Param("playerName") String playerName);
}
@Mapper
public interface ContainerPosMapper {
    @Select("SELECT COUNT(*) " +
            "FROM ctr_block_table cb " +
            "JOIN ctr_table c ON cb.ctr_uuid = c.uuid")
    int getTotalCount();

    @Select("SELECT c.name AS containerName, c.content, p.x, p.y, p.z, p.dim_id AS dimId, cb.latest_timestamp AS latestTimestamp " +
            "FROM ctr_block_table cb " +
            "JOIN pos_table p ON cb.pos_id = p.id " +
            "JOIN ctr_table c ON cb.ctr_uuid = c.uuid " +
            "ORDER BY cb.latest_timestamp DESC " +
            "LIMIT #{start}, #{limit}")
    List findAll(int start, int limit);

    @Select("SELECT c.name AS containerName, c.content, p.x, p.y, p.z, p.dim_id AS dimId, cb.latest_timestamp AS latestTimestamp " +
            "FROM ctr_block_table cb " +
            "JOIN pos_table p ON cb.pos_id = p.id " +
            "JOIN ctr_table c ON cb.ctr_uuid = c.uuid " +
            "WHERE p.dim_id = #{dimId} " +
            "ORDER BY cb.latest_timestamp DESC")
    List findByDimId(@Param("dimId") int dimId);
}
@Mapper
public interface MessageMapper {
    @Select("")
    int getTotalCount(@Param("msgType") String msgType);

    @Select("")
    List findAll(int start, int limit, @Param("msgType") String msgType);
}
@Mapper
public interface PlayerHistoryPosMapper {
    @Select("")
    int getTotalCount(@Param("playerName") String playerName);

    @Select("")
    List findAll(int start, int limit, @Param("playerName") String playerName);

    @Select("SELECT x, y, z, dim_id " +
            "FROM pos_table " +
            "WHERE pos_table.id = #{pos_id}")
    PlayerHistoryPos findByPosId(int pos_id);
}
@Mapper
public interface PlayerMapper {
    @Select("SELECT COUNT(*) FROM player_table")
    int getTotalCount();

    @Select("SELECT p.name AS playerName, " +
            "c1.content AS bagItems, " +
            "c2.content AS enderItems, " +
            "p.latest_timestamp AS latestTimestamp " +
            "FROM player_table p " +
            "JOIN ctr_table c1 ON p.bag_uuid = c1.uuid " +
            "JOIN ctr_table c2 ON p.enc_uuid = c2.uuid " +
            "ORDER BY p.latest_timestamp DESC " +
            "LIMIT #{start}, #{limit}")
    List findAll(int start, int limit);

    @Select("SELECT name AS playerName FROM player_table")
    List getNameList();
}

Controller:

@RestController
@RequestMapping("/api")
public class ApiController {
    @Autowired
    private PlayerMapper playerMapper;

    @Autowired
    private PlayerHistoryPosMapper playerHistoryPosMapper;

    @Autowired
    private ContainerPosMapper containerPosMapper;

    @Autowired
    private MessageMapper messageMapper;

    @Autowired
    private BlockChangePosMapper blockChangePosMapper;

    @Autowired
    private AttackEntityPosMapper attackEntityPosMapper;

    @GetMapping("/playerList")
    public List getPlayerList(@RequestParam("start") int start, @RequestParam("limit") int limit) {
        return playerMapper.findAll(start, limit);
    }

    @GetMapping("/playerHistoryPosList")
    public List getPlayerHistoryPosList(@RequestParam("start") int start, @RequestParam("limit") int limit,
                                                          @RequestParam(value = "playerName", required = false) String playerName) {
        return playerHistoryPosMapper.findAll(start, limit, playerName);
    }

    @GetMapping("/containerPosList")
    public List getContainerPosList(@RequestParam("start") int start, @RequestParam("limit") int limit) {
        return containerPosMapper.findAll(start, limit);
    }

    @GetMapping("/messageList")
    public List getMessageList(@RequestParam("start") int start, @RequestParam("limit") int limit,
                                        @RequestParam(value = "msgType", required = false) String msgType) {
        return messageMapper.findAll(start, limit, msgType);
    }

    @GetMapping("/blockChangePosList")
    public List getBlockChangePosList(@RequestParam("start") int start, @RequestParam("limit") int limit,
                                                      @RequestParam(value = "playerName", required = false) String playerName) {
        return blockChangePosMapper.findAll(start, limit, playerName);
    }

    @GetMapping("/attackEntityPosList")
    public List getAttackEntityPosList(@RequestParam("start") int start, @RequestParam("limit") int limit,
                                                        @RequestParam(value = "playerName", required = false) String playerName) {
        return attackEntityPosMapper.findAll(start, limit, playerName);
    }

    @GetMapping("/totalPlayerCount")
    public int getTotalPlayerCount() {
        return playerMapper.getTotalCount();
    }

    @GetMapping("/totalPlayerHistoryPosCount")
    public int getTotalPlayerHistoryPosCount(@RequestParam(value = "playerName", required = false) String playerName) {
        return playerHistoryPosMapper.getTotalCount(playerName);
    }

    @GetMapping("/totalContainerPosCount")
    public int getTotalContainerPosCount() {
        return containerPosMapper.getTotalCount();
    }

    @GetMapping("/totalMessageCount")
    public int getTotalMessageCount(@RequestParam(value = "msgType", required = false) String msgType) {
        return messageMapper.getTotalCount(msgType);
    }

    @GetMapping("/totalBlockChangePosCount")
    public int getTotalBlockChangePosCount(@RequestParam(value = "playerName", required = false) String playerName) {
        return blockChangePosMapper.getTotalCount(playerName);
    }

    @GetMapping("/totalAttackEntityPosCount")
    public int getTotalAttackEntityPosCount(@RequestParam(value = "playerName", required = false) String playerName) {
        return attackEntityPosMapper.getTotalCount(playerName);
    }

    @GetMapping("/playerNameList")
    public List getPlayerNameList() {
        return playerMapper.getNameList();
    }

    @GetMapping("/pos")
    public PlayerHistoryPos getPos(@RequestParam("pos_id") int pos_id) {
        return playerHistoryPosMapper.findByPosId(pos_id);
    }

    @GetMapping("/containerPosListByDimId")
    public List getContainerPosListByDimId(@RequestParam("dimId") int dimId) {
        return containerPosMapper.findByDimId(dimId);
    }
}

Application:

@SpringBootApplication
public class BbDataServerApplication {

    public static void main(String[] args) {
        SpringApplication.run(BbDataServerApplication.class, args);
    }

    @Bean
    public WebMvcConfigurer corsConfigurer() {
        return new WebMvcConfigurer() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                // 生产环境中,需要将 "*" 替换为实际的前端域名
                registry.addMapping("/**").allowedOrigins("*");
            }
        };
    }
}

前端代码

App:


main:

import {
	nextTick,
	createApp
} from 'vue';
import {
	createRouter,
	createWebHistory
} from 'vue-router';
import App from './App.vue';
import ElementPlus from 'element-plus';
import 'element-plus/theme-chalk/index.css';
import './assets/global.css';

import HomeView from './views/HomeView.vue';
import PosView from './views/PosView.vue';
import TabView from './views/TabView.vue';

const routes = [{
		path: '/',
		name: 'HomeView',
		component: HomeView,
	},
	{
		path: '/pos',
		name: 'PosView',
		component: PosView,
	},
	{
		path: '/tab',
		name: 'TabView',
		component: TabView,
	},
];

const router = createRouter({
	history: createWebHistory(),
	routes,
});

const app = createApp(App);
app.use(ElementPlus);
app.use(router);
app.mount('#app');

assets(global.css):

	/* 通用文字色 */
	#text {
		color: #27342b;
	}

	/* 头栏的行内容填满 */
	#header-row {
		width: 100%;
		height: 100%;
		display: flex;
		align-items: center;
	}

	/* 头栏每列内容居中 */
	#header-col {
		display: flex;
		align-items: center;
		justify-content: center;
	}

	#page-header {
		padding-left: 36px;
		padding-top: 1vh;
		padding-bottom: 1vh;
		border: 2px dashed #27342b;
		border-radius: 4px;
	}

	/* 滑动条样式 */
	.el-slider__button {
		width: 25px !important;
		height: 15px !important;
		background: #ffffff !important;
		border-color: #27342b !important;
		border-radius: 4px !important;
	}

	.el-slider__bar {
		background-color: #f6f5ec !important;
	}

	.el-slider__runway {
		background-color: #f6f5ec !important;
		border-radius: 2 !important;
	}
	
	.el-button {
		border: 2px solid #27342b !important;
		border-radius: 4px;
		background-color: white !important;
	}
	
	.el-button:hover {
		background-color: #27342b !important;
	}
	
	#tab-expand {
		margin-left: 40px;
		margin-right: 40px;
	}

components:





























utils:

//api.js
import axios from 'axios';

const baseURL = '你的后端URL/api';

export default {
	async fetchTotalPlayerCount() {
		const response = await axios.get(`${baseURL}/totalPlayerCount`);
		return response.data;
	},
	async fetchTotalPlayerHistoryPosCount(playerName) {
		const response = await axios.get(`${baseURL}/totalPlayerHistoryPosCount`, {
			params: {
				playerName
			}
		});
		return response.data;
	},
	async fetchTotalContainerPosCount() {
		const response = await axios.get(`${baseURL}/totalContainerPosCount`);
		return response.data;
	},
	async fetchTotalMessageCount(msgType) {
		const response = await axios.get(`${baseURL}/totalMessageCount`, {
			params: {
				msgType
			}
		});
		return response.data;
	},
	async fetchTotalBlockChangePosCount(playerName) {
		const response = await axios.get(`${baseURL}/totalBlockChangePosCount`, {
			params: {
				playerName
			}
		});
		return response.data;
	},
	async fetchTotalAttackEntityPosCount(playerName) {
		const response = await axios.get(`${baseURL}/totalAttackEntityPosCount`, {
			params: {
				playerName
			}
		});
		return response.data;
	},
	async fetchPlayerList(start, limit) {
		const response = await axios.get(`${baseURL}/playerList`, {
			params: {
				start,
				limit
			},
		});
		return response.data;
	},
	async fetchPlayerHistoryPosList(start, limit, playerName) {
		const response = await axios.get(`${baseURL}/playerHistoryPosList`, {
			params: {
				start,
				limit,
				playerName
			},
		});
		return response.data;
	},
	async fetchContainerPosList(start, limit) {
		const response = await axios.get(`${baseURL}/containerPosList`, {
			params: {
				start,
				limit
			},
		});
		return response.data;
	},
	async fetchMessageList(start, limit, msgType) {
		const response = await axios.get(`${baseURL}/messageList`, {
			params: {
				start,
				limit,
				msgType
			},
		});
		return response.data;
	},
	async fetchBlockChangePosList(start, limit, playerName) {
		const response = await axios.get(`${baseURL}/blockChangePosList`, {
			params: {
				start,
				limit,
				playerName
			},
		});
		return response.data;
	},
	async fetchAttackEntityPosList(start, limit, playerName) {
		const response = await axios.get(`${baseURL}/attackEntityPosList`, {
			params: {
				start,
				limit,
				playerName
			},
		});
		return response.data;
	},
	async fetchTotalPlayerNameList() {
		const response = await axios.get(`${baseURL}/playerNameList`);
		return response.data;
	},
	async fetchPosById(id) {
		const response = await axios.get(`${baseURL}/pos`, {
			params: {
				pos_id: id
			}
		});
		return response.data;
	},
	async fetchContainerPosListByDimId(dimId) {
		const response = await axios.get(`${baseURL}/containerPosListByDimId`, {
			params: {
				dimId
			},
		});
		return response.data;
	},
};
//tools.js
export default {
	getDimName(dimId) {
		switch (dimId) {
			case -1:
				return '未知';
			case 0:
				return '主世界';
			case 1:
				return '下界';
			case 2:
				return '末地';
			default:
				return '未知';
		}
	},
	getTimeStr(currentTime, value) {
		let timeString = new Date(value).toLocaleString();
		timeString += `(${this.getTimeAgo(currentTime, value)})`;
		return timeString;
	},
	getTimeAgo(currentTime, value) {
		const recordTime = new Date(value);
		const diffInSeconds = Math.floor((currentTime - recordTime) / 1000);
		if (diffInSeconds < 60) {
			return `${diffInSeconds} 秒前`;
		}
		const diffInMinutes = Math.floor(diffInSeconds / 60);
		if (diffInMinutes < 60) {
			return `${diffInMinutes} 分钟前`;
		}
		const diffInHours = Math.floor(diffInMinutes / 60);
		if (diffInHours < 24) {
			return `${diffInHours} 小时前`;
		}
		const diffInDays = Math.floor(diffInHours / 24);
		return `${diffInDays} 天前`;
	}
}

views:




















项目总结

  • 运行时占内存的大头是 LiteloaderBDS,项目的逻辑集中在插件和前端部分
  • 后端的安全性和容错性不足
  • 前端的地图相机选点功能不够易用,需要改进
  • 数据库部分并发处理不好,可能需要重新设计

你可能感兴趣的:(java,javascript,spring,boot,vue.js,sqlite)