做促销商城这类JavaWeb项目,很多人一开始会觉得无非是商品增删改查加一个购物车,但真把这个题目拆开看——广告、抽奖、促销、打折,这四个词才是真正拉开分水岭的地方。尤其是抽奖和促销规则,直接决定了系统是"一个能跑的CRUD"还是"一个能拿去答辩、能演示运营逻辑的完整管理系统"。我这篇就把做这类系统时最容易被忽略、但恰恰是核心价值的几个模块逐个拆开讲,包括打折促销的规则引擎怎么设计、抽奖概率如何控制、购物车会话方案怎么选、广告位如何动态配置,以及我在实际开发中踩过的坑。
这套系统我用的是经典JavaWeb技术栈:JSP + Servlet + MySQL + Tomcat,完全契合课程设计和毕业设计的需求,不引入Spring这类重量级框架,反而能更清楚地看到Web应用底层的工作机制。
1. 这个管理系统到底要管什么:业务模块与功能地图
很多人拿到"促销商城购物管理系统"这个题目,第一件事就是建表、写登录、做商品列表,然后发现做着做着不知道下一步该干什么。原因很简单——没有先把系统拆成清晰的业务模块。
1.1 系统角色与权限边界的划分
前台用户角色能做什么、后台管理员能做什么,这两条线必须一开始就划清楚,因为后续所有功能都是在角色边界内展开的。
前台用户端,我建议至少包含这些功能:
- 注册与登录:用户名 + 密码,密码在数据库里存MD5或加盐哈希,千万别明文存
- 商品浏览与搜索:按分类筛选、按关键词模糊搜索、按价格排序
- 购物车管理:加入、修改数量、删除、批量结算
- 订单管理:下单、模拟支付、查看订单状态、取消订单
- 促销活动参与:领取优惠券、参与打折、抽奖
管理员后台这边,功能比前台更重:
- 商品管理:上架、下架、库存调整、价格修改
- 订单管理:发货、查看订单详情
- 会员管理:禁用/启用账号、查看会员等级
- 促销管理:配置打折商品、设置满减规则、发放优惠券
- 抽奖管理:配置奖品、设置中奖概率、查看中奖记录
- 广告管理:上传广告图、配置广告位置、设置跳转链接、查看点击统计
这么一拆,整个系统的开发顺序就出来了:先做账号体系,再做商品和购物车这套核心交易链路,然后在此基础上叠加促销、抽奖、广告这三个营销模块。核心交易链路是地基,营销模块是上层建筑,反过来先做营销再做交易,你会发现自己连可促销的商品都没有。
1.2 营销功能是系统区别于普通商城的核心
一个只有商品浏览和下单功能的系统,叫"商城购物管理系统"也能说得过去,但加上"促销"和"广告抽奖"这两个限定词,系统的性质就变了——它是一个具备拉新、转化、促活能力的营销型商城。这就意味着,你的系统里不光要有交易数据,还要有营销数据:谁在什么时候参与过抽奖、哪个广告位点击率最高、哪些商品被设置过折扣、优惠券核销了多少。
如果你在数据库设计阶段就把这些营销相关的表和字段考虑进去,后面写功能的时候会非常顺畅。很多人的项目做到一半推翻重来,就是因为一开始没给营销模块留好数据模型。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 打折与促销规则的引擎化设计
促销是整个系统里最容易写出"死代码"的模块。我见过不少人把促销逻辑直接写在JSP页面里或者Servlet代码中:
java复制double price = product.getPrice();
if (product.getId() == 5) { // 商品ID为5的打8折
price = price * 0.8;
}
这种写法跑通Demo没问题,但没有任何扩展性可言。运营人员每加一个促销商品,你就要改一次代码、重新部署一次。真正合理的做法,是把促销规则当成"数据"来管理,然后用统一的计算引擎去执行。
2.1 促销规则的数据模型设计
我建议设计一张促销规则表,核心字段如下:
| 字段名 | 类型 | 说明 |
|---|---|---|
| promotion_id | INT | 促销ID,主键 |
| promotion_name | VARCHAR(50) | 促销名称,如"夏季大促8折" |
| promotion_type | TINYINT | 促销类型:1-折扣,2-满减,3-优惠券 |
| product_id | INT | 限定的商品ID,0表示全场 |
| condition_value | DECIMAL(10,2) | 条件值,满减场景是门槛金额 |
| benefit_value | DECIMAL(10,2) | 优惠值,折扣场景是0.8,满减场景是减免金额 |
| start_time | DATETIME | 促销开始时间 |
| end_time | DATETIME | 促销结束时间 |
| status | TINYINT | 状态:0-关闭,1-启用 |
有了这张表,运营要发起一个促销活动,只需要在后台插入一条记录,前台结算时由促销引擎动态读取生效中的规则进行计算。商品的折扣不再硬编码在Java类里,而是读表。
这里有一个关键点:折扣到底是用0.8表示8折,还是用80表示? 我建议用0.8的小数形式,字段用DECIMAL(10,2)。计算的时候直接用商品单价乘以这个系数,语义清晰。
2.2 结算时的促销计算逻辑
订单结算的促销计算是整个交易链路的核心。我建议用策略模式来组织不同促销类型的算法,而不是堆一堆if-else。做一个接口:
java复制public interface PromotionStrategy {
// 返回优惠金额
BigDecimal calculateDiscount(OrderItem item, Promotion promotion);
}
满减策略,注意这里判断的是订单总金额:
java复制public class FullReductionStrategy implements PromotionStrategy {
@Override
public BigDecimal calculateDiscount(OrderItem item, Promotion promotion) {
// 满减基于订单总金额,需要结合订单级金额判断
BigDecimal totalAmount = item.getTotalPrice();
if (totalAmount.compareTo(promotion.getConditionValue()) >= 0) {
return promotion.getBenefitValue();
}
return BigDecimal.ZERO;
}
}
折扣策略:
java复制public class DiscountStrategy implements PromotionStrategy {
@Override
public BigDecimal calculateDiscount(OrderItem item, Promotion promotion) {
BigDecimal discountAmount = item.getTotalPrice()
.multiply(BigDecimal.ONE.subtract(promotion.getBenefitValue()));
return discountAmount.setScale(2, RoundingMode.HALF_UP);
}
}
为什么推荐策略模式?因为每增加一种促销玩法,你只需要新增一个实现类,再在工厂里注册一个类型映射,不需要改动订单结算的主流程代码。课程设计的项目做到这个程度,答辩的时候面试官问"系统好不好扩展",你能拿出实际的架构设计来讲,而不是空口说"我用了面向对象"。
2.3 促销叠加与互斥的关键判断
促销引擎最重要的逻辑不是怎么计算折扣,而是决定哪些促销可以叠加、哪些必须互斥。我常用的规则是:
- 折扣和满减互斥:一件商品要么走折扣,要么走满减,不能两种都计算
- 优惠券可以和折扣叠加,但不能和满减叠加
- 会员价是基础价格:先算会员价,再在会员价基础上叠加其他促销
这个规则不写死在代码里,而是用一张促销互斥表,配置"哪两类促销不允许同单参与"。其实对于课程设计来说,把互斥逻辑写在计算引擎里也完全够用,关键是你要有"促销需要定义优先级和互斥关系"这个意识,这比单纯的CRUD高一个层次。
举个例子,订单里有件商品300元,设置了8折促销,同时有一个满300减50的优惠券。如果两者可以叠加,最终价格 = 300 × 0.8 - 50 = 190元;如果互斥,系统要自动为这个订单选择优惠最大的方案(折扣优惠60元 > 满减优惠50元,选折扣)。我在做的时候,让系统自动比较两种互斥促销的优惠金额,取更划算的那种,用户无感知,运营也觉得逻辑合理。
3. 抽奖模块的随机算法与概率控制
抽奖功能听起来简单,无非是后台随机判断中没中,但真正做起来,有几个点容易被带偏:概率是否可配置、奖品库存如何把控、中奖记录如何追溯。
3.1 抽奖流程的完整链路
我设计的抽奖流程是:
- 用户点击抽奖,前端请求Servlet接口,携带用户ID和奖品批次ID
- 后端校验用户资格:是否登录、是否有剩余抽奖次数或积分是否充足
- 扣减抽奖次数或积分,这一步要和抽奖结果在一个事务里,避免用户抽完了结果次数没扣
- 执行抽奖算法,确定中奖结果
- 更新奖品库存、记录中奖明细
- 返回结果给前端,中奖奖品如果是优惠券,直接发放到用户的优惠券账户
步骤2和3之间有个并发问题:如果普通用户用两个浏览器同时请求抽奖接口,可能会导致抽奖次数被扣成负数。解决思路是在扣次数的SQL语句里加上条件判断:
sql复制UPDATE user_profile
SET lottery_chances = lottery_chances - 1
WHERE user_id = ? AND lottery_chances > 0
这样即使并发请求同时到达,数据库的行锁也会保证只有一条SQL能成功执行,从而保证次数不会扣成负数。
3.2 加权随机抽奖算法
奖品不可能每个概率都一样,所以用简单的Math.random()是搞不定的。我采用加权随机算法:每个奖品配置一个权重值,权重越大,抽中概率越高。奖品的等级和权重在后台可配置,运营人员可以随时调整。
算法实现不复杂,但很好用:
java复制public class WeightedRandomLottery {
// prizeList 是从数据库加载的奖品列表,每个奖品都有 weight 字段
public Prize draw(List<Prize> prizeList) {
int totalWeight = 0;
for (Prize prize : prizeList) {
totalWeight += prize.getWeight();
}
int randomNum = ThreadLocalRandom.current().nextInt(totalWeight);
int cumulativeWeight = 0;
for (Prize prize : prizeList) {
cumulativeWeight += prize.getWeight();
if (randomNum < cumulativeWeight) {
return prize;
}
}
return null; // 理论上不会走到这里
}
}
这里的核心思想是把所有奖品的权重加总,然后生成一个0到总权重之间的随机数,按奖品顺序累加权重,落在哪个区间就中哪个奖品。权重为0的奖品天然不可能被抽中,很适合用来做"未中奖"这一档。
关于概率控制,我个人强烈建议把概率设计成"相对概率"而不是"绝对概率"。比如A奖品权重5、B奖品权重15、未中奖权重80,那A的中奖率就是5%。如果运营想调高A的概率,直接把A的权重改成10就行,不需要重新计算其他奖品的概率,系统会自动归一化。
3.3 奖品库存控制的并发处理
抽奖并发是真正考验系统的地方。我第一版代码是"先查库存,大于0就扣减",结果压测的时候库存直接变负数。后来改成了乐观锁方案:
sql复制UPDATE lottery_prize
SET stock = stock - 1
WHERE prize_id = ? AND stock > 0
受影响行数为1,说明扣减成功,可以发奖;为0说明库存没了,本次抽奖应该按未中奖处理或者提示用户奖品已抽完。
另外有个业务细节——未中奖本身也是一条抽奖记录,只是因为奖品ID为空或为0所以不算中奖。记录下每一次抽奖行为,对于后续做数据分析和排查问题非常重要。我在抽奖记录表里存了用户ID、参与时间、抽奖结果、奖品ID、IP地址,后来运营反馈说排查刷奖问题全靠这张表。
3.4 前端抽奖交互的实现思路
前端抽奖的交互形式五花八门:九宫格转盘、刮刮卡、大转盘,但本质上都是体验层的东西,后台接口才是核心。我用的是最稳妥的"先请求后播放"模式:
- 页面加载时向后端请求抽奖结果,拿到中奖情况
- 根据结果播放转盘动画,转到对应奖品位置
- 动画结束后展示中奖弹窗
千万不要用"先转动画、请求结果、把转盘指向结果位置"这种容易导致前端指向和后端结果不一致的做法。最简单可靠的方案是先把接口抽奖结果拿到,再根据结果确定转盘最终停的位置。
4. 购物车与订单的会话管理
购物车是商城系统的命脉,也是JavaWeb会话管理最典型的应用场景。选什么方案存购物车,直接决定了用户体感和开发工作量。
4.1 购物车存储方案对比与选型
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| Session | 实现简单,代码量少,不需要持久化 | 服务器重启数据丢失,占用服务器内存 | 课程设计、中小型Demo |
| Cookie | 可以跨请求保持,不占服务器内存 | 大小受限(4KB),存储在客户端有篡改风险 | 匿名购物车 |
| 数据库 | 永久保存,用户换设备也能同步 | 每次操作都要读写数据库 | 生产级系统 |
课程设计阶段,用Session是性价比最高的选择。但不是简单地把一个List塞进Session就完了,我建议封装一个购物车类:
java复制public class Cart {
private Map<Integer, CartItem> items; // key是商品ID
public void addItem(Product product, int quantity) {
Integer productId = product.getId();
if (items.containsKey(productId)) {
CartItem item = items.get(productId);
item.setQuantity(item.getQuantity() + quantity);
} else {
items.put(productId, new CartItem(product, quantity));
}
}
public BigDecimal getTotalPrice() {
BigDecimal total = BigDecimal.ZERO;
for (CartItem item : items.values()) {
total = total.add(item.getSubtotal());
}
return total;
}
}
用Map的原因是同一个商品重复加入购物车时应该合并数量,而不是生成两条重复记录。同时,购物车中要保存商品的快照信息(加入时的价格和商品名称),而不是只存商品ID——因为下单后再去查商品表,如果管理员改了价格,用户购物车里的价格也会变,这就会产生结算纠纷。我当时做的时候加入了"价格快照"这个字段,被带我的老师特意表扬过,说这是很多初级开发者想不到的点。
4.2 Session会话生命周期与购物车恢复
Session方案最大的缺点是服务器重启后购物车没了。我在实际开发中给了一个折中方案:用户加入购物车时,同时把购物车序列化为JSON字符串存到数据库的购物车备份表里。每次用户把商品加入购物车后,如果5秒内没有后续操作,就把当前购物车同步到数据库;用户登录时检查数据库里有没有备份,有的话提示"是否恢复上次的购物车"。
这个方案代码量不大,但很好地弥补了Session方案的缺陷,而且用到JSON序列化和异步请求,反而成了项目的一个亮点。
4.3 订单状态机的设计逻辑
订单状态的流转是系统里最容易写乱的部分。我用一个常量类管理所有状态,杜绝魔法数字散落在代码各处:
java复制public class OrderStatus {
public static final int PENDING_PAYMENT = 1; // 待付款
public static final int PAID = 2; // 已付款待发货
public static final int SHIPPED = 3; // 已发货
public static final int COMPLETED = 4; // 已完成
public static final int CANCELLED = 5; // 已取消
}
状态流转规则:
- 待付款 → 用户点击模拟支付 → 已付款待发货
- 待付款 → 用户取消 → 已取消
- 已付款待发货 → 管理员发货 → 已发货
- 已发货 → 用户确认收货 → 已完成
每一步状态变化,我都更新订单的status字段和update_time字段,同时往订单日志表里插一条记录。订单日志表非常有用,答辩时你可以直接展示"用户何时下单、何时支付、何时发货"的完整时间线,这比单纯说"系统有订单管理功能"有说服力得多。
下单时还有一个极其关键的操作——扣库存和创建订单必须在同一个数据库事务里。我第一版代码是先创建订单再扣库存,结果有一次用户下单后库存没扣掉,导致超卖。后来改成先扣库存(UPDATE product SET stock = stock - ? WHERE id = ? AND stock >= ?),扣库成功才创建订单,才解决了这个问题。
5. 广告位的数据库化管理与投放逻辑
标题里把"广告"放在第一位,说明这不是一个可有可无的功能,而是系统的核心模块之一。广告管理的核心不是"上传一张图片展示在首页",而是广告位的动态配置和点击数据回收。
5.1 广告位与广告内容的表结构设计
我设计了广告位和广告两张表:
sql复制CREATE TABLE ad_position (
position_id INT PRIMARY KEY AUTO_INCREMENT,
position_name VARCHAR(50) NOT NULL, -- 广告位名称:首页轮播、侧边栏
position_code VARCHAR(20) NOT NULL, -- 编码标识,如 index_banner
width INT,
height INT,
status TINYINT DEFAULT 1
);
CREATE TABLE ad_item (
ad_id INT PRIMARY KEY AUTO_INCREMENT,
position_code VARCHAR(20) NOT NULL,
ad_title VARCHAR(100),
ad_image VARCHAR(255), -- 广告图片路径
target_url VARCHAR(255), -- 跳转链接
sort_order INT DEFAULT 0, -- 排序权重
start_time DATETIME,
end_time DATETIME,
status TINYINT DEFAULT 1 -- 0-下架,1-展示
);
前端首页轮播图的位置固定,但轮播图内容从数据库读取。运营人员要换广告图,只需在后台更新一张图片,前端无需改代码。这就是"广告位"和"广告内容"分离的核心逻辑——位置是固定的模板,内容是可变的配置。
5.2 广告图片上传实现细节
广告图片上传是JavaWeb里比较经典的文件上传场景。如果用Servlet 3.0以上版本,可以用注解方式处理,但要记得在web.xml里配置multipart-config,比如设置最大文件大小。代码如下:
java复制@WebServlet("/admin/uploadAdImage")
@MultipartConfig(maxFileSize = 1024 * 1024 * 3, maxRequestSize = 1024 * 1024 * 10)
public class AdImageUploadServlet extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
Part part = request.getPart("adImage");
String fileName = extractFileName(part);
// 重新生成文件名,防止重名覆盖
String newFileName = UUID.randomUUID().toString()
+ fileName.substring(fileName.lastIndexOf("."));
String uploadPath = getServletContext().getRealPath("/upload/ad");
File file = new File(uploadPath, newFileName);
part.write(file.getAbsolutePath());
// 返回可访问的URL
String fileUrl = "upload/ad/" + newFileName;
// 保存到数据库...
}
}
重新生成文件名是个很关键的细节,因为不同用户上传的图片可能同名,直接保存会互相覆盖。用UUID拼上原始扩展名是最稳妥的做法。
5.3 广告点击统计的异步上报
广告点击统计如果做成同步请求,前端跳转会卡顿。我用的方案是:广告跳转链接指向一个统计Servlet,这个Servlet先记录点击日志,再通过response.sendRedirect()跳转到真实的URL。这里面有个性能优化点——点击记录可以先插入内存队列,再异步批量写入数据库。不过课程设计阶段,直接同步插库问题也不大,访问量没那么高。
广告点击统计表至少要有这些字段:广告ID、用户ID(可能为空)、点击时间、IP地址、来源页面。有了这张表,你可以很直观地看到每个广告位的点击量。我甚至做了一个简单的时间维度聚合查询,按天统计点击趋势,生成的柱状图效果在答辩时是个加分项。
6. 数据库设计:营销系统的骨架与坑
数据库是整个促销商城系统的骨架。早期设计踩到一个坑,后面写代码就要绕很多弯路。下面是我整理出的核心表结构,以及一些常见的设计陷阱。
6.1 核心数据表全景
| 表名 | 用途 | 核心字段 |
|---|---|---|
| user | 用户表(会员表) | user_id, username, password, points, lottery_chances, user_level |
| category | 商品分类表 | category_id, category_name, parent_id |
| product | 商品表 | product_id, category_id, product_name, price, stock, status |
| cart | 购物车备份表 | cart_id, user_id, cart_data, update_time |
| orders | 订单主表 | order_id, order_no, user_id, total_amount, pay_amount, status, create_time |
| order_item | 订单明细表 | item_id, order_id, product_id, product_name, price, quantity, subtotal |
| promotion | 促销规则表 | promotion_id, promotion_name, promotion_type, product_id, condition_value, benefit_value |
| user_coupon | 用户优惠券表 | coupon_id, user_id, promotion_id, status, receive_time, use_time |
| lottery_prize | 抽奖奖品表 | prize_id, prize_name, prize_type, weight, stock, image |
| lottery_record | 抽奖记录表 | record_id, user_id, prize_id, create_time, ip_address |
| ad_item | 广告表 | ad_id, position_code, ad_title, ad_image, target_url, sort_order, status |
订单表拆成orders和order_item两张表是必须的,因为一个订单包含多个商品。orders表存订单维度的信息,order_item表存商品维度的信息,两个表通过order_id关联。千万不要把所有商品明细塞到一个字段里,那会让订单统计查询变成噩梦。
6.2 经典字段设计陷阱
金额字段必须用DECIMAL(10,2),绝对不能使用FLOAT或DOUBLE。二进制浮点数在计算金额时会产生精度误差,比如0.1 + 0.2的结果出来一长串小数。我在做促销计算时用BigDecimal,数据库存DECIMAL,两头都做到精确计算。
库存字段建议用INT UNSIGNED,这样从数据库层面就杜绝了库存变成负数。虽然代码里已经写了库存大于0的判断,但多一层数据库约束总归多一重保障。
时间字段统一用DATETIME,存Timestamp类型。每个表都建议带上create_time和update_time两个字段,排查问题时这两个字段能帮你还原数据变化的时间线。
能用外键吗? 我建议逻辑外键而不是物理外键。也就是说,表与表之间用ID关联,但不加FOREIGN KEY约束。理由非常简单:加了物理外键后,删除商品要先确认有没有订单引用它,删除用户要先确认有没有订单——课程设计阶段对这种操作很不友好,调试起来特别痛苦。逻辑外键加合适的索引,查询效率不受影响,灵活性还更高。
6.3 索引设计的基本原则
索引不用多,但是关键查询路径上必须有:
orders表的(user_id, status)联合索引——用户查看"我的订单"时按用户和状态查product表的(category_id, status)联合索引——按分类查商品order_item表的order_id索引——查订单明细
我见过有人给每个字段都建索引,不但浪费空间,还拖慢插入速度。索引是为了高频查询服务的,先想清楚哪些查询是高频的,再为它们建立索引。
订单号(order_no)的生成也是个值得说明的点。我用的方案是:时间戳 + 用户ID + 随机数,拼成一个20位以内的唯一字符串。绝对不能直接用自增ID做主键暴露给用户,那样别人可以通过修改订单号ID来遍历你的所有订单。
7. 从开发到部署:我踩过的坑
这一部分,我把自己实际开发这个系统时遇到的坑集中说一下。很多问题不实际跑一遍根本不会遇到,但遇到了又会卡你几个小时。
7.1 中文乱码的三处根治方法
中文乱码是JavaWeb经典问题了,数不清的初学者在这里栽跟头。我从三个层面做了处理:
POST请求乱码:在Servlet里获取参数之前,执行request.setCharacterEncoding("UTF-8")。注意位置必须在getParameter()之前,否则无效。最稳妥的办法是写一个编码过滤器(Filter),对所有请求统一设置UTF-8。
GET请求乱码:Tomcat 8.0以上版本默认的URI编码已经是UTF-8,基本不需要额外配置。如果你用的是Tomcat 7或更早版本,需要修改Tomcat的server.xml,给Connector加上URIEncoding="UTF-8"参数。
响应乱码:response.setContentType("text/html;charset=UTF-8")。所有Servlet的响应都要设置这行代码。
我当时把编码过滤器写完之后,整个系统的乱码问题一次性全部解决,比在每一个Servlet里单独设置要省事得多。
7.2 购物车Session会话丢失的问题
开发过程中修改代码后Tomcat会自动重启,Session会随之丢失,用户就需要重新登录,购物车也清空了。这其实是Session方案的固有缺陷,我在开发阶段会频繁遇到。为了减少这类干扰,我做了用户购物车备份表,每次加入购物车就异步备份,这样即使Session丢失也能恢复。
提示:Session默认过期时间是30分钟,可以在web.xml中调整,但建议不要设置太长。如果后台管理员登录的Session一直没有超时,会有安全风险。
7.3 JSP页面中Java代码过多导致的维护噩梦
我第一版页面把Java代码片段全部写在JSP里,一个商品列表页面塞了上百行scriptlet,改动一个样式要在一堆Java代码里找位置,那体验极其糟糕。
后来我强制自己使用JSTL和EL表达式重构页面对数据的展示:
jsp复制<c:forEach items="${productList}" var="product">
<div class="product-item">
<h3>${product.productName}</h3>
<p>价格:¥${product.price}</p>
<a href="product/detail?id=${product.productId}">查看详情</a>
</div>
</c:forEach>
页面整洁了,逻辑可维护性大幅提升。这里也建议在Servlet中把查询结果封装为List<Product>对象后通过request.setAttribute("productList", list)转发给JSP渲染,业务逻辑和页面展示各司其职。
7.4 部署到服务器时最容易忽略的问题
本地开发一切正常,部署到服务器就各种报错,这套路我太熟了。几个高频问题:
数据库访问权限:本地连接MySQL用的是root,部署到服务器后MySQL默认只允许localhost访问,应用连不上数据库。需要给应用创建专用账号并授权远程访问。这里特别提醒,不要把root的密码写在代码里(尤其是公网服务器),创建一个只有业务库权限的账号是更安全的选择。
端口占用:Tomcat默认8080端口,服务器防火墙要放行该端口,云服务器还要在安全组规则中放行。这个步骤很多人漏掉,结果本机访问正常,服务器外网访问不通。
文件上传路径问题:本地Windows的路径和服务器Linux的路径完全不一样。代码里绝对不能写死C:/upload/ad这种路径,要使用System.getProperty("user.dir")动态获取当前目录,或者把上传根目录配置到配置文件中。我在第一版就写死了路径,换到Linux服务器后所有上传功能全部报错,排查了很久才发现是路径分隔符的问题。
部署方式:最省事的方案还是用IDEA直接打包成war包,放到Tomcat的webapps目录下启动。新版IDEA的打包入口可能在"Build"菜单的"Build Artifacts"里,需要先配置Project Structure里的Artifacts选项。
7.5 SQL注入与XSS防护的底线
商城系统涉及大量数据库交互,SQL注入的雷区必须避开。我见过有人写成这样:String sql = "SELECT * FROM user WHERE username = '" + username + "'",这就是典型的注入入口。所有SQL都必须使用PreparedStatement预编译参数绑定:
java复制PreparedStatement ps = conn.prepareStatement(
"SELECT * FROM user WHERE username = ? AND password = ?");
ps.setString(1, username);
ps.setString(2, md5Password);
XSS防护方面,所有用户生成的内容在输出到页面之前都要转义HTML特殊字符。比如用户昵称可能包含<script>标签,直接输出会执行恶意脚本。使用JSTL的<c:out>标签输出用户内容,它会自动进行HTML转义,比直接用${}输出更安全。
这套促销商城购物管理系统做下来,我的最大体会是:技术能力只是一个层面,更难的是对业务规则的梳理和抽象。打折、促销、抽奖、广告这些模块,表面上是增删改查,实际上每一块背后都有一套运营逻辑。你在设计数据表的时候,有没有预留"促销互斥"的扩展;在做抽奖的时候,有没有考虑过并发扣减库存;在写购物车的时候,会不会想到服务器重启后的恢复方案——这些才是区分"能跑"和"好用"的分水岭。哪怕只是个课程设计项目,把它当成真正的产品来打磨,收获会完全不一样。
