Java+SQL Server 2000超市管理系统:JDBC连接、事务处理与数据库迁移实战
简介面向Java初学者与课程设计人群的超市管理系统完整源码包基于Java和SQL Server 2000数据库实现适合作为课程设计、毕业设计或Java桌面应用开发的入门参照。压缩包共218个文件压缩后仅2.76MB内含编译后的class文件、Java源文件、多份历史版本备份、SQL脚本、界面GIF预览图和说明文本可通过文件类型快速区分运行程序、源码与文档。系统覆盖超市日常运营中的常见模块包括用户登录、员工管理、库存与订单处理、管理员窗口等并涉及Swing界面设计、JDBC数据库连接、SQL增删改查、异常处理等关键知识点能够帮助读者理解从界面操作到数据库持久化存储的完整交互链路并适合在已有代码基础上进行模块扩展或界面调整。目前已有551人学习下载适合快速搭建超市管理演示原型可作为二次开发的起点。1. 为什么还有人找JavaSQLserver2000超市管理系统这套老代码的价值与坑看到“JavaSQLserver2000超市管理系统源代码.zip”这个标题的多半不是闲着翻旧货而是手里正攥着一个毕业设计、课程设计或者公司里还在跑的老收银系统。SQL Server 2000是微软2000年前后的产品Java也还是J2SE 1.4那个年代的东西但超市管理系统的基本盘——商品、库存、销售、会员——二十年来没变过。这套代码的价值在于它能完整展示一个桌面版进销存系统怎么从登录窗口到报表统计串起来而且数据量小逻辑直白比那些堆了十几个微服务的“单体怪”好啃得多。适合谁呢正在学JDBC和Swing的Java初学者或者需要快速复制一套后端逻辑来改的老工程师。但打开它之前你得先知道两件事一是它的连接配置和现在的SQL Server写法不一样二是它几乎没有安全设计直接跑起来就是给数据库裸奔。2. 系统拆解超市管理系统的模块划分与SQLserver2000数据库设计2.1 从登录到盘点六个核心模块的职责边界超市管理系统听着大拆开就是一组围绕“进销存”的CRUD外加一个收银事务。我过手的这类项目模块边界基本是这样登录与用户管理操作员登录、权限区分店长、收银员、仓库管理员。商品管理商品信息增删改查包括条码、品名、进价、售价、分类。库存管理入库、出库、库存调整、预警。销售收银购物车、结算、小票打印。会员管理办卡、积分累计、等级折扣。报表统计日销售额、毛利、商品销售排行。理模块边界时最容易翻的车是“把一个操作塞进两个模块”。比如把入库当成商品管理里的“编辑”来做最后库存流水对不上。经验是商品表只管“现在是什么”库存表管“还有多少”库存流水表管“每次加减的凭证”。这个三层分开后面写事务和排查差账会省很多事。这类项目的难点不在窗口布局而在“扣库存和加销售记录必须同时成功”。很多学生代码只写了先查库存再减库存却在网络断线时出现卖出去一件、库存没减的账实不符。所以模块拆分从一开始就要把“销售”做成一个独立事务边界。2.2 建表脚本用户表、商品表、销售表、库存表的设计与主外键SQL Server 2000没有现在那些花哨功能但基础的表和约束毫不含糊。下面是这套系统最核心的四张表我按常规做法整理成了可直接执行的建表脚本CREATE DATABASE supermarket GO USE supermarket GO CREATE TABLE t_user ( user_id INT IDENTITY(1,1) PRIMARY KEY, user_name VARCHAR(20) NOT NULL UNIQUE, user_password VARCHAR(32) NOT NULL, user_role CHAR(1) NOT NULL DEFAULT 1, -- 1收银员 2店长 reg_time DATETIME NOT NULL DEFAULT GETDATE() ) GO CREATE TABLE t_category ( cate_id INT IDENTITY(1,1) PRIMARY KEY, cate_code VARCHAR(10) NOT NULL UNIQUE, -- 分类编码比如 0101 cate_name VARCHAR(30) NOT NULL ) GO CREATE TABLE t_product ( prod_id INT IDENTITY(1,1) PRIMARY KEY, bar_code VARCHAR(20) NOT NULL UNIQUE, -- 条码 prod_name VARCHAR(50) NOT NULL, cate_code VARCHAR(10) NOT NULL, sale_price DECIMAL(10,2) NOT NULL, cost_price DECIMAL(10,2) NOT NULL, low_stock INT NOT NULL DEFAULT 10, -- 预警下限 create_time DATETIME NOT NULL DEFAULT GETDATE(), CONSTRAINT fk_product_cate FOREIGN KEY (cate_code) REFERENCES t_category(cate_code) ) GO CREATE TABLE t_stock ( prod_id INT PRIMARY KEY, quantity INT NOT NULL DEFAULT 0, last_in DATETIME, CONSTRAINT fk_stock_product FOREIGN KEY (prod_id) REFERENCES t_product(prod_id) ) GO CREATE TABLE t_sale ( sale_id INT IDENTITY(1,1) PRIMARY KEY, user_id INT NOT NULL, sale_time DATETIME NOT NULL DEFAULT GETDATE(), total_amt DECIMAL(10,2) NOT NULL, pay_type VARCHAR(10) NOT NULL DEFAULT 现金, CONSTRAINT fk_sale_user FOREIGN KEY (user_id) REFERENCES t_user(user_id) ) GO CREATE TABLE t_sale_detail ( detail_id INT IDENTITY(1,1) PRIMARY KEY, sale_id INT NOT NULL, prod_id INT NOT NULL, quantity INT NOT NULL, price DECIMAL(10,2) NOT NULL, CONSTRAINT fk_detail_sale FOREIGN KEY (sale_id) REFERENCES t_sale(sale_id), CONSTRAINT fk_detail_prod FOREIGN KEY (prod_id) REFERENCES t_product(prod_id) ) GO这段脚本要拆开讲。t_user表我用VARCHAR(20)做账号密码字段给了32位不是为了存好听的是为了给后面改MD5留位置。t_category表用cate_code作为商品表的外键关联而不是直接用cate_id。原因是商品录入时操作员扫的是编码不是数字主键用cate_code做关联能减少一次子查询。代价是分类编码万一改了就麻烦所以UNIQUE约束必须加。t_stock表是典型的“一商品一条库存记录”设计。quantity只在事务里被更新不能随便直接改。last_in字段用来记录最后一次入库时间方便查滞留商品。DECIMAL(10,2)是钱的标准写法千万别用FLOAT浮点数存钱会出现0.10.2不等于0.3的经典问题超市毛利算到分时能气死对账的人。2.3 商品分类与编码规则为什么建议用编码而不是名称做关联键这里有个从实际项目里带出来的血泪经验分类和商品的关联字段能选编码就不选中文名能选短编码就不选自增ID。中文名做关联看着直观但「饮料」和「饮品」在Excel里是两个值录入员一偷懒ERP里就会出现几十个“近义词分类”最后商品列表下拉框翻三页。自增ID虽然稳定但分类搬到另一个系统时ID会冲突编码只要规则清晰就能带过去。我一般建议分类编码用两层四位数比如01代表食品0101代表膨化食品0102代表饼干。这样在t_product表里cate_code既能直接关联又能在报表里通过前两位做聚合LEFT(cate_code, 2)就能统计食品大类销售。对SQL Server 2000来说这种字符串查询走索引也没压力毕竟数据量不至于上千万。-- 按分类编码前两位统计销售额 SELECT LEFT(c.cate_code, 2) AS cate_group, SUM(d.quantity * d.price) AS total_sales FROM t_product p JOIN t_sale_detail d ON p.prod_id d.prod_id JOIN t_category c ON p.cate_code c.cate_code GROUP BY LEFT(c.cate_code, 2) ORDER BY total_sales DESC这条SQL用到了三表关联和字符串函数在SQL Server 2000里跑得很顺。注意GROUP BY里必须写LEFT(c.cate_code, 2)的表达式本身不能写别名这是老版本的一个限制新版也不能保证所有场景都允许别名。很多新手在这里报错就是因为SQL标准里GROUP BY的执行顺序先于SELECT别名解析。3. 用JDBC把Java和SQLserver2000连起来驱动配置、连接参数与最小Demo3.1 驱动加载与URL写法为什么SQLserver2000和2008的URL不一样在写代码之前先把“Java 8 SQL Server 2000”这个组合的驱动问题说透。SQL Server 2000的年代微软提供的JDBC驱动是com.microsoft.jdbc.sqlserver.SQLServerDriver对应URL是jdbc:microsoft:sqlserver://127.0.0.1:1433;DatabaseNamesupermarket。这个驱动在Java 6之后就基本不维护了在Java 8下能跑但偶尔会报奇怪的ClassNotFoundException原因多半是编译时用了新驱动运行时却引了老驱动。后来微软出了com.microsoft.sqlserver.jdbc.SQLServerDriverURL变成jdbc:sqlserver://127.0.0.1:1433;DatabaseNamesupermarket。新驱动兼容SQL Server 2000到2019的大部分版本但SQL Server 2000有些老协议得在连接串里加encryptfalse否则手机会端到端加密搞出握手失败。所以最小可用的JDBC连接代码如下import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DbUtil { private static final String DRIVER com.microsoft.sqlserver.jdbc.SQLServerDriver; // 如果跑老驱动改成 com.microsoft.jdbc.sqlserver.SQLServerDriver private static final String URL jdbc:sqlserver://127.0.0.1:1433;DatabaseNamesupermarket;encryptfalse; private static final String USER sa; private static final String PASSWORD 你的密码; public static Connection getConnection() { Connection conn null; try { Class.forName(DRIVER); conn DriverManager.getConnection(URL, USER, PASSWORD); } catch (ClassNotFoundException e) { System.err.println(找不到驱动类检查jar包是否放在lib里); } catch (SQLException e) { System.err.println(连接失败错误码 e.getErrorCode()); System.err.println(错误信息 e.getMessage()); } return conn; } public static void close(Connection conn) { if (conn ! null) { try { conn.close(); } catch (SQLException ignored) { } } } }注意代码里的Class.forName不是必须的JDBC 4.0以后驱动会自动注册但保留它对老项目是种“保险”。另外getConnection返回Connection调用方用完必须关闭。这套工具类我做了简化实际项目里还会加close(ResultSet, Statement)的重载不然一个窗口开下来连接池满了就翻车。参数说明1433是SQL Server默认端口装了命名实例就得改成jdbc:sqlserver://主机名\\实例名的格式端口那块经常被忽略。3.2 用DbUtil封装连接一份可以直接抄的JDBC工具类上面的DbUtil是连接管理的最小版本但在这个超市系统里每个业务类都调getConnection()然后自己finally关闭代码会极其臃肿。我习惯把“取连接”和“释放资源”再包一层让业务代码只关注SQL本身import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; public final class DbTemplate { public interface QueryCallbackT { T doInQuery(PreparedStatement ps, ResultSet rs) throws SQLException; } public static T T query(String sql, QueryCallbackT callback, Object... params) { Connection conn DbUtil.getConnection(); PreparedStatement ps null; ResultSet rs null; try { ps conn.prepareStatement(sql); for (int i 0; i params.length; i) { ps.setObject(i 1, params[i]); } rs ps.executeQuery(); return callback.doInQuery(ps, rs); } catch (SQLException e) { throw new RuntimeException(查询失败 sql, e); } finally { closeQuietly(rs); closeQuietly(ps); DbUtil.close(conn); } } private static void closeQuietly(AutoCloseable ac) { if (ac ! null) { try { ac.close(); } catch (Exception e) { // 关闭资源失败不影响业务但可以打印日志 e.printStackTrace(); } } } }这个模板的核心价值有两点第一参数用Object...可变参数统一传入内部用ps.setObject设置避免每个DAO里重写一遍try-with-resources第二回调接口把ResultSet交给业务代码自己解析避免搞出一大堆DTO。调用方式是在业务类里这样用ListInteger productIds DbTemplate.query( SELECT prod_id FROM t_product WHERE sale_price ?, (ps, rs) - { ListInteger ids new ArrayList(); while (rs.next()) { ids.add(rs.getInt(1)); } return ids; }, 50.00 );这里有个关键点回调的泛型是T意味着任何类型都能直接返回不用为了拿一个整数列表去写一个单独的DAO方法。但注意别在回调里关闭连接否则rs会失效。模板的finally里已经关了业务代码绝不能再手动关。还有一点ps.setObject对DECIMAL类型传Double会自动转但传Float可能精度丢失建议参数统一用BigDecimal封装金额。3.3 参数说明端口、实例名、登录方式、字符集这块是排错的重灾区。SQL Server 2000默认实例用1433端口如果跑的是命名实例比如PC-001\SQLEXPRESS则URL写成private static final String URL jdbc:sqlserver://PC-001\\SQLEXPRESS;DatabaseNamesupermarket;encryptfalse;注意\\是Java字符串里的一个反斜杠。登录方式有两种sa账号密码或Windows集成认证。SQL Server 2000默认装了Windows混合认证所以sa密码可能是空的直接连就进去了但这在生产环境是巨大的安全洞。连接串里的DatabaseName参数指定默认库如果不写连接会落到用户默认数据库SQL语句里的表就变成数据库.dbo.t_product这种写法。还有字符集encryptfalse和charsetUTF-8是最常加的。SQL Server 2000默认字符集如果是简体中文那VARCHAR列存中文没问题但Java端读取时如果不指定characterEncodingUTF-8getString返回的可能是乱码。在JDBC URL里加characterEncodingUTF-8不是标准参数老驱动会忽略新驱动走sendStringParametersAsUnicode所以中文乱码的根源通常不在URL而在建表时的COLLATE设定。同行们最常用的做法是建表时指定Chinese_PRC_CI_AS排序规则Java端全部用String类型接收不要手工转码。4. 复现核心业务登录校验、商品录入、收银台与库存预警的代码实现4.1 登录模块明文校验的坑与MD5改造这套系统源码里的登录校验八成是拿user_password直接和输入框里的值比对这在毕业设计里能交差但放到真实超市就是个笑话——任何人打开数据库就能看到所有密码明文。我改造时遵循的规则是“数据库里不存明文”。public class UserDao { public static final String MD5_KEY supermarket2024; public static boolean login(String userName, String rawPassword) { String md5Hash md5(MD5_KEY rawPassword); // 加盐防止彩虹表 String sql SELECT COUNT(*) FROM t_user WHERE user_name ? AND user_password ?; return DbTemplate.query(sql, (ps, rs) - { rs.next(); return rs.getInt(1) 0; }, userName, md5Hash) ! null loginResult(userName, md5Hash); } private static String md5(String input) { try { MessageDigest md MessageDigest.getInstance(MD5); byte[] bytes md.digest(input.getBytes(UTF-8)); StringBuilder sb new StringBuilder(); for (byte b : bytes) { sb.append(String.format(%02x, b)); } return sb.toString(); } catch (Exception e) { throw new RuntimeException(e); } } private static boolean loginResult(String username, String md5Hash) { // 这里实际接收模板返回值避免上面写得太绕 String sql SELECT COUNT(*) FROM t_user WHERE user_name ? AND user_password ?; return DbTemplate.query(sql, (ps, rs) - { rs.next(); return rs.getInt(1) 0; }, username, md5Hash); } }这段逻辑里有个小坑我写了两个函数是因为第一个简化版本里DbTemplate.query返回值会被拆成两次调用真实项目里应该直接把查询结果返回。但这个示例的重点是告诉大家密码比对要在数据库里完成不要先查出用户再在Java里比对否则“数据库连接泄露”和“SQL注入”同时找上门。用COUNT(*)作为结果只返回一个整数也方便之后改成“超过三次锁定”的逻辑。MD5本身已经不推荐用于高强度安全场景但在老系统里改造成本最低。你要做的是把前后摘要对比改成MessageDigest.isEqual()防止时序攻击。注意代码里的加盐串MD5_KEY不能写死在代码里至少放到config.properties里读取。4.2 商品录入与批量导入PreparedStatement和Statement的差距商品录入的表单很简单条码、品名、进价、售价、分类编码。但真正暴露问题的是批量导入——从Excel读取几百条商品记录时新手最容易用Statement拼SQL字符串。拼出来的语句不仅难看还有SQL注入危险更致命的是每次执行都要让SQL Server编译一次几百条数据还好上万条就会卡死。这里直接把正确的批量插入代码写出来public void batchInsert(ListProduct products) throws SQLException { String sql INSERT INTO t_product(bar_code, prod_name, cate_code, sale_price, cost_price, low_stock) VALUES(?, ?, ?, ?, ?, ?); Connection conn DbUtil.getConnection(); PreparedStatement ps null; try { ps conn.prepareStatement(sql); for (Product p : products) { ps.setString(1, p.getBarCode()); ps.setString(2, p.getProdName()); ps.setString(3, p.getCateCode()); ps.setBigDecimal(4, p.getSalePrice()); ps.setBigDecimal(5, p.getCostPrice()); ps.setInt(6, p.getLowStock()); ps.addBatch(); // 每200条刷一次防止事务日志膨胀 if (products.size() % 200 0) { ps.executeBatch(); } } ps.executeBatch(); } finally { if (ps ! null) ps.close(); DbUtil.close(conn); } }PreparedStatement对SQL Server 2000的最大好处是参数化的SQL在第一次执行后会被缓存的执行计划复用性能直接翻倍。addBatch和executeBatch的作用是把多条插入攒成一个网络往返大幅减少总耗时。注意代码里那个if (products.size() % 200 0)判断有个边界问题如果总条数刚好是200的倍数会在循环结束前刷一次循环后再刷一次导致最后一批多执行。更稳的写法是维护一个计数器每累计200条就刷最后再判断ps.executeBatch()是否已被空批次触发。SQL Server 2000的批处理能力不像新版支持表值参数所以这个做法是稳妥的。另外商品表bar_code有UNIQUE约束批量导入时如果Excel里有重复条码会直接整体回滚。要拿到不全成功的体验就得在插入前先查询一次把已有条码过滤掉。这里要注意并发两个客户端同时导入相同条码查询后再插仍可能冲突最简单的办法是给t_product加一个session_temp标记但这会过度设计。入门阶段记住“先查再插捕获重复键异常即可”。4.3 收银台事务一次扣库存和加销售记录为什么必须用事务收银台是整个系统里唯一一个必须用数据库事务的地方因为“生成销售主表 生成销售明细 扣减库存”三件事要么全部成功要么全部失败。想象一下收银员扫码商品加入购物车点结算程序先插入销售单再更新库存结果第二步执行完程序在第三步更新库存时断电了——数据库里就有了销售记录但库存没扣期末盘点怎么都对不上。标准事务代码是public void checkout(ListCartItem items, String userId) throws SQLException { String insertSaleSql INSERT INTO t_sale(user_id, total_amt) VALUES(?, ?); String insertDetailSql INSERT INTO t_sale_detail(sale_id, prod_id, quantity, price) VALUES(?, ?, ?, ?); String updateStockSql UPDATE t_stock SET quantity quantity - ? WHERE prod_id ? AND quantity ?; Connection conn DbUtil.getConnection(); conn.setAutoCommit(false); // 开启手动事务 try { BigDecimal total BigDecimal.ZERO; for (CartItem item : items) { total total.add(item.getPrice().multiply(BigDecimal.valueOf(item.getQty()))); } PreparedStatement ps1 conn.prepareStatement(insertSaleSql, Statement.RETURN_GENERATED_KEYS); ps1.setString(1, userId); ps1.setBigDecimal(2, total); ps1.executeUpdate(); ResultSet rs ps1.getGeneratedKeys(); int saleId 0; if (rs.next()) { saleId rs.getInt(1); } rs.close(); ps1.close(); PreparedStatement ps2 conn.prepareStatement(insertDetailSql); PreparedStatement ps3 conn.prepareStatement(updateStockSql); for (CartItem item : items) { ps2.setInt(1, saleId); ps2.setInt(2, item.getProdId()); ps2.setInt(3, item.getQty()); ps2.setBigDecimal(4, item.getPrice()); ps2.addBatch(); // 带条件更新库存不足时影响行数为0 ps3.setInt(1, item.getQty()); ps3.setInt(2, item.getProdId()); ps3.setInt(3, item.getQty()); ps3.addBatch(); } ps2.executeBatch(); ps3.executeBatch(); conn.commit(); } catch (SQLException e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(true); DbUtil.close(conn); } }这段代码有几个关键决策用getGeneratedKeys()拿自增主键SQL Server 2000支持IDENTITY的当前值获取但使用RETURN_GENERATED_KEYS是JDBC 3.0标准老驱动不一定支持。如果报了SQLException: This driver is not configured for integrated authentication那就是驱动兼容性问题替代方案是在插入前先查SELECT IDENT_CURRENT(t_sale)但这有并发风险。稳妥做法是用一条INSERT语句加OUTPUT inserted.sale_id可SQL Server 2000不支持OUTPUT只能退化成在一个事务里先SELECT MAX(sale_id)1再插入虽然不完美但在小系统里可用。扣库存用UPDATE t_stock SET quantity quantity - ? WHERE prod_id ? AND quantity ?这个条件quantity ?能防止库存变负数。如果实际影响行数为0就抛业务异常回滚整个销售。这样杜绝了“卖完还在卖”的情况。setAutoCommit(false)之后凡是这个连接上的操作都在一个事务里包括之前用DbTemplate创建的连接不能用同一个Connection对象混着来。这里我直接用DbUtil.getConnection()拿新连接然后在finally里恢复autoCommit为true否则连接回到池里时还是手动提交状态下一个用户调用conn.commit()会把别人的事务也提交了。4.4 库存预警用一条SQL找出需要补货的商品超市系统的库存预警逻辑很简单当前库存低于某个阈值就提示补货。阈值存在t_product.low_stock字段。但要注意有些老系统里阈值写在代码里比如if (stock 10)这种硬编码换商品就翻车。阈值要么每个商品自己配要么按分类给默认值然后在商品管理界面允许修改。SELECT p.bar_code, p.prod_name, s.quantity AS current_qty, p.low_stock, (p.low_stock - s.quantity) AS missing_qty FROM t_product p JOIN t_stock s ON p.prod_id s.prod_id WHERE s.quantity p.low_stock ORDER BY missing_qty DESC这条SQL会返回所有低于预警线的商品missing_qty是还差多少补到阈值。如果要按严重程度排序ORDER BY missing_qty DESC把最缺的放最前面。这里有个性能细节t_product和t_stock都按prod_id关联如果数据量到了几十万要给t_stock.quantity和t_product.low_stock建复合索引。但SQL Server 2000的查询优化器对和表达式p.low_stock - s.quantity不一定能正确用索引所以实际调优时可以在Java层先查出所有库存和阈值再内存排序。数据量不大时先别做无根据的优化。还有一个排查差账的黄金句式比较“库存变动流水表”与“销售明细表”。很多超市系统没有独立的库存变动流水表那在盘点时就会发现库存量和“期初 入库 - 销售”对不上。如果这套源码里没有流水表建议后续补上否则库存预警再准也只是看着数字没法审计。5. 避坑指南SQLserver2000连接和运行时的5个高频问题5.1 现象连接超时报错“Connection refused”这个问题在本地开发机上的出现频率最高尤其是下载了绿色版SQL Server 2000或者用虚拟机跑的。原因通常是SQL Server服务没启动或者TCP/IP协议被禁用。打开“SQL Server 2000服务器网络实用工具”检查是否启用了TCP/IP默认端口是否为1433。另外Linux上用Docker跑SQL Server 2000模拟环境时网络模式如果不是host从Java容器里连不上也很正常。解决步骤我按顺序排先netstat -an | findstr 1433看端口监听再检查SQL Server服务状态最后用telnet 127.0.0.1 1433测通不通。连不通时就别怀疑Java代码先解决网络层。5.2 现象ClassNotFoundException: com.microsoft.sqlserver.jdbc.SQLServerDriver驱动类找不到核心原因是sqljdbc的jar包没放进运行环境。但这个报错更常见的场景是编译时引用了mssql-jdbc-8.2.2.jre8.jar运行时却把sqljdbc4.jar放在lib里导致类加载器找不到新驱动名。解决方法是确认全项目只有一个版本的SQL Server驱动建议用Maven统一管理把驱动依赖写入pom.xml。如果系统是老的web项目检查WEB-INF/lib下是否同时存在多个sqljdbc*.jar把它们清到只剩一个。还有一个很容易被忽略的JDK 9以后模块系统可能导致驱动自动注册失败但Class.forName显式加载不受影响所以代码里别删掉那一行。5.3 现象中文乱码数据库中显示正常但Java读出来是“???”乱码的坑我踩过多次最典型的场景是SQL Server 2000数据库排序规则是Chinese_PRC_CI_AS但Java连接串没设置characterEncodingUTF-8导致getString按系统默认编码解析。解决方法是双管齐下第一建表时明确指定排序规则比如CREATE TABLE t_product (prod_name NVARCHAR(50) COLLATE Chinese_PRC_CI_AS ...)第二JDBC连接串加sendStringParametersAsUnicodetrue这是新驱动的默认值但老驱动不走这个参数只能靠数据库端排序规则兜底。如果乱码只在控制台输出时出现那是Windows控制台代码页问题不是数据错误别去折腾数据库。5.4 现象java.sql.SQLException: [Microsoft][SQLServer 2000 Driver for JDBC]Error establishing socket这个错误看起来像连接拒绝其实是驱动和数据库版本之间的加密协议不匹配。SQL Server 2000默认不走强加密但较新的JDBC驱动客户端比如9.x版本默认请求加密连接服务器那边不支持就报错。解决是在URL里加encryptfalse;trustServerCertificatefalse新版驱动还要加sslProtocolTLS。如果加了encryptfalse还报错试试用sqljdbc4.jar的旧版4.2版本兼容性最好。我一般会在这类老系统里同时保留两个驱动jar包但运行时只选择一个避免类冲突。5.5 现象查询很慢一条SQL要几秒SQL Server 2000的查询慢不一定是索引问题很多人忽略的是结果集游标类型。JDBC里createStatement()默认生成TYPE_FORWARD_ONLY和CONCUR_READ_ONLY游标但有些老代码为了能翻页手动指定了TYPE_SCROLL_INSENSITIVE这会变成在服务器内存里生成整个结果集再传给客户端数据量大时慢得离谱。解决方法是把Statement创建改成默认的Statement stmt conn.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);此外SQL Server 2000的查询优化器在选择索引时对LIKE %关键字%这种方式不会走索引全表扫描难免。如果商品表要支持模糊搜索建议用LEFT(cate_code,2) %这种前缀匹配方式或者直接在应用层用startsWith过滤。6. 把老系统变现代从SQLserver2000迁到SQL Server 2019或MySQL的实战路径最后一个话题是这套源码最常见的归宿从SQL Server 2000迁到现代数据库。很多学校机房的SQL Server 2000跑不起来但项目又必须演示那就把数据迁到SQL Server 2019。迁移路径有三步。第一步是导数据。SQL Server 2000支持直接备份成.bak文件但新版本还原时经常报版本不兼容。于是常见做法是先在本机装一个SQL Server 2000实例把.bak还原再用“SQL Server导入和导出向导”把表结构和数据转到2019。这里要手工确认主外键和自增列向导有时会丢约束。更简单的是用bcp把每张表导出为CSV再导新库但中间类型映射容易出错比如datetime变varchar。第二步是改连接参数。SQL Server 2019和2000的JDBC驱动兼容性更好新的mssql-jdbc包直接支持但encryptfalse还是要保留否则默认加密会报登录失败。同时端口不变还是1433数据库名不变所以业务代码里几乎只需要改一个DbUtil常量。这个设计让我尝到甜头——当初把连接封装在一个类里现在就改一行不用全局搜DriverManager。第三步是验证。我用一条对比SQL来核对迁移前后的数据-- 旧库执行 SELECT COUNT(*) FROM t_sale -- 新库执行 SELECT COUNT(*) FROM t_sale -- 对每个表都跑一遍然后核对各自的最大自增值 SELECT MAX(sale_id), MAX(prod_id) FROM t_sale, t_product不要只比行数还要比SUM(total_amt)、SUM(quantity)这些业务度量。老库和新库的tempdb排序规则如果不同字符串比较的结果会不一样这也是一个隐藏雷。我会先跑三条基线SQL总销售金额、总库存量、总会员数这三条能对上基本业务就没大问题。最后分享一个我的习惯拿到这种“毕业设计级别”的老系统第一件事不是读代码而是用数据库系统的“文档生成器”把表结构导成Word或Excel然后对照着画一张模块与表的映射图。这张图能帮你少走一半弯路。等到真要改代码时优先改DbUtil而不是改业务逻辑因为老系统最大的问题永远是连接不可控。改完连接层和事务层整个系统就已经脱离了“只能交作业”的范畴能承担小超市的真实收银任务了。希望这些踩坑记录能帮你在跑这套老系统时少一点“玄学”多一点能追溯的问题定位手段。本文还有配套的精品资源点击获取

