Java+MySQL+Swing图书管理系统实战:数据闭环与生产级设计

发布时间:2026/10/7 12:59:03
Java+MySQL+Swing图书管理系统实战:数据闭环与生产级设计 简介本资源是一套完整的Java课程设计项目——基于Swing GUI与MySQL数据库开发的图书管理系统面向计算机专业本科生及Java初学者解决图书馆基础业务管理需求涵盖管理员登录、图书增删改查、用户信息维护、借阅归还全流程功能。压缩包共187个文件含48个核心Java源码如BookDao、ModifyWindow、FindBookWindow等、64个编译后class文件、58张界面资源png图、4个SQL建表与初始化脚本、4份Word文档说明及2份Markdown使用指南整体16.25MB结构清晰便于分模块学习与调试。已有999人下载学习提供可直接运行的GUI桌面程序、完整数据库设计与交互逻辑、典型Swing组件封装实践及常见SQL操作示例助读者深入理解面向对象编程、JDBC连接、事件驱动机制与三层架构雏形。1. 图书管理系统不是 Demo用 Java MySQL GUI 落地一个能真入库、可查重、带借阅状态的桌面应用这不是一个“Swing 写个窗口JDBC 连个库”的教学玩具。我去年帮高校继续教育学院重构他们的教材借用登记系统就是从这个标题出发——Java 做业务逻辑层、MySQL 存真实借阅流水、GUI 界面要支持扫码录入 ISBN、批量导入 Excel、按班级/学期筛选未归还记录。它解决的是纸质登记本易丢、Excel 表多人共编易冲突、微信接龙无法校验库存、Web 系统在机房断网时完全瘫痪这四类真实痛点。适合刚学完 JDBC 和 Swing 的开发者也适合需要快速交付轻量级内部管理工具的中小团队。核心不在炫技而在数据闭环录入 → 库存扣减 → 借阅记录生成 → 归还触发库存回填 → 报表导出。所有操作必须落库、可审计、不丢数据。下面每一步我都用自己部署在 Windows Server 2019 MySQL 8.0.33 OpenJDK 17 环境下的生产实例验证过。2. 用 Java Swing 搭建可维护 GUI不是拖控件而是分层设计窗口与事件流2.1 主窗口结构用 CardLayout 实现「主菜单 ↔ 功能页」无刷新切换很多初学者直接把所有按钮、表格堆在 JFrame 里结果改一个借书功能就要重写整个界面。我采用CardLayout JPanel 分页容器主窗口只保留顶部菜单栏和中央卡片面板。每个功能图书管理、借阅登记、归还处理、统计报表封装为独立 JPanel 子类通过cardLayout.show(cardPanel, borrowPanel)切换。这样做的好处是各功能页代码隔离修改借阅逻辑不影响图书录入页可单独测试每个 JPanel例如new BorrowPanel().setVisible(true)避免setVisible(false)堆积导致内存泄漏Swing 组件不销毁。// MainFrame.java - 主窗口初始化关键片段 public class MainFrame extends JFrame { private CardLayout cardLayout; private JPanel cardPanel; public MainFrame() { setTitle(图书管理系统 v2.3); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setLayout(new BorderLayout()); // 顶部菜单栏固定 JMenuBar menuBar createMenuBar(); setJMenuBar(menuBar); // 中央卡片面板动态切换 cardPanel new JPanel(); cardLayout new CardLayout(); cardPanel.setLayout(cardLayout); // 注册各功能页注意实例化但不 setVisible cardPanel.add(new BookManagementPanel(), bookPanel); cardPanel.add(new BorrowPanel(), borrowPanel); cardPanel.add(new ReturnPanel(), returnPanel); cardPanel.add(new ReportPanel(), reportPanel); add(cardPanel, BorderLayout.CENTER); pack(); setLocationRelativeTo(null); } }提示不要在JFrame构造函数里调用setVisible(true)应在所有组件初始化完毕、数据库连接确认可用后再统一显示。否则可能因数据库未就绪导致界面卡死。2.2 表格组件用 JTable DefaultTableModel 实现「双击编辑 回车保存」的库存联动图书列表页必须支持双击某行「库存数」列直接编辑、按回车立即更新 MySQL、同时刷新同页其他关联字段如“可借数量 库存 - 已借出”。这里不能用JTable#setEditable(true)简单粗暴因为需校验输入合法性非负整数、触发数据库更新、并同步刷新计算列。// BookManagementPanel.java - 表格编辑核心逻辑 private void setupBookTable() { String[] columns {ID, ISBN, 书名, 作者, 出版社, 库存, 已借出, 可借}; DefaultTableModel model new DefaultTableModel(columns, 0) { Override public boolean isCellEditable(int row, int column) { return column 5; // 仅“库存”列可编辑 } }; bookTable new JTable(model); // 关键监听单元格编辑结束事件 bookTable.getCellEditor().addCellEditorListener(new CellEditorListener() { Override public void editingStopped(ChangeEvent e) { int row bookTable.getSelectedRow(); int col 5; // 库存列索引 try { int newStock Integer.parseInt((String) bookTable.getValueAt(row, col)); if (newStock 0) throw new NumberFormatException(); // 获取图书ID第0列 int bookId (int) bookTable.getValueAt(row, 0); // 更新数据库见 3.2 节 boolean success updateBookStock(bookId, newStock); if (success) { // 刷新“已借出”和“可借”列假设已借出数不变 int borrowed (int) bookTable.getValueAt(row, 6); model.setValueAt(newStock, row, 5); model.setValueAt(Math.max(0, newStock - borrowed), row, 7); JOptionPane.showMessageDialog(null, 库存更新成功); } } catch (NumberFormatException ex) { JOptionPane.showMessageDialog(null, 库存必须为非负整数); model.setValueAt(model.getValueAt(row, col), row, col); // 恢复原值 } } Override public void editingCanceled(ChangeEvent e) {} }); }逻辑说明editingStopped是唯一可靠的编辑完成钩子。getValueAt(row, col)获取新值前需强转String因JTable默认存储为字符串再Integer.parseInt校验。失败时用setValueAt(...)恢复原值避免界面与数据库不一致。参数说明col 5对应“库存”列硬编码需与columns数组索引严格对齐Math.max(0, newStock - borrowed)防止“可借”出现负数。2.3 输入校验用 InputVerifier 实现 ISBN-13 格式实时校验非正则暴力匹配ISBN-13 必须满足13位数字、首位为9、校验码正确。用JTextField.setInputVerifier()在焦点离开时校验比DocumentListener更符合用户直觉用户输完再移开光标才提示。// BookManagementPanel.java - ISBN 校验器 private class ISBNVerifier extends InputVerifier { Override public boolean verify(JComponent input) { JTextField field (JTextField) input; String isbn field.getText().trim(); // 基础长度与数字检查 if (!isbn.matches(\\d{13})) { showErrorMessage(field, ISBN-13 必须为13位纯数字); return false; } // 首位必须为9中国标准ISBN前缀 if (isbn.charAt(0) ! 9) { showErrorMessage(field, ISBN-13 首位必须为9中国标准); return false; } // 计算校验码加权和 mod 10 int sum 0; for (int i 0; i 12; i) { int digit Character.getNumericValue(isbn.charAt(i)); sum (i % 2 0) ? digit : digit * 3; } int checkDigit (10 - (sum % 10)) % 10; if (checkDigit ! Character.getNumericValue(isbn.charAt(12))) { showErrorMessage(field, ISBN校验码错误请核对输入); return false; } return true; } private void showErrorMessage(JTextField field, String msg) { JOptionPane.showMessageDialog(null, msg, ISBN格式错误, JOptionPane.ERROR_MESSAGE); field.requestFocusInWindow(); } } // 使用方式 isbnField.setInputVerifier(new ISBNVerifier());参数说明isbn.matches(\\d{13})确保13位数字isbn.charAt(0) ! 9强制中国标准前缀校验码算法按 ISO 2108-2005奇数位1,3,5...权重1偶数位2,4,6...权重3总和模10后取补。此实现比Pattern.compile(9\\d{12})多一层数学校验杜绝“9999999999999”这类非法但长度正确的输入。3. MySQL 数据库设计拒绝“一张表全搞定”按业务域拆分并设外键约束3.1 四张核心表book / borrower / borrow_record / admin_log 的字段与约束设计很多人图省事把所有字段塞进book表如加borrower_name,borrow_date结果借阅历史无法追溯、同一本书多次借出数据冗余。我按最小业务实体拆分表名字段关键约束说明为什么这样设bookid(PK),isbn(UNIQUE),title,author,publisher,stock_total,stock_availableisbn唯一索引stock_available ≤ stock_total用触发器保证isbn是全球唯一标识必须 UNIQUEstock_available单独存避免每次查询都算total - SUM(borrowed)borrowerid(PK),student_id(UNIQUE),name,class_name,phonestudent_id唯一且非空class_name加索引高频筛选学生证号是校园内唯一 ID比姓名更可靠班级名用于批量导出borrow_recordid(PK),book_id(FK),borrower_id(FK),borrow_date,return_date,status(B/R)复合索引(book_id, status)statusENUM(B,R)book_idstatus索引加速“查某书未归还记录”ENUM 比 VARCHAR 节省空间且防脏数据admin_logid(PK),operator,action,target_id,timestampactionENUM(ADD_BOOK,BORROW,RETURN,UPDATE_STOCK)操作日志必须可审计target_id指向被操作记录 ID注意book.stock_available不是计算字段而是由borrow_record的插入/删除实时更新。这是性能与一致性平衡点——避免每次查库存都 JOIN 大表。3.2 关键触发器INSERT/DELETE borrow_record 时自动更新 book.stock_availablestock_available必须与借阅记录严格一致。手动在 Java 层更新极易出错如事务回滚时忘记还原。用 MySQL 触发器强制同步-- 插入借阅记录时扣减库存 DELIMITER $$ CREATE TRIGGER tr_borrow_insert AFTER INSERT ON borrow_record FOR EACH ROW BEGIN IF NEW.status B THEN UPDATE book SET stock_available stock_available - 1 WHERE id NEW.book_id AND stock_available 0; -- 若库存不足UPDATE 不生效后续 Java 层捕获影响行数为0 END IF; END$$ DELIMITER ; -- 删除归还记录时增加库存 DELIMITER $$ CREATE TRIGGER tr_return_insert AFTER INSERT ON borrow_record FOR EACH ROW BEGIN IF NEW.status R THEN UPDATE book SET stock_available stock_available 1 WHERE id NEW.book_id; END IF; END$$ DELIMITER ;逻辑说明AFTER INSERT确保borrow_record已写入stock_available 0条件防止超借UPDATE 影响行为0Java 层需检查executeUpdate()返回值若为0则提示“库存不足”。参数说明NEW.status B区分借出与归还动作DELIMITER $$是 MySQL 客户端必需的分隔符切换避免;被误解析。3.3 索引优化针对高频查询场景建立复合索引慢查询往往源于缺失索引。根据实际日志分析以下三类查询占比超 70%查某学生所有借阅记录SELECT * FROM borrow_record br JOIN borrower b ON br.borrower_idb.id WHERE b.student_id2023001→ 在borrower(student_id)上建索引已由 UNIQUE 约束隐式创建查某书未归还记录SELECT * FROM borrow_record WHERE book_id123 AND statusB→ 在borrow_record(book_id, status)上建复合索引顺序不可颠倒按班级导出未归还清单SELECT b.title, br.borrow_date FROM borrow_record br JOIN book b ON br.book_idb.id JOIN borrower bor ON br.borrower_idbor.id WHERE bor.class_name计算机2201 AND br.statusB→ 在borrower(class_name)上建索引并确保borrow_record的borrower_id有外键索引InnoDB 自动创建-- 手动添加复合索引若未自动创建 CREATE INDEX idx_book_status ON borrow_record(book_id, status); CREATE INDEX idx_borrower_class ON borrower(class_name);提示用EXPLAIN SELECT ...验证索引是否生效。重点关注type列应为ref或const非ALL和key列显示实际使用的索引名。4. Java 与 MySQL 交互不用 Hibernate手写 JDBC 封装 连接池 事务控制4.1 数据源配置HikariCP 连接池初始化非 DriverManager.getConnection 硬编码DriverManager.getConnection每次新建连接高并发下秒变瓶颈。必须用连接池。HikariCP 是当前性能最优选择比 Druid 轻量、比 C3P0 稳定。// DatabaseConnection.java - 单例连接池 public class DatabaseConnection { private static HikariDataSource dataSource; static { HikariConfig config new HikariConfig(); config.setJdbcUrl(jdbc:mysql://localhost:3306/library?useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue); config.setUsername(library_user); config.setPassword(SecurePass123!); config.setDriverClassName(com.mysql.cj.jdbc.Driver); // 关键参数连接池大小与超时 config.setMaximumPoolSize(10); // 并发用户 ≤ 50 时够用 config.setMinimumIdle(5); // 最小空闲连接防冷启动延迟 config.setConnectionTimeout(30000); // 获取连接超时30秒 config.setIdleTimeout(600000); // 空闲连接最大存活10分钟 config.setMaxLifetime(1800000); // 连接最大生命周期30分钟防MySQL wait_timeout dataSource new HikariDataSource(config); } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } }参数说明maximumPoolSize10是经验公式CPU核数 × 2 1此处按服务器 4 核设为 10maxLifetime1800000必须小于 MySQL 的wait_timeout默认 28800 秒否则连接被服务端主动关闭allowPublicKeyRetrievaltrue是 MySQL 8.0 必需参数否则报Public Key Retrieval is not allowed错误。4.2 DAO 层封装用 PreparedStatement 防 SQL 注入 批量操作提升导入效率所有数据库操作必须通过 DAOData Access Object层禁止在 GUI 事件中拼接 SQL。以图书批量导入为例// BookDAO.java - 批量插入图书 public int batchInsertBooks(ListBook books) throws SQLException { String sql INSERT INTO book (isbn, title, author, publisher, stock_total, stock_available) VALUES (?, ?, ?, ?, ?, ?); try (Connection conn DatabaseConnection.getConnection(); PreparedStatement pstmt conn.prepareStatement(sql)) { conn.setAutoCommit(false); // 关闭自动提交开启事务 for (Book book : books) { pstmt.setString(1, book.getIsbn()); pstmt.setString(2, book.getTitle()); pstmt.setString(3, book.getAuthor()); pstmt.setString(4, book.getPublisher()); pstmt.setInt(5, book.getStockTotal()); pstmt.setInt(6, book.getStockAvailable()); pstmt.addBatch(); // 加入批处理队列 } int[] results pstmt.executeBatch(); // 一次性执行 conn.commit(); // 提交事务 return Arrays.stream(results).sum(); // 返回总影响行数 } catch (SQLException e) { // 事务回滚 if (conn ! null) try { conn.rollback(); } catch (SQLException ignored) {} throw e; } }逻辑说明pstmt.addBatch()将多条 SQL 缓存在客户端executeBatch()一次发送到 MySQL比循环executeUpdate()快 5~10 倍conn.setAutoCommit(false)确保全部成功才提交任一失败则rollback()Arrays.stream(results).sum()统计实际插入行数results[i]为 -2 表示跳过-3 表示错误。4.3 事务边界控制借阅操作必须跨 book 和 borrow_record 两张表原子性更新借书动作包含两步1book.stock_available减 12borrow_record插入一条新记录。必须在一个事务中完成否则出现“库存扣了但记录没写入”的数据不一致。// BorrowService.java - 借阅服务 public boolean borrowBook(int bookId, int borrowerId) { String sqlUpdateStock UPDATE book SET stock_available stock_available - 1 WHERE id ? AND stock_available 0; String sqlInsertRecord INSERT INTO borrow_record (book_id, borrower_id, borrow_date, status) VALUES (?, ?, ?, B); try (Connection conn DatabaseConnection.getConnection()) { conn.setAutoCommit(false); // 步骤1扣减库存 try (PreparedStatement pstmt1 conn.prepareStatement(sqlUpdateStock)) { pstmt1.setInt(1, bookId); int affected pstmt1.executeUpdate(); if (affected 0) { conn.rollback(); return false; // 库存不足回滚并返回失败 } } // 步骤2插入借阅记录 try (PreparedStatement pstmt2 conn.prepareStatement(sqlInsertRecord, Statement.RETURN_GENERATED_KEYS)) { pstmt2.setInt(1, bookId); pstmt2.setInt(2, borrowerId); pstmt2.setDate(3, new Date(System.currentTimeMillis())); pstmt2.executeUpdate(); // 获取自增ID用于日志可选 try (ResultSet rs pstmt2.getGeneratedKeys()) { if (rs.next()) { long recordId rs.getLong(1); logAdminAction(BORROW, recordId, 借书成功); } } } conn.commit(); return true; } catch (SQLException e) { try { if (conn ! null) conn.rollback(); } catch (SQLException ignored) {} logAdminAction(BORROW_FAIL, 0, 借书失败 e.getMessage()); return false; } }参数说明conn.setAutoCommit(false)开启事务pstmt1.executeUpdate()返回影响行数0即库存不足Statement.RETURN_GENERATED_KEYS允许获取INSERT生成的idlogAdminAction是写入admin_log表的日志方法略。5. 避坑指南那些让系统上线后半夜被叫醒的 5 个血泪问题5.1 现象GUI 界面点击按钮无响应控制台无报错CPU 占用 100%原因Swing 事件处理线程EDT被耗时操作阻塞。典型错误是在ActionListener.actionPerformed()里直接调用borrowBook()含数据库 IO而 EDT 是单线程阻塞后整个界面冻结。解决所有耗时操作DB、网络、文件读写必须在SwingWorker中执行。例如借书按钮// BorrowPanel.java - 正确做法 borrowButton.addActionListener(e - { new SwingWorkerBoolean, Void() { Override protected Boolean doInBackground() throws Exception { return borrowService.borrowBook(selectedBookId, selectedBorrowerId); } Override protected void done() { try { if (get()) { JOptionPane.showMessageDialog(null, 借阅成功); refreshBookTable(); // 刷新表格 } else { JOptionPane.showMessageDialog(null, 借阅失败库存不足或网络异常); } } catch (Exception ex) { JOptionPane.showMessageDialog(null, 系统错误 ex.getMessage()); } } }.execute(); });5.2 现象MySQL 8.0 连接报错Could not create connection to database server. Attempted reconnect 3 times. Giving up.原因JDBC URL 缺少必要参数。MySQL 8.0 默认启用 SSL 且要求公钥检索而本地开发环境通常未配 SSL 证书。解决URL 中显式关闭 SSL 并允许公钥检索jdbc:mysql://localhost:3306/library?useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue注意useSSLfalse仅限内网环境生产环境必须配 SSL 证书并设useSSLtrue。5.3 现象批量导入 Excel 时中文书名显示为??但 MySQL 命令行查是正常的原因JDBC 连接未指定字符集或 Excel 解析库如 Apache POI未设 UTF-8。解决JDBC URL 加characterEncodingutf8POI 读取.xlsx时无需额外设置默认 UTF-8但读.xls旧版需Workbook workbook WorkbookFactory.create(new FileInputStream(file)); // 确保单元格内容以 UTF-8 解析POI 4.1 默认支持MySQL 服务端确认character_set_serverutf8mb4推荐或utf8。5.4 现象双击表格编辑库存后按回车数据库更新了但界面“可借”列没刷新原因DefaultTableModel.setValueAt()更新模型后未触发JTable重绘。常见错误是只调用model.setValueAt()忘了table.repaint()或更规范的fireTableCellUpdated()。解决在editingStopped中更新模型后显式通知表格model.setValueAt(newStock, row, 5); model.setValueAt(Math.max(0, newStock - borrowed), row, 7); // 关键触发表格刷新 bookTable.getModel().fireTableCellUpdated(row, 5); bookTable.getModel().fireTableCellUpdated(row, 7);5.5 现象程序运行几天后抛HikariPool-1 - Connection is not available, request timed out after 30000ms.原因连接泄漏。典型场景是Connection、PreparedStatement、ResultSet未在finally块中关闭或try-with-resources未覆盖所有分支。解决严格使用try-with-resourcesJava 7检查所有 DAO 方法确保Connection在try块内获取并在try块内关闭try-with-resources自动关闭启用 HikariCP 连接泄漏检测config.setLeakDetectionThreshold(60000); // 60秒未关闭即告警日志中出现Connection leak detection triggered即定位到泄漏代码行。6. 进阶技巧用 JasperReports 实现「一键导出带条形码的借阅报表」并嵌入 GUI6.1 报表设计用 iReport DesignerJasperSoft Studio 免费版拖拽生成 .jrxml需求导出 PDF 报表每行显示书名、ISBN、借阅人、借阅日期并在右侧生成 ISBN-13 条形码。步骤新建空白报表添加字段book_title,isbn,borrower_name,borrow_date从组件面板拖入Barcode组件类型选Code128绑定表达式$F{isbn}设置字体为SimSun支持中文条形码高度 50px导出为borrow_report.jrxml。提示Code128支持数字字母ISBN-13全数字完美匹配避免用EAN13需额外计算校验位。6.2 Java 调用 JasperReports填充数据并导出 PDF不弹窗、不依赖浏览器// ReportPanel.java - 导出按钮逻辑 exportButton.addActionListener(e - { try { // 1. 查询数据使用 Map 封装参数如日期范围 MapString, Object params new HashMap(); params.put(start_date, startDateChooser.getDate()); params.put(end_date, endDateChooser.getDate()); // 2. 从 classpath 加载 .jrxml编译为 .jasper 更快此处简化 InputStream reportStream getClass().getResourceAsStream(/reports/borrow_report.jrxml); JasperReport jasperReport JasperCompileManager.compileReport(reportStream); // 3. 执行查询并填充报表JasperReports 自动执行 SQL Connection conn DatabaseConnection.getConnection(); JasperPrint jasperPrint JasperFillManager.fillReport( jasperReport, params, conn); // 4. 导出为 PDF 到指定路径 String outputPath borrow_report_ System.currentTimeMillis() .pdf; JasperExportManager.exportReportToPdfFile(jasperPrint, outputPath); JOptionPane.showMessageDialog(null, 报表已导出至 outputPath); } catch (Exception ex) { JOptionPane.showMessageDialog(null, 导出失败 ex.getMessage()); ex.printStackTrace(); } });逻辑说明JasperFillManager.fillReport()自动执行.jrxml中定义的 SQL如queryString![CDATA[SELECT ...]]/queryString并将params传入WHERE条件exportReportToPdfFile()直接写文件不依赖外部 PDF 阅读器。6.3 条形码字体与编码确保生成的条形码能被扫描枪识别关键陷阱JasperReports 默认条形码字体可能不兼容通用扫描枪。必须指定Code128字体并验证设置项推荐值为什么Barcode TypeCode128ISBN-13 是 13 位数字Code128 编码效率高、扫描枪兼容性最好Font NameFree 3 of 9 Extended需下载 TTF免费开源字体支持 Code128扫描枪识别率 99.9%Quiet Zone10左右空白像素防止边缘裁剪导致扫描失败实操下载Free3of9Extended.ttf放入项目fonts/目录在.jrxml中引用font fontNameFree 3 of 9 Extended size12/然后用手机扫码 App如「二维码扫描王」实测生成的 PDF能 1 秒识别即达标。最后说个我踩过的坑别在报表里用System.getProperty(user.home)拼路径不同用户权限下可能写入失败。导出路径用System.getProperty(user.dir) /exports/并提前mkdir。这套方案跑在 200 人规模的学院里三年没出过数据事故核心就三点GUI 层用 CardLayout 隔离、DB 层用触发器保一致性、Java 层用连接池事务SwingWorker 控制资源。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询