邪修实战系列(6)


1、第一阶段邪修实战总览(9.1-9.30)

把第一阶段(基础夯实期)的学习计划拆解成极具操作性的每日行动方案。这个计划充分利用我"在职学习"的特殊优势,强调"用输出倒逼输入",确保每一分钟的学习都直接服务于面试和实战。

  • 核心目标:构建起Java后端开发的知识树主干,并能通过一个小型项目串联起所有知识点。
  • 核心策略:每天3小时雷打不动的高效学习(工作日可分散,周末集中攻坚)。

2、目标(9.15-9.21)

用一周时间来强化!要能熟练编写SQL语句,并详细了解前端如何调用咱们写的API。我们现在不能再满足于"跑通",而是要"理解"和"掌控"。最终产出是一个功能完整、结构清晰、可以展示的广告数据管理平台。

学习重点与行动概述

2.1、MySQL

  • 行动:把安装和使用MySQL的基本方法烂熟于心。重点练习增删改查、联表查询(JOIN)、分组统计(GROUP BY)。就用咱们自己之前项目的广告数据表做练习。
  • 邪修技巧:==一定要手敲SQL语句!!!==图形化工具确实现在很好用,后期确实可以提高效率,但是!!!我们面试的时候很有可能会遇到需要手写SQL的情况,而且这也确实是基本功,整理一下核心语句每天手写一边练出肌肉记忆!!

2.2、前端三剑客

  • 行动:不要学Vue/React!我们的目标是"具备Web开发经验",而不是成为前端专家。用一天的时间快速过一遍HTML表单、CSS基础、JavaScript的Ajax(用jQery或Axios库发表请求)。
  • 邪修技巧:最终目标是能够更加深刻的了解我们之前写的那个项目的前后端是如何交互的!

2.3、综合练习:

  • 行动:将我们的项目进行优化,优化我们的前端页面,并为我们的项目增加一些新的功能。

3、学习分享

3.1、MySQL核心

  • 行动:我们可以通过使用Navicat创建数据库,在其中手动创建数据表,对照我们的实体类设计字段,对照我们项目的实体类设计字段;并在Navicat的查询窗口中先通过可视化点一点,再看它生成的SQL代码,最后手写各种SQL语句,这是最快的学习方式。
  • 邪修技巧:学习数据库相关知识,本人推荐一篇博客、一个网页和一个软件哈,自己搭配着使用感觉效率还是蛮高的。
    • W3Cchool SQL教程可以把他作为一个系统学习的工具,肯定是需要系统学习一下的,这个自己量力而行来。
    • 最后再在手机上下载力扣,当然也有网页端力扣,通过刷题实操来巩固知识。

3.2、前端三剑客核心

  • 行动:能看懂并修改HTML/JS/CSS,理解它们如何协作
    • HTML:看懂表单<form>、输入框<input>、表格<table>div等标签;
    • CSS:理解Bootstrap的类(如btn btn-primary)是如何工作的。尝试微调样式(如改颜色、边距);
    • JavaScript:深挖jQuery Ajax。彻底搞懂$.ajax({url, type, data, success, error})这几个参数的含义。
  • 邪修技巧:
    • 可以通过把MDN Web文档当作权威参考,遇到不懂的标签或API就去查;
    • 在浏览器中按F12打开开发者工具,使用"元素(Elements)"面板查看HTML/CSS,使用"控制台(Console)"测试JS代码,这是我们学习前端的最强神器。

3.3、综合实战:增删改查+前端联调美化+打包部署文件

3.3.1、实现删除功能

后端(AdController.java):

java 复制代码
@DeleteMapping("/{id}") // 映射 DELETE /api/ads/1 这样的请求
public String deleteAd(@PathVariable Long id) {
    if (adRepository.existsById(id)) { // 检查ID是否存在
        adRepository.deleteById(id);
        return "广告删除成功!";
    } else {
        // 更友好的做法是返回404状态码,这里简单处理
        return "广告不存在!";
    }
}

前端(main.js):在loadAds()函数中,为每一行数据添加删除按钮和事件:

javascript 复制代码
// 在append表格行的代码内部
$('#adTable tbody').append(
    '<tr>' +
    '   <td>' + ad.id + '</td>' +
    '   <td>' + ad.adName + '</td>' +
    '   ...其他字段...' +
    '   <td><button class="btn btn-danger btn-sm" onclick="deleteAd(' + ad.id + ')">删除</button></td>' +
    '</tr>'
);

// 新增删除函数
function deleteAd(adId) {
    if (confirm('确定要删除这条广告吗?')) {
        $.ajax({
            type: 'DELETE',
            url: '/api/ads/' + adId, // 将ID作为URL的一部分
            success: function(data) {
                alert(data); // 显示后端返回的消息
                loadAds(); // 重新加载数据
            },
            error: function() {
                alert('删除失败!');
            }
        });
    }
}

3.3.2、实现更新功能

后端(AdController.java

java 复制代码
@PutMapping("/{id}") // 映射 PUT /api/ads/1 这样的请求
public AdAdvertisement updateAd(@RequestBody AdAdvertisement updatedAd, @PathVariable Long id) {
    // 简单的实现:先查出来,再替换值,最后保存
    return adRepository.findById(id)
        .map(ad -> {
            ad.setAdName(updatedAd.getAdName());
            ad.setCost(updatedAd.getCost());
            // ... 设置其他字段
            return adRepository.save(ad);
        })
        .orElseGet(() -> {
            // 如果找不到,可以选择返回null或抛出异常
            return null;
        });
}

3.3.3、添加"编辑功能":

  • 首先我们先找一个模态框,为我们接下来的弹出窗做一些准备,复制一个Bootstrap官方文档中的Modal示例,我们要在index.heml<body>末尾,并修改其中的表单字段。
  • 打开Bootstrap官方文档,然后找一个自己想更新的模态框,我找的是Static backdrop,如下图所示
  • 官方文档中的示例代码如下:
html 复制代码
<!-- Button trigger modal -->
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#staticBackdrop">
  Launch static backdrop modal
</button>

<!-- Modal -->
<div class="modal fade" id="staticBackdrop" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" aria-labelledby="staticBackdropLabel" aria-hidden="true">
  <div class="modal-dialog">
    <div class="modal-content">
      <div class="modal-header">
        <h1 class="modal-title fs-5" id="staticBackdropLabel">Modal title</h1>
        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
      </div>
      <div class="modal-body">
        ...
      </div>
      <div class="modal-footer">
        <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
        <button type="button" class="btn btn-primary">Understood</button>
      </div>
    </div>
  </div>
</div>
  • 这里注意除了需要更改显影的接口设置,还需要引入一个版本更新的Bootstrap javaScript和一个错误提示容器,所以整体的index.html的代码如下:
html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>广告数据管理面板</title>
    <!-- 引入Bootstrap CSS,用于快速美化页面,无需自己写CSS -->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="container mt-4">

<h1>广告数据管理</h1>
<p>这是一个调用后端API的简易管理界面。</p>

<!-- 数据展示表格 -->
<h2 class="mt-4">数据列表</h2>
<table class="table table-striped table-bordered" id="adTable">
    <thead>
    <tr>
        <th>广告日期</th>
        <th>广告花费</th>
        <th>线索数量</th>
        <th>私信数量</th>
    </tr>
    </thead>
    <tbody>
    <!-- 数据会通过JavaScript动态填充到这里 -->
    </tbody>
</table>

<!-- 添加数据的表单 -->
<h2 class="mt-4">添加新广告</h2>
<form id="adForm" class="mb-5">
    <div class="mb-3">
        <label class="form-label">广告日期</label>
        <input type="text" class="form-control" name="date" required>
    </div>
    <div class="mb-3">
        <label class="form-label">广告花费</label>
        <input type="number" class="form-control" name="cost" required>
    </div>
    <div class="mb-3">
        <label class="form-label">线索数量</label>
        <input type="number" step="0.01" class="form-control" name="leadCount" required>
    </div>
    <div class="mb-3">
        <label class="form-label">私信数量</label>
        <input type="number" step="0.01" class="form-control" name="messageCount" required>
    </div>
    <button type="submit" class="btn btn-primary">
        提交
    </button>
    </div>
</form>

<!-- 引入jQuery(简化JS操作)和Bootstrap JavaScript -->
<script src="https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.min.js"></script>
<!-- 引入Bootstrap JavaScript -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script>
<!-- 引入自定义的JS脚本 -->
<script src="js/main.js"></script>

<!-- Modal静态背景弹出框 -->
<div class="modal fade" id="staticBackdrop" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" aria-labelledby="staticBackdropLabel" aria-hidden="true">
    <div class="modal-dialog">
        <div class="modal-content">
            <div class="modal-header">
                <h1 class="modal-title fs-5" id="staticBackdropLabel">编辑提示</h1>
                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
            </div>
            <div class="modal-body">
                <div class="mb-3">
                    <label class="form-label">广告日期</label>
                    <input type="text" class="form-control" name="editDate" data-field="date" required>
                </div>
                <div class="mb-3">
                    <label class="form-label">广告花费</label>
                    <input type="number" class="form-control" name="editCost" data-field="cost" required>
                </div>
                <div class="mb-3">
                    <label class="form-label">线索数量</label>
                    <input type="number" step="0.01" class="form-control" name="editLeadCount" data-field="leadCount" required>
                </div>
                <div class="mb-3">
                    <label class="form-label">私信数量</label>
                    <input type="number" step="0.01" class="form-control" name="editMessageCount" data-field="messageCount" required>
                </div>
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
                <button type="button" class="btn btn-primary" id="saveEdit">确定</button>
            </div>
        </div>
    </div>
</div>

<!-- Toast错误提示容器(放在body末尾) -->
<div class="toast-container position-fixed bottom-0 end-0 p-3">
    <div id="errorToast" class="toast bg-danger text-white" role="alert">
        <div class="toast-body">
            <span id="toast-body"></span>
        </div>
    </div>
</div>

</body>
</html>
  • 添加"编辑"按钮:类似删除按钮,在main.js的加载广告数据的函数中增加按钮"onclick="editAd(" + ad.id + ")""。
javascript 复制代码
'   <td><button class="btn btn-primary btn-sm" onclick="editAd(' + ad.id + ')">编辑</button></td>' +
  • 接下来我们来编写JS函数:
    • 编写editAd(id)编辑功能函数:发送GET请求获取该ID的详细数据,用数据填充Modal表单,然后显示Modal。
    • 再Modal的"保存"按钮上,绑定事件,收集表单数据,发送PUT请求到/api/ads/{id}中,整体main.js代码如下所示:
javascript 复制代码
// 页面加载完成后执行
$(document).ready(function() {
    // 1. 加载并显示所有数据
    loadAds();

    // 2. 监听表单提交事件
    $('#adForm').on('submit', function(event) {
        event.preventDefault(); // 阻止表单默认提交行为

        // 收集表单数据,并转换成JSON对象
        const formData = {
            date: $('input[name="date"]').val(),
            cost: $('input[name="cost"]').val(),
            leadCount: parseInt($('input[name="leadCount"]').val()),
            messageCount: parseInt($('input[name="messageCount"]').val())
        };

        // 3. 发送POST请求到后端API
        $.ajax({
            type: 'POST',
            url: '/api/ads', // 这是你后端Controller的地址
            contentType: 'application/json', // 告诉后端发送的是JSON
            data: JSON.stringify(formData), // 将JS对象转换为JSON字符串
            success: function(data) {
                // 请求成功
                alert('添加成功!');
                $('#adForm')[0].reset(); // 清空表单
                loadAds(); // 重新加载广告数据
            },
            error: function() {
                // 请求失败
                alert('添加失败,请检查控制台日志!');
            }
        });
        $('#staticBackdrop').modal('hide'); // 关闭模态框
    });
    
    // 新增:监听保存编辑按钮点击事件
    $('#saveEdit').on('click', function() {
        // 获取当前编辑的广告ID
        const adId = $('#staticBackdrop').data('currentAd');
        
        // 收集表单数据
        const formData = {
            date: $('input[name="editDate"]').val(),
            cost: $('input[name="editCost"]').val(),
            leadCount: parseInt($('input[name="editLeadCount"]').val()),
            messageCount: parseInt($('input[name="editMessageCount"]').val())
        };
        
        // 发送PUT请求到后端API更新数据
        $.ajax({
            type: 'PUT',
            url: '/api/ads/' + adId,
            contentType: 'application/json',
            data: JSON.stringify(formData),
            success: function(data) {
                alert('编辑成功!');
                $('#staticBackdrop').modal('hide'); // 关闭模态框
                loadAds(); // 重新加载广告数据
            },
            error: function() {
                alert('编辑失败,请检查控制台日志!');
            }
        });
    });
});
// 新增删除函数
function deleteAd(adId) {
    if (confirm('确定要删除这条广告吗?')) {
        $.ajax({
            type: 'DELETE',
            url: '/api/ads/' + adId, // 将ID作为URL的一部分
            success: function(data) {
                alert(data); // 显示后端返回的消息
                loadAds(); // 重新加载广告数据
            },
            error: function() {
                alert('删除失败!');
            }
        });
    }
}

// 优化后的编辑函数(Bootstrap 5规范)
function editAd(adId) {
    // 预加载模态框实例
    const editModal = new bootstrap.Modal('#staticBackdrop');

    // 发送 GET 请求获取广告数据
    $.ajax({
        type: 'GET',
        url: `/api/ads/${encodeURIComponent(adId)}`, // 安全编码
        dataType: 'json' // 明确响应类型
    }).done(data => {
        // 动态字段映射
        const fieldMap = {
            date: data.date,
            cost: data.cost,
            leadCount: data.leadCount,
            messageCount: data.messageCount
        };

        // 动态填充模态框字段
        Object.entries(fieldMap).forEach(([field, value]) => {
            const input = $(`#staticBackdrop [data-field="${field}"]`);
            if (input.length) {
                input.val(value ?? ''); // 处理空值
            } else {
                console.warn(`未找到字段: ${field}`);
            }
        });

        // 设置模态框中的隐藏 ID 域
        $('#staticBackdrop').data('currentAd', adId);

        // 显示模态框
        editModal.show();
    }).fail((xhr, status, error) => {
        // 增强错误处理
        console.error('AJAX 错误:', {
            status: status,
            error: error,
            responseText: xhr.responseText
        });
        const errorMsg = `获取数据失败: ${xhr.status} ${xhr.statusText}`;
        showErrorToast(errorMsg);
    });
}


// Toast提示组件示例
function showErrorToast(msg) {
    const toast = new bootstrap.Toast(document.getElementById('errorToast'));
    $('#toast-body').text(msg);
    toast.show();
}


// 加载广告数据的函数
function loadAds() {
    $.ajax({
        type: 'GET',
        url: '/api/ads', // 调用GET接口
        success: function(date) {
            // 清空表格现有数据
            $('#adTable tbody').empty();
            // 遍历返回的数据数组
            $.each(date, function(index, ad) {
                // 将每条数据追加为表格的一行
                $('#adTable tbody').append(
                    '<tr>' +
                    '   <td>' + ad.date + '</td>' +
                    '   <td>' + ad.cost + '</td>' +
                    '   <td>' + ad.leadCount + '</td>' +
                    '   <td>' + ad.messageCount + '</td>' +
                    '   <td><button class="btn btn-danger btn-sm" onclick="deleteAd(' + ad.id + ')">删除</button></td>' +
                    '   <td><button class="btn btn-primary btn-sm" onclick="editAd(' + ad.id + ')">编辑</button></td>' +
                    '</tr>'
                );
            });
        },
        error: function() {
            alert('加载数据失败!');
        }
    });
}

3.3.4、生成可独立运行的.jar文件

Spring Boot最大的优势之一就是内嵌了Web服务器(如Tomcat),因此咱们不需要额外安装Tomcat,只需要一个Java运行环境(JRE)就能运行我们的项目了。
第一步:准备工作 - 检查与配置

  • 确保pom.xml配置正确:我们的 pom.xml 中应该已经包含了 spring-boot-starter-web 依赖,并且打包方式为 jar。这是Spring Initializr创建项目的默认配置,但最好还是检查一下。
xml 复制代码
<!-- 1. 确认打包方式为 jar (默认就是,通常不用显式写出来) -->
<packaging>jar</packaging>

<!-- 2. 确认有 Spring Boot Maven 插件 -->
<!-- 这个插件是打包的关键!它会将所有依赖打包成一个可执行的"fat jar" -->
<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>
  • 调整应用配置(application.properties):在打包用于生产环境的版本前,强烈建议创建一个生产环境的配置文件,或者确保我们的默认配置适合生产环境。
  • 关闭开发特性:在生产环境,你不希望Hibernate自动修改你的数据库表结构。
bash 复制代码
# 在 src/main/resources/application.properties 中修改
spring.jpa.hibernate.ddl-auto=validate # 或 none
# validate: 启动时验证数据库表结构与实体类是否匹配,不匹配则报错,更安全。
# none: 什么都不做。
  • 指定生产环境数据库:我们肯定不会想用本地数据库。但目前阶段,我们先确保打包后程序能连接到你本地的MySQL即可。确保数据库服务是启动的。
    第二步:使用 Maven 进行打包
    Maven插件会帮助我们完成所有复杂的工作:编译代码、运行测试、下载依赖、打包。
  • 在IDEA的右侧,找到Maven标签页,展开我们的项目->Lifecycle->clean+package,记得要双击。
    • clean:清理旧的编译文件,避免缓存问题。
    • package:执行打包操作。
  • 执行成功后,我们会在终端看到 BUILD SUCCESS 的字样,如下图所示。

    第三步:找到并验证生成的Jar包
  • 打包完成后,在根目录下会生成一个target文件夹。
  • 进入target文件夹,我们会找到打包生成的文件。其中有两个jar文件:
    • your-project-name-0.0.1-SNAPSHOT.jar:这是可执行的"fat jar",它包含了你的所有代码和依赖库。文件大小通常有15-30MB。
    • your-project-name-0.0.1-SNAPSHOT.jar.original:这是普通的jar包,只包含你自己的代码,很小。我们需要的是那个大的fat jar。
      第四步:运行我们的Jar包
      现在是最激动人心的时刻,我们不再需要IDE了!!!
  • 在终端中,cd进入target目录,使用java -jar命令命令运行我们的项目,如下图所示:
bash 复制代码
java -jar your-project-name-0.0.1-SNAPSHOT.jar
  • 恭喜!我们的项目现在已经作为一个独立的应用程序运行了!完整的代码放到了Github中,收藏Data_Board_Maven_CRUD自取嗷~
  • 我们可以通过打开浏览器访问对应的路径并测试前端页面,进行增删改查,确保所有功能能够正常运行。

总结

  • 遇到问题-第一时间F12:看Console报错、看Network请求状态码和响应体。90%的问题都能在这里找到答案。
  • 抄、改、试:不要从零开始写。从Bootstrap文档抄组件,从我们的旧代码里面修改逻辑,然后立即测试。
  • 目标驱动:本周的唯一目标就是做出CRUD功能。所有的学习都是按照这个目标进行,不懂得细节先记下来,日后研究。

如何介绍我们的项目?

我们独立完成了一个广告数据管理平台的全栈开发。它是一个前后端分离的项目,后端用Spring Boot提供RESTful API,前端用Bootstrap和jQuery构建界面,实现了广告数据的增删改查、筛选和可视化展示。通过这个项目,我将Java后端知识和前端实践完整地串联了起来,也解决了不少实际问题。

项目详述(技术栈与职责)

在这个项目中,我们主要负责了后端API的设计与实现和前端页面的开发。

  • 后端:我使用Spring Boot快速搭建了项目框架,用Spring Data JPA作为ORM层来操作MySQL数据库,这极大地简化了数据库的增删改查操作。我设计了/api/ads等相关RESTful接口来处理前端请求。
  • 前端:考虑到这是一个以数据管理为核心的内部后台系统,开发效率和使用体验是关键。我没有选择重型的Vue/React框架,而是采用了Bootstrap + jQuery的方案。Bootstrap让我能快速搭建出美观且响应式的界面,而jQuery的Ajax功能则完美地满足了与后端API交互的需求。这个技术选型让我在很短的时间内就完成了开发目标。
  • 数据库:我使用MySQL存储数据,并通过Navicat进行可视化的管理和调试。"

深入难点与解决方案(展示能力)

在开发过程中,我遇到一个印象比较深的问题。前端页面表格中的数据一直显示为undefined。

  • 排查:我第一时间打开浏览器的开发者工具,在'网络(Network)'面板里发现后端API返回的数据结构包含了adName、cost等字段,而我前端的JavaScript代码却在试图读取adDate、leadsCount等不存在的字段。
  • 分析:我立刻意识到这是前后端数据模型不一致导致的。
  • 解决:我并没有盲目地瞎试,而是根据项目现状做出了决策:因为后端API已经基本完善,所以我选择修改前端代码来适配后端的数据结构。我调整了JavaScript中获取数据的逻辑以及表格的列显示,很快就解决了问题。
    这个过程让我深刻体会到前后端协同开发中,定义清晰、统一的接口规范是多么重要。

项目总结与收货

通过这个项目,我最大的收获有三点:

  • 第一,是对一个完整Web应用的生命周期有了切身的体会。从数据库设计、API编写到前端交互和最终打包部署,我独立走完了全程,这对我的代码架构能力提升非常大。
  • 第二,我学会了如何高效地解决问题。无论是查阅官方文档、在社区搜索,还是通过调试工具定位问题,我的自学和排错能力都得到了极大的锻炼。
  • 最后,这个项目让我坚定了成为Java后端工程师的决心。我享受在后端构建稳定、高效API的逻辑过程,也希望能在这个方向上深入学习微服务、分布式等高阶技术,为公司创造价值。

Spring Boot的核心优势

  • 我个人认为是自动配置(Auto-Configuration)和起步依赖(Starer Dependencies)。它简化了传统Spring应用的繁琐配置,让我们能快速搭建一个独立、生产级的应用,真正做到了开箱即用。

JPA和Mybatis的区别以及本项目用JPA的原因

  • Mybits更偏向于对SQL的灵活掌控,需要手写SQL和ResultMap。而JPA的优势在于开发效率,它通过方法命名规约和API,让我几乎不写SQL就能完成绝大部分CRUD操作。在我们的项目中,业务逻辑相对简单,所以JPA的高效性更合适。

本项目实现前后端联调的过程

  • 我们主要使用Postman来测试后端的每一个API接口,确保它们返回的数据结构和状态码是正确的。后端调试通过后,再在前端用jQuery的Ajax来调用这些接口,并在浏览器的开发者工具中查看网络请求和相应,确保前后端数据交互无误。

相关推荐
Lei活在当下19 分钟前
先用起来,再理解,关于协程Coroutine应该知道的事
android·java·jvm
Java爱好狂.35 分钟前
Java程序员体系化学习路线(2026最新版)
java·后端·java面试·java架构师·java程序员·java八股文·java学习路线
网络与设备以及操作系统学习使用者1 小时前
Linux与Windows核心差异深度解析
linux·运维·网络·windows·学习
tongluowan0071 小时前
以ReentrantLock为例解释AQS的工作流程
java·模板方法模式·aqs·reentrantlock
装不满的克莱因瓶1 小时前
SpringBoot 如何将 lib 目录中jar包打包进最终的jar包里面
spring boot·后端·maven·jar·mvn
佚泽1 小时前
Android Studio 如何配置gradle
android·ide·android studio
身如柳絮随风扬2 小时前
Java 项目打包与部署完全指南:JAR vs WAR,从构建到运行
java·firefox·jar
云烟成雨TD2 小时前
Spring AI Alibaba 1.x 系列【62】时光旅行(Time-Travel)
java·人工智能·spring
浩少7023 小时前
【无标题】
java·开发语言
一棵白菜3 小时前
java 学习
java