相关新闻

自托管 HTTP 隧道实战:用 Go 实现内网穿透与远程访问

自托管 HTTP 隧道实战:用 Go 实现内网穿透与远程访问

这次的实操目标很明确:一个用 Go 写的 self-hosted HTTP 隧道项目,项目名Smuf。说直白一点,它就是解决"本地服务怎么安全暴露到公网"这个问题的工具,而且强调数据走自己的服务器,不依赖第三方中转平台。如果…

2026/10/9 3:03:53 阅读更多 →
Spring Boot汽车维修管理系统毕设:数据库设计、状态流转与部署避坑指南

Spring Boot汽车维修管理系统毕设:数据库设计、状态流转与部署避坑指南

做毕设选了Springboot汽车维修服务管理系统这类题目的同学,大概率心里都有点打鼓:又是一个“程序源码数据库调试部署”的一站式项目,网上铺天盖地,但到底能不能跑起来、答辩时能不能讲清楚、论文稳不稳,全是未知数。我…

2026/10/9 3:03:53 阅读更多 →
大模型困于局部最优,持续学习如何破解灾难性遗忘?

大模型困于局部最优,持续学习如何破解灾难性遗忘?

最近,强化学习领域的奠基人之一 Richard Sutton 的一段观点又引发了圈内热议:大模型目前更像是被困在“局部最优”里,AI 真正要往前走,必须拥有持续学习的能力。这个判断击中了当前大模型发展的一个核心痛点——我们从海量静态数据…

2026/10/9 3:03:53 阅读更多 →

最新新闻

从元器件到LM317可调电源:零基础硬件电路设计实战指南

从元器件到LM317可调电源:零基础硬件电路设计实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/9 3:33:14 阅读更多 →
三秒生成Alpha通道:贴花素材自动抠图并直连Substance Painter

三秒生成Alpha通道:贴花素材自动抠图并直连Substance Painter

做3D材质的兄弟应该都有过这种经历:在素材网站上看到一张完美的磨损贴花,下载下来,丢进Substance Painter一拖——结果背景一片灰,边缘全是杂边,Alpha通道干干净净地写着“无”。于是你打开Photoshop,魔棒、…

2026/10/9 3:33:14 阅读更多 →
Uber集成缓存:把缓存嵌入MySQL存储引擎的高并发读架构

Uber集成缓存:把缓存嵌入MySQL存储引擎的高并发读架构

买卖双方都不傻,亿级流量面前,缓存方案一定不是“用 Redis 顶在前面”这么简单。Uber 这篇集成缓存(Integrated Cache)的分享,是系统设计圈里少有的、把缓存直接塞进数据库存储引擎里的实战派解法。我读完第一反应是&a…

2026/10/9 3:33:14 阅读更多 →
AMD Ross FPGA Agent 深度解析:从 Vivado 工程自动化到智能调试

AMD Ross FPGA Agent 深度解析:从 Vivado 工程自动化到智能调试

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/9 3:33:14 阅读更多 →
EtherNet/IP工业以太网协议实战:从CIP架构到PLC与SCADA联调配置

EtherNet/IP工业以太网协议实战:从CIP架构到PLC与SCADA联调配置

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/9 3:33:14 阅读更多 →
双指针算法全攻略:对撞、快慢、滑动窗口三大模板与实战总结

双指针算法全攻略:对撞、快慢、滑动窗口三大模板与实战总结

刷题刷到一定量,很多人会慢慢总结出一条规律:有一类题的解法特别“固定”——有序数组里找两个数凑目标值、链表中判断有没有环、字符串里找不重复的最长子串,题面长得完全不一样,翻开题解一看,底层全是同一个思路&…

2026/10/9 3:32:13 阅读更多 →

日新闻

Java时间API实战:LocalDate、Date与ZonedDateTime的转换与避坑指南

Java时间API实战:LocalDate、Date与ZonedDateTime的转换与避坑指南

Java时间API这个话题,隔三差五就会在群里被翻出来讨论一次。上周还有个同事线上处理一个订单超时问题,排查到最后发现是ZonedDateTime序列化后时区丢了,用户在下单当天晚上看到的时间整整差了8个小时。这类问题几乎每个做Java开发的人都遇到过…

2026/10/9 0:00:49 阅读更多 →
EasyTier实践:从NAT穿透到子网代理的异地组网部署与排错

EasyTier实践:从NAT穿透到子网代理的异地组网部署与排错

前几个月我手头有好几台机器需要互相访问:办公室台式机、家里 NAS、还有一台云主机。如果只是偶尔传个文件倒还好,问题是工作场景经常要在几处环境之间来回切换,每次都先登录跳板机再层层代理,实在折腾。我先后试过端口映射、自建…

2026/10/9 0:00:49 阅读更多 →
AI Agent工程实战:从七要素到七个决策点的系统设计指南

AI Agent工程实战:从七要素到七个决策点的系统设计指南

AI Agent 这个词在过去一年里被反复提及,但真正动手搭过一套能跑起来的 Agent 系统的人都知道,从"知道它是什么"到"让它稳定干活"之间隔着一整套工程决策。我前后参与过几个 Agent 项目的落地,从最初用现成框架拼装&…

2026/10/9 0:01:50 阅读更多 →

周新闻

KT148A语音芯片外挂8002D功放的工程实践指南

KT148A语音芯片外挂8002D功放的工程实践指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 15:26:32 阅读更多 →
LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 15:26:40 阅读更多 →
ARM架构深度解析:从RISC设计理念到交叉编译实战

ARM架构深度解析:从RISC设计理念到交叉编译实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 10:10:36 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 21:13:17 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 15:26:17 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/7 13:34:55 阅读更多 →