SpringBoot+SSM+MySQL+JSP:手把手搭建商城系统的经典实践

1. 题外话:这套技术组合是“老”,但它照样能打

先把结论放前面:如果你在课程设计、毕业设计或者个人练手阶段选了“基于 JavaWeb + SpringBoot + SSM + MySQL + JSP + Maven 的商城系统”,这个方向并不落后,反而是最稳妥的一条路。商城类项目是所有 JavaWeb 项目里最典型的综合性场景——它天然牵扯到用户体系、商品展示、购物车、订单流转、库存扣减、后台管理这些模块,几乎把 Java 后端日常开发会碰到的 CRUD、关联查询、事务、会话管理、权限拦截、文件上传全部覆盖了一遍。换句话讲,你做完这一个项目,约等于把 JavaWeb 时代到 SpringBoot 时代的三板斧全部过了一遍。

我见过不少同学一上来就追求 vue3 + springboot 前后端分离,结果前端框架学了大半年,后端连一个完整的订单事务都没写明白。而 JSP + SpringBoot 的方式恰好把重点拉回到服务端:页面渲染交给 JSP,业务逻辑交给 Service 层,数据访问交给 MyBatis,你不需要纠结跨域、token 刷新、前端路由守卫这些额外概念,可以专心把所有精力放在后端模型设计上。

这篇博文我不会只给一个通篇的“看着能跑”的代码粘贴,而是按照我自己从零到一搭这套商城时踩过的坑、用过的取舍、反复调过的配置来写,尽量做到你跟着走一遍,能真正跑起来,并且知道每一步为什么这么干。

需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。

2. 冷启动:搭工程骨架最容易翻车的地方

2.1 版本组合是最大的坑:SpringBoot 2.7.x + JSP 兼容性

第一件非常重要的事:如果你打算用 JSP 做视图层,千万别直接用 SpringBoot 3.x。SpringBoot 3.0 全面转向 Jakarta EE,底层包名从 javax.* 变成了 jakarta.*,而传统 JSP 的页面解析、Taglib 依赖大量基于 javax.servlet,哪怕你把依赖版本强行调上去了,本地启动也会报各种 ClassNotFoundException 或者 JSP 编译不过的错。

我自己最开始没注意,直接用了当时最新的 SpringBoot 3.1.2,结果 JSP 页面一直 404,后来切换到 2.7.18 后一切正常。所以建议直接用 SpringBoot 2.7.x,JDK 用 8 或者 11(我用的 1.8),Maven 用 3.6.3 以上版本。这套组合你见过的大部分资料都能完全对上,遇到问题搜一下也能得到靠谱答案。

核心依赖大致如下(只列关键部分):

xml复制<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.7.18</version>
    <relativePath/>
</parent>

<dependencies>
    <!-- Web 启动器,内嵌 Tomcat -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- JSP 解析依赖:jasper 负责把 .jsp 编译为 servlet -->
    <dependency>
        <groupId>org.apache.tomcat.embed</groupId>
        <artifactId>tomcat-embed-jasper</artifactId>
        <scope>provided</scope>
    </dependency>

    <!-- JSTL 标签库,页面要用的 c:forEach / c:if -->
    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>jstl</artifactId>
        <version>1.2</version>
    </dependency>

    <!-- MyBatis 与 SpringBoot 整合 -->
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.3.2</version>
    </dependency>

    <!-- MySQL 驱动 -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.33</version>
    </dependency>

    <!-- 数据库连接池,内置的 Tomcat JDBC Pool 可以用,但我更推荐 druid -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid-spring-boot-starter</artifactId>
        <version>1.2.20</version>
    </dependency>

    <!-- 分页插件 -->
    <dependency>
        <groupId>com.github.pagehelper</groupId>
        <artifactId>pagehelper-spring-boot-starter</artifactId>
        <version>1.4.7</version>
    </dependency>

    <!-- Lombok 简化实体类,可有可无 -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>

构建配置里还要注意 packaging 必须改成 war,因为 SpringBoot 内置 Tomcat 默认只处理静态资源和 Controller 返回的模板内容,JSP 需要 Servlet 容器支持,打成 war 包后由外部/内嵌 Tomcat 才能正确编译 JSP。同时在 spring-boot-maven-plugin 里加一句 <mainClass> 指向启动类,保证打成 war 包后用 java -jar xxx.war 也能跑:

xml复制<build>
    <finalName>cosmetic-mall</finalName>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <configuration>
                <mainClass>com.cosmetic.mall.CosmeticMallApplication</mainClass>
            </configuration>
        </plugin>
    </plugins>
</build>

2.2 application.yml 配置的三个注意点

第一个注意点是 JSP 视图解析器。SpringBoot 默认支持 SpringMVC 的视图解析规则,但 JSP 前缀后缀并没有像 spring.thymeleaf.prefix 那样有官方自动配置,需要手动在 application.yml 里加:

yaml复制spring:
  mvc:
    view:
      prefix: /WEB-INF/jsp/
      suffix: .jsp

第二个注意点是 spring-boot 内嵌 Tomcat 对 JSP 的支持要额外配置静态资源放行。如果你把静态文件放在 src/main/webapp/static 下,默认 SpringBoot 不会把 webapp 目录打包进可执行 jar(这也是为什么打 war 包更稳),建议静态资源直接放在 src/main/resources/static/ 下,然后在 JSP 里用 ${pageContext.request.contextPath} 拼接资源路径,这样本地跑和部署到 Tomcat 都不会丢样式。

第三个注意点是你如果用了 MySQL 8.x,驱动类要写 com.mysql.cj.jdbc.Driver,并且连接串里必须加时区和编码参数:

yaml复制spring:
  datasource:
    url: jdbc:mysql://localhost:3306/cosmetic_mall?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false
    username: root
    password: 你自己的密码
    driver-class-name: com.mysql.cj.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource

mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.cosmetic.mall.entity
  configuration:
    map-underscore-to-camel-case: true

pagehelper:
  helper-dialect: mysql
  reasonable: true

map-underscore-to-camel-case: true 这个配置我单独强调一下:MySQL 里习惯用 create_timeuser_name 这种下划线命名,Java 实体类是驼峰 createTimeuserName,开启这个配置后 MyBatis 自动映射,能少写大量 resultMap

2.3 启动类别忘了继承 SpringBootServletInitializer

为了让 war 包能部署到独立 Tomcat,并且被 SpringBoot 的启动器识别,启动类最好继承 SpringBootServletInitializer 并重写 configure

java复制@SpringBootApplication
@MapperScan("com.cosmetic.mall.mapper")
public class CosmeticMallApplication extends SpringBootServletInitializer {

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

    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
        return builder.sources(CosmeticMallApplication.class);
    }
}

这一步不是必须的——如果你只是用 java -jar 本地跑,不写也能启动。但很多同学的评分要求是“打成 war 包丢进 Tomcat 的 webapps 下能跑”,提前把这一步做掉,后面省事。

3. 动手前先想清楚表结构:商城数据的骨架就是这六张表

写商城项目最容易犯的错误是急着写代码。我的经验是,先把表设计出来,再回推实体类和 Mapper,写起代码会顺畅很多。一套麻雀虽小五脏俱全的商城,至少需要这几张表:

  • 用户表 t_user
  • 分类表 t_category
  • 商品表 t_product
  • 轮播图表 t_carousel
  • 购物车表 t_cart
  • 订单表 t_order
  • 订单明细表 t_order_item

先看用户表。重点不是字段多,而是把唯一约束做好。用户名和手机号做唯一索引,防止注册时并发插入两条同名的脏数据。密码保存的是 MD5 加盐后的值,一律不要明文存储,即使只是个课设项目,这也是基本素养。角色字段我用 role 区分管理员和普通用户:0 表示后台管理员,1 表示前台用户。商城系统一般会同时包含前台的用户端和后台的管理端,这个字段方便后续做权限拦截。

商品表是核心中的核心。除开基本的名称、价格、描述、图片,我建议一定要单独加 stock 库存字段和 sales 销量字段。这两个字段直接影响商城日常业务:商品列表按销量排序、商品详情判断还能买多少、提交订单时校验库存。图片字段我存的是相对路径 /static/img/product/xxx.jpg,不存完整 URL,因为本地开发环境、部署到服务器环境,IP 和端口都可能变,完整 URL 存库后换环境就废了。

订单表和订单明细表需要分开设计。t_order 记录一次购买,包含订单号、用户ID、总金额、收货人信息、订单状态、创建时间;t_order_item 记录这一次购买里的每一件商品,包含商品ID、商品名称、商品图片、购买单价、购买数量、小计金额。这么设计是为了“快照”——商品价格和商品名称会变,但订单明细里保存的必须是用户下单那一刻的价格和名称,否则日后查订单会出大问题。

订单状态我用整数表示:0 待付款、1 待发货、2 待收货、3 已完成、4 已取消。用整数比用字符串的好处是后续如果你接支付回调,状态流转判断写起来比较清晰;页面上需要展示“待付款”这几个字的时候,在 JSP 里用一个状态映射方法转成中文即可。

下面给出建表 SQL 的核心部分,字段上我做了一些取舍,够用但不过度设计:

sql复制CREATE TABLE `t_user` (
  `id` int NOT NULL AUTO_INCREMENT,
  `username` varchar(50) NOT NULL COMMENT '登录用户名',
  `password` varchar(128) NOT NULL COMMENT 'MD5加密后的密码',
  `nickname` varchar(50) DEFAULT NULL,
  `phone` varchar(20) DEFAULT NULL,
  `email` varchar(100) DEFAULT NULL,
  `avatar` varchar(255) DEFAULT NULL,
  `role` tinyint NOT NULL DEFAULT '1' COMMENT '0管理员 1普通用户',
  `status` tinyint NOT NULL DEFAULT '1' COMMENT '1正常 0禁用',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_username` (`username`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;

CREATE TABLE `t_category` (
  `id` int NOT NULL AUTO_INCREMENT,
  `name` varchar(50) NOT NULL,
  `sort_order` int DEFAULT '0',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE `t_product` (
  `id` int NOT NULL AUTO_INCREMENT,
  `category_id` int NOT NULL,
  `name` varchar(200) NOT NULL,
  `subtitle` varchar(500) DEFAULT NULL COMMENT '副标题/卖点',
  `detail` text COMMENT '商品详情',
  `main_image` varchar(255) DEFAULT NULL,
  `price` decimal(10,2) NOT NULL,
  `stock` int NOT NULL DEFAULT '0',
  `sales` int NOT NULL DEFAULT '0',
  `status` tinyint NOT NULL DEFAULT '1' COMMENT '1上架 0下架',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `idx_category` (`category_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE `t_cart` (
  `id` int NOT NULL AUTO_INCREMENT,
  `user_id` int NOT NULL,
  `product_id` int NOT NULL,
  `quantity` int NOT NULL DEFAULT '1',
  `checked` tinyint NOT NULL DEFAULT '1' COMMENT '是否勾选 1选中 0未选中',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP,
  `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_user_product` (`user_id`, `product_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE `t_order` (
  `id` int NOT NULL AUTO_INCREMENT,
  `order_no` varchar(64) NOT NULL COMMENT '订单号',
  `user_id` int NOT NULL,
  `total_price` decimal(10,2) NOT NULL,
  `receiver_name` varchar(50) NOT NULL,
  `receiver_phone` varchar(20) NOT NULL,
  `receiver_address` varchar(255) NOT NULL,
  `status` tinyint NOT NULL DEFAULT '0' COMMENT '0待付款 1待发货 2待收货 3已完成 4已取消',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP,
  `pay_time` datetime DEFAULT NULL,
  `send_time` datetime DEFAULT NULL,
  `finish_time` datetime DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_order_no` (`order_no`),
  KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE `t_order_item` (
  `id` int NOT NULL AUTO_INCREMENT,
  `order_id` int NOT NULL,
  `product_id` int NOT NULL,
  `product_name` varchar(200) NOT NULL,
  `product_image` varchar(255) DEFAULT NULL,
  `current_unit_price` decimal(10,2) NOT NULL COMMENT '下单时商品单价',
  `quantity` int NOT NULL,
  `total_price` decimal(10,2) NOT NULL COMMENT '小计金额',
  PRIMARY KEY (`id`),
  KEY `idx_order_id` (`order_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

一个容易忽略的小点:购物车表要设置 user_id + product_id 的唯一索引。没有这个唯一索引,代码里每次加购都要先 select 再判断是新增还是更新数量,而且并发请求下可能出现同一用户购物车里出现两条相同商品记录。有了唯一索引,直接用 INSERT ... ON DUPLICATE KEY UPDATE quantity = quantity + 1 一条 SQL 搞定,省一次查询,也避免重复。

4. 后端编码:SSM 三层架构在 SpringBoot 里的落地姿势

4.1 用户注册登录:加密、Session 和拦截器

用户这块的逻辑不算复杂,但有两个细节值得写进代码习惯里。

第一是密码加密。我用的办法是 MD5 + 固定盐:MD5(password + salt)。注意注册时要先查一遍用户名是否已存在,如果存在直接返回“用户名已占用”。MD5 在真实场景里不够安全,但课设和入门项目里已经能体现“不能明文存密码”的意识。如果你有余力,可以直接用 BCryptPasswordEncoder,代码量差不多。

注册的 Service 里大致是:

java复制@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public Result register(User user) {
        if (userMapper.findByUsername(user.getUsername()) != null) {
            return Result.error("用户名已存在");
        }
        String salt = "cosmetic2024";
        String encrypted = DigestUtils.md5DigestAsHex(
            (user.getPassword() + salt).getBytes(StandardCharsets.UTF_8));
        user.setPassword(encrypted);
        user.setRole(1);
        user.setStatus(1);
        userMapper.insert(user);
        return Result.success();
    }
}

登录成功的关键操作是把用户信息放进 Session。user 对象里绝对不能放密码,只放 idusernamenicknamerole 这些展示需要的信息,我一般单独封装一个 UserVO。然后在 SpringMVC 配置类里注册拦截器,拦截 /user/**/cart/**/order/** 这些路径,从 Session 取不到用户就重定向到登录页,同时放行首页、商品列表、商品详情和登录注册接口:

java复制@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Autowired
    private LoginInterceptor loginInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginInterceptor)
                .addPathPatterns("/user/**", "/cart/**", "/order/**", "/admin/**")
                .excludePathPatterns("/login", "/register", "/index", "/product/**", "/static/**", "/carousel/**");
    }
}

这种基于 Session 的会话管理在 JSP 项目里最自然,JSP 里需要显示当前登录人时直接 ${sessionScope.user.nickname} 就行,不需要像前后端分离那样每次请求都带 token

4.2 商品与分类:列表、搜索、详情和 PageHelper 分页

前台首页的商品展示,我分了三块:分类导航、轮播图、热销商品/新品推荐。这样既能把首页撑起来,又能练到多表查询和关联查询的基本功。

分类导航是最简单的,select * from t_category order by sort_order desc 查出来放到 request 域的 categories,然后在 JSP 顶部循环输出。轮播图需要单独建一张表,字段大概就是 id, image_url, target_url, sort_order, status,后台管理员可以维护。热销商品列表用 select * from t_product where status = 1 order by sales desc limit 8,新品列表改成 order by create_time desc limit 8,两次查询一组合就是首页的大致形态。

商品列表页一定要做分页。这里强烈推荐 PageHelper,因为它和 SpringBoot + MyBatis 的配合非常丝滑:只需要在查询前调用 PageHelper.startPage(pageNum, pageSize),紧接着执行的那条 Mapper 查询就会自动被拦截加上 LIMIT 语句。返回的是一个 PageInfo,里面天生带着 totalpageNumpageslist 这些分页要用的数据。

一个典型的 Controller 方法长这样:

java复制@Controller
public class ProductController {

    @Autowired
    private ProductService productService;

    @GetMapping("/product/list")
    public String list(@RequestParam(defaultValue = "1") int pageNum,
                       @RequestParam(defaultValue = "8") int pageSize,
                       @RequestParam(required = false) Integer categoryId,
                       @RequestParam(required = false) String keyword,
                       Model model) {
        PageInfo<ProductVO> pageInfo = productService.queryList(pageNum, pageSize, categoryId, keyword);
        model.addAttribute("pageInfo", pageInfo);
        model.addAttribute("categoryId", categoryId);
        model.addAttribute("keyword", keyword);
        return "product-list";
    }
}

Mapper XML 里对应的动态 SQL 大概是:

xml复制<select id="queryList" resultType="com.cosmetic.mall.vo.ProductVO">
    select p.id, p.name, p.subtitle, p.main_image, p.price, p.sales,
           c.name as categoryName
    from t_product p
    left join t_category c on p.category_id = c.id
    where p.status = 1
    <if test="categoryId != null">
        and p.category_id = #{categoryId}
    </if>
    <if test="keyword != null and keyword != ''">
        and (p.name like concat('%', #{keyword}, '%')
             or p.subtitle like concat('%', #{keyword}, '%'))
    </if>
    order by p.sales desc, p.create_time desc
</select>

注意使用 concat('%', #{keyword}, '%') 而不是 '%${keyword}%'。用 ${} 拼接会有 SQL 注入风险,用 #{} 参数占位符才是安全姿势。

4.3 购物车与订单:核心流程和事务是关键中的关键

购物车模块本身是几张表的增删改查,但有几个细节影响体验:加购时判断库存是否足够;同一商品再次加购时数量累加而不是新增一行;购物车列表里显示商品当前价格,并且小计要由后端重新计算,不能直接用前端传过来的数字。最后一条尤其重要——一个正经系统里,前端传的任何金额都不可信。

订单模块是整个商城的技术重点。从购物车勾选商品生成订单,到支付(这里我用模拟支付)、发货、收货、取消,这一条链路把 Spring 声明式事务体现得淋漓尽致。

下单的 Service 方法大致如下:

java复制@Service
public class OrderServiceImpl implements OrderService {

    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private OrderItemMapper orderItemMapper;
    @Autowired
    private CartMapper cartMapper;
    @Autowired
    private ProductMapper productMapper;

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Result createOrder(List<Long> cartIds, Long userId, UserAddress address) {
        // 1. 查询勾选的购物车记录
        List<CartVO> cartList = cartMapper.selectCheckedCartItems(cartIds, userId);
        if (cartList == null || cartList.isEmpty()) {
            return Result.error("请选择要结算的商品");
        }

        // 2. 计算总金额,同时逐条校验库存
        BigDecimal totalPrice = new BigDecimal("0");
        List<Product> productsToUpdate = new ArrayList<>();
        for (CartVO cart : cartList) {
            Product product = productMapper.selectById(cart.getProductId());
            if (product == null || product.getStatus() != 1) {
                return Result.error(cart.getProductName() + "已下架");
            }
            if (product.getStock() < cart.getQuantity()) {
                return Result.error(cart.getProductName() + "库存不足");
            }
            totalPrice = totalPrice.add(product.getPrice().multiply(new BigDecimal(cart.getQuantity())));
            // 记录待扣库存的商品
            cart.setCurrentPrice(product.getPrice());
            productsToUpdate.add(product);
        }

        // 3. 生成订单号,插入订单主表
        String orderNo = generateOrderNo();
        Order order = new Order();
        order.setOrderNo(orderNo);
        order.setUserId(userId);
        order.setTotalPrice(totalPrice);
        order.setReceiverName(address.getReceiverName());
        order.setReceiverPhone(address.getReceiverPhone());
        order.setReceiverAddress(address.getReceiverAddress());
        order.setStatus(0);
        orderMapper.insert(order);

        // 4. 插入订单明细,并扣减库存、增加销量
        for (CartVO cart : cartList) {
            OrderItem item = new OrderItem();
            item.setOrderId(order.getId());
            item.setProductId(cart.getProductId());
            item.setProductName(cart.getProductName());
            item.setProductImage(cart.getProductImage());
            item.setCurrentUnitPrice(cart.getCurrentPrice());
            item.setQuantity(cart.getQuantity());
            item.setTotalPrice(cart.getCurrentPrice().multiply(new BigDecimal(cart.getQuantity())));
            orderItemMapper.insert(item);

            productMapper.decreaseStock(cart.getProductId(), cart.getQuantity());
            productMapper.increaseSales(cart.getProductId(), cart.getQuantity());
        }

        // 5. 清空已结算的购物车
        cartMapper.deleteBatch(cartIds, userId);

        return Result.success(orderNo);
    }
}

这个方法加了 @Transactional(rollbackFor = Exception.class),等于告诉 Spring:这个方法里任何一个环节抛异常,前面已经执行的数据库操作全部回滚。这是购物车到订单转化过程中最重要的保证,否则可能出现“订单主表写入成功、明细没写进去”或者“库存扣了、订单没建成”这种数据不一致的问题。

rollbackFor = Exception.class 这里必须显式指定。Spring 默认只在遇到 RuntimeException 时回滚,你如果抛的是 Exception 子类,默认策略下事务不会回滚,库存就白白扣掉了。这类细节面试时最爱问,写代码时也最容易忽略。

5. JSP 前端:不用前后端分离也能把页面写干净

5.1 JSP 在这个项目里的价值

很多新手不理解为什么课程设计还要用 JSP,明明 SpringBoot 官方推荐的是 Thymeleaf。原因分两层:一方面许多学校教材、老项目的资料都是 JSP 体系,用 JSP 意味着你能吃透 Java Web 的完整知识链——Servlet 容器、过滤器、标签库、EL 表达式,这些在面试的时候仍然是基本功;另一方面,JSP 方便实时修改预览,改完 .jsp 页面刷新就能看到效果,而不像某些前后端分离项目,改一个按钮要同时端起前端和后端两套服务。

JSP 写商城,几个实践细节分享给大家。

5.2 公共片段一定要抽出来,否则改样式改到哭

商城页面多:首页、商品列表、商品详情、购物车、结算页、订单列表、后台管理……如果每个 JSP 都复制粘贴一大段 header 和 footer,后面改导航栏里的一个链接,你得手动一个一个文件改,改漏了就出现页面风格不统一。所以第一步是把公共部分抽成片段。

我用的是最传统也最稳的方式:<jsp:include>,不需要额外引入任何依赖,JDK 和 JSP 容器天然支持。

header.jsp 里放公共导航栏和登录状态判断:

jsp复制<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%
    String ctx = request.getContextPath();
    pageContext.setAttribute("ctx", ctx);
%>
<div class="header">
    <div class="logo">
        <a href="${ctx}/index">化妆品商城</a>
    </div>
    <div class="nav">
        <a href="${ctx}/index">首页</a>
        <a href="${ctx}/product/list">全部商品</a>
        <a href="${ctx}/cart/list">购物车</a>
        <a href="${ctx}/order/list">我的订单</a>
    </div>
    <div class="user-info">
        <c:choose>
            <c:when test="${sessionScope.user != null}">
                <span>欢迎,${sessionScope.user.nickname}</span>
                <a href="${ctx}/user/logout">退出</a>
                <c:if test="${sessionScope.user.role == 0}">
                    <a href="${ctx}/admin/index">后台管理</a>
                </c:if>
            </c:when>
            <c:otherwise>
                <a href="${ctx}/user/login">登录</a>
                <a href="${ctx}/user/register">注册</a>
            </c:otherwise>
        </c:choose>
    </div>
</div>

注意我在这里通过 request.getContextPath() 存了一个 ctx 变量。这是 JSP 项目里最常见的引静态资源方式:${ctx}/static/css/style.css。项目部署到 Tomcat 之后,ContextPath 可能变成 /cosmetic-mall,如果写死 /static/xxx,换环境就全崩。

页面头部再加一行:

jsp复制<%@ include file="/WEB-INF/jsp/common/header.jsp" %>

这是静态包含,相当于把 header.jsp 的代码原样复制进来,ctx 变量在每个页面都能用。相应地,底部单独一个 footer.jsp,在页面末尾静态包含。

5.3 商品循环、空列表判断、分页链接的 JSP 写法

商品列表页要做三件事:循环展示商品卡片、判断列表是否为空、生成分页链接。

循环用 JSTL:

jsp复制<c:forEach items="${pageInfo.list}" var="product">
    <div class="product-card">
        <a href="${ctx}/product/detail/${product.id}">
            <img src="${ctx}${product.mainImage}" alt="${product.name}">
        </a>
        <div class="product-name">${product.name}</div>
        <div class="product-price">¥${product.price}</div>
        <div class="product-sales">已售${product.sales}件</div>
        <a class="btn-add-cart" href="${ctx}/cart/add?productId=${product.id}">加入购物车</a>
    </div>
</c:forEach>

空列表判断:

jsp复制<c:if test="${pageInfo == null || pageInfo.list.size() == 0}">
    <div class="empty-tip">还没有相关商品,换个关键词试试吧。</div>
</c:if>

分页链接是一个比较经典的“小技术大麻烦”:很多人的分页总是丢参数,翻到第 2 页以后分类筛选、关键字就没了。正确做法是把当前查询条件通过隐藏参数一起拼进分页链接。我在 Controller 里把 categoryIdkeyword 都放了进 Model,JSP 生成分页时带上:

jsp复制<c:if test="${pageInfo.pages > 1}">
    <div class="pagination">
        <c:if test="${pageInfo.hasPreviousPage}">
            <a href="${ctx}/product/list?pageNum=${pageInfo.prePage}&pageSize=8&categoryId=${categoryId}&keyword=${keyword}">上一页</a>
        </c:if>
        <span>第 ${pageInfo.pageNum} / ${pageInfo.pages} 页</span>
        <c:if test="${pageInfo.hasNextPage}">
            <a href="${ctx}/product/list?pageNum=${pageInfo.nextPage}&pageSize=8&categoryId=${categoryId}&keyword=${keyword}">下一页</a>
        </c:if>
    </div>
</c:if>

这里 categoryIdkeyword 可能为 null,拼在 URL 里就是 categoryId=null,MyBatis 参数接收后 categoryId != null 判断依然成立,会产生错误查询。所以在前端处理一下:

jsp复制<c:set var="categoryParam" value="${empty categoryId ? '' : categoryId}" />
<c:set var="keywordParam" value="${empty keyword ? '' : keyword}" />

然后在链接里用 categoryId=${categoryParam}&keyword=${keywordParam},避免 null 值乱入。

5.4 商品详情页:数量选择和加入购物车

商品详情页 JSP 的核心是一个带数量选择器的购买表单,数量不能超过库存。这部分我用最原始的 JS 实现:

jsp复制<div class="detail-info">
    <h2>${product.name}</h2>
    <div class="subtitle">${product.subtitle}</div>
    <div class="price">¥${product.price}</div>
    <div class="stock">库存:${product.stock} 件</div>
    <form action="${ctx}/cart/add" method="post">
        <input type="hidden" name="productId" value="${product.id}">
        <div>
            <span>数量:</span>
            <button type="button" onclick="changeQty(-1)">-</button>
            <input type="text" id="qty" name="quantity" value="1" readonly style="width:40px;text-align:center;">
            <button type="button" onclick="changeQty(1)">+</button>
        </div>
        <button type="submit">加入购物车</button>
    </form>
</div>

<script>
    var maxStock = ${product.stock};
    function changeQty(delta) {
        var current = parseInt(document.getElementById('qty').value);
        var newVal = current + delta;
        if (newVal < 1) {
            newVal = 1;
        }
        if (newVal > maxStock) {
            alert('库存最多 ' + maxStock + ' 件');
            newVal = maxStock;
        }
        document.getElementById('qty').value = newVal;
    }
</script>

前端限制数量只是体验层面的保护,真正的库存校验仍然必须发生在后端,这个原则我在前面已经反复强调过,JSP 页面里也一样。

6. 订单流程里的三个硬骨头

6.1 库存扣减怎么做到不超卖

一个商城项目如果只是“能把流程跑通”,那库存扣减随便 update product set stock = stock - 1 就行。但如果你想在面试时讲出点东西,或者项目答辩时经得起老师追问,就必须考虑并发场景:同一件商品,两个用户同时下单,库存只剩 1 件,会不会两个人都下单成功?

我在实现里用了最简单可靠的办法:在扣减库存的 SQL 上加上 stock >= #{quantity} 条件。

xml复制<update id="decreaseStock">
    update t_product
    set stock = stock - #{quantity}
    where id = #{productId}
    and stock >= #{quantity}
</update>

这样当库存不足时,update 影响行数为 0,Service 层只需要检查 result == 0 就知道超卖了,直接抛异常回滚事务。比先 select 再判断库存再 update 的方式更安全——因为 select 和 update 之间存在时间差,并发下可能两个请求都读到库存足够,然后都执行了 update,导致超卖。

我的完整下单流程里,校验库存之后实际扣库存时还会再执行一次这个带条件的 update 作为兜底,这是双保险:

java复制int updated = productMapper.decreaseStock(productId, quantity);
if (updated == 0) {
    throw new RuntimeException("库存不足");
}

这个方案不是最强的,但对于单机部署的课设和入门项目完全够用,而且能讲清楚“为什么不用悲观锁/乐观锁”——因为这个业务规模下,一条带条件的 update 就完成了原子扣减,代价最小。

6.2 订单状态机的设计

订单从生成到完成,状态不是随便乱跳的。我在系统里维护了下面这张状态流转表:

状态 可执行操作 下一状态
待付款 0 取消订单 / 模拟支付 已取消(4) / 待发货(1)
待发货 1 管理员发货 待收货(2)
待收货 2 用户确认收货 已完成(3)
已完成 3
已取消 4

零散状态用一个整数来表示,Service 里写一个校验方法:

java复制private boolean canTransition(Integer currentStatus, Integer targetStatus) {
    return (currentStatus == 0 && (targetStatus == 1 || targetStatus == 4))
        || (currentStatus == 1 && targetStatus == 2)
        || (currentStatus == 2 && targetStatus == 3);
}

这个校验看着简单,但在真实项目里价值很大。否则你写的 updateStatus 方法谁都能调用,前端只要改一下请求参数就能把订单从“待发货”直接改成“已完成”,这就是权限漏洞。后台管理端发货、用户确认收货、用户取消订单,都应该走各自独立的 Service 方法,方法内部先查当前状态再做状态校验,而不是暴露一个通用的 updateStatus

模拟支付这个环节,我在项目里只做了非常简单的一步:把订单状态从 0 改成 1,然后记录 pay_time。真实项目中这里会对接微信/支付宝,需要回调验签、第三方报文解析,整套流程相当考验功底,但核心思路不变:支付成功异步回调,回调里根据订单号把订单状态置为已支付。

6.3 金额计算为什么不能信前端

前端展示购物车总金额、商品价格小计,一切看着都合理,但后端必须重新从数据库查价格再算一遍。为什么?因为前端的数据可以被伪造,任何人打开浏览器开发者工具,都可以把请求体里的价格字段从 100 改成 0.01。

我在生成订单的方法里,价格一律以 productMapper.selectById(productId) 查出来的 price 字段为准,购物车表里存的商品价格字段我都不作为下单价格依据。购物车展示时可以取一个 ProductVO 里的实时价格,但是下单时重新查表取最新价。这个原则在简历上写“订单金额以后端计算为准”绝对是个加分项。

还有一个容易忽略的点:订单明细里保存 current_unit_priceproduct_name 的快照。这是防止商品后来改价、改名导致历史订单对不上账,也是电商系统的行业惯例。以后用户投诉“我买的时候是 50 块,跟我的订单记录对不上”的时候,就能用订单明细里的快照作为证据。这个思路在答辩里讲出来,老师会认为你理解到了实际业务的层次。

7. 打包部署、本地运行和常见问题排查

7.1 打成 war 包还是 jar 包?

两种方式都可以,取决于你怎么评分。如果是老师要求“部署到 Tomcat”,打 war 包丢到 webapps 下即可;如果只是本地跑或者 Docker 部署,用 java -jar 直接运行也完全没问题。

打出 war 包的操作:

bash复制mvn clean package -Dmaven.test.skip=true

执行完,target 目录下会生成 cosmetic-mall.war。把这个文件复制到 Tomcat 的 webapps 目录,启动 Tomcat,访问的时候路径需要加 ContextPath:

code复制http://localhost:8080/cosmetic-mall/index

如果打的是 jar 包,直接把 cosmetic-mall.war 当 jar 跑:

bash复制java -jar cosmetic-mall.war

访问路径就不带项目名,直接:

code复制http://localhost:8080/index

7.2 本地运行最常见的 5 个问题

现象 大概率原因 解决办法
JSP 页面 404 没加 tomcat-embed-jasper 依赖,或 packaging 不是 war 检查 pom.xml,加依赖并改为 war
页面能打开但 CSS/JS 全丢 JSP 里用了绝对路径 /static/...,没有加 ContextPath 全部改成 ${ctx}/static/... 写法
数据库连接失败 MySQL 8 驱动类写错,或连接串没带时区 驱动类写 com.mysql.cj.jdbc.Driver,连接串加 serverTimezone=Asia/Shanghai
中文乱码 页面编码和数据库编码不一致 MySQL 表统一 utf8mb4,JSP 顶部加 pageEncoding="UTF-8",连接串加 characterEncoding=utf8
Invalid bound statement Mapper 接口和 XML 的 namespace 不匹配 检查 XML 的 namespace 是否等于接口全限定名,方法 id 是否等于接口方法名

最后一个问题在 MyBatis 项目里出现频率极高,几乎每个新手都会碰到。Invalid bound statement 的报错信息会直接告诉你是哪个方法没绑定成功,点开报错日志,十有八九是 Mapper XML 的 namespace 写成了 com.cosmetic.mall.mapper.ProductMapper,但接口实际包名是 com.cosmetic.mall.dao.ProductMapper,或者 XML 里的方法 id 和接口方法名不一致。这类问题通常不是逻辑问题,而是命名问题,所以我的习惯是:接口文件和 XML 文件名保持一致,放同一个包路径下,能避免大部分手误。

7.3 后续还能往哪扩展

这套商城系统已经具备一个完整电商骨架,往后的扩展方向非常清晰。我按优先级列三个方向:

方向一是后台管理功能增强。管理员目前能管理分类、商品和订单,但缺了数据统计报表。可以加一个简单的首页数据面板:今日订单数、总销售额、库存预警列表,SQL 都是聚合查询,学习价值很高。

方向二是支付功能真实化。可以尝试接入沙箱环境,把模拟支付替换成真实的第三方支付下单流程,重点学习回调验签、异步通知处理、掉单补偿。

方向三是管理端从 JSP 改成 Vue 独立部署。这个更适合你已经有前后端分离基础之后再去动,思路是后端仍然使用 SpringBoot,只写 REST API,JSP 页面保留给前台用户端,后台管理用 Vue 写单页应用,通过 Axios 调接口,鉴权方式从 Session 改成 Token。

8. 写在最后:关于这段开发经历,我的一点体会

整个项目从零到一这次做下来,我最大的感触是:这类“老技术栈”项目,真正考验人的不是某一个单独的技术点,而是整体串联能力。配置一个分页插件、写一个事务注解、建几张表,单独拎出来都不难,但要把 JSP 的页面渲染和 SpringBoot 的后端契约、Mapper 的 SQL 和 MySQL 的索引设计、订单状态和购物车清除逻辑全部对上,才能算真正跑通一个系统。

如果你也打算照着这个思路自己搭一套,我建议别贪多,先把最核心的“注册登录 → 浏览商品 → 加购物车 → 下订单 → 模拟支付 → 后台发货 → 确认收货”这条链路完整跑通,再回头补轮播图管理、商品评论、个人中心这些东西。一开始就想着把淘宝所有功能都塞进去,往往是一个模块都做不深。

另外一个很实用的小技巧:开发阶段每次都从首页手动点击操作太费时间,我建议用 IDEA 的 HTTP Client 或者 Postman 把注册、登录、加购、下单这几个关键流程的请求保存下来,每次改完代码跑一遍接口集合,能帮你快速发现某个改动是不是把流程弄断了。这其实就是最原始的接口自动化回归测试意识,成本很低,收获很大。

内容推荐

虚拟机忘记密码?Windows/Linux修改密码方法实战
虚拟机 · 密码重置 · VMware
虚拟化技术通过软件模拟硬件环境,将整个系统封装为可管理的镜像文件,这为系统维护带来了前所未有的灵活性。当虚拟机因密码遗忘而无法访问时,无需像物理机那样拆机或重装系统,只需利用虚拟机的启动顺序控制和ISO挂载机制,即可进入维护模式或借助外部救援环境重置密码。虚拟机密码恢复的原理在于,管理员可以通过引导参数修改或挂载系统盘,获得一个具备系统权限的Shell,从而执行改密操作。这项技术广泛应用于运维应急、系统故障恢复、安全审计等场景,无论是企业级虚拟化平台还是个人桌面虚拟化工具,均适用。本文结合VMware与VirtualBox等常见环境,深入讲解Windows和Linux虚拟机在忘记密码时的重置方案,涵盖单用户模式、LiveCD、PE工具等常见路径,并分享实际踩坑经验,帮助读者快速恢复系统访问权。
SQL插入数据实战指南:从INSERT语法到批量优化与踩坑避险
SQL插入 · INSERT语句 · 批量插入
在数据库日常开发中,新增数据是最常见的操作之一,但看似简单的INSERT语句背后,往往隐藏着语法差异、性能瓶颈与安全风险。从基础的单条插入到批量写入,从MySQL到SQL Server,如何高效准确地添加数据,是每位开发者必须掌握的技能。同时,插入后获取自增ID(如TP5框架中的db方法)和SQL文件导入(如用DBeaver导入sql)也是高频需求。而像sql注入万能密码绕过这类安全问题,更是提醒我们在拼装SQL时要保持警惕。本文从INSERT的基础语法出发,深入探讨批量插入优化、自增ID获取、客户端工具导入细节及常见报错排查,帮助你在实际项目中少踩坑。
Windows更新暂停时间延长全攻略:注册表、组策略与脚本实操
Windows更新 · 暂停更新 · 注册表
Windows系统的自动更新机制在保障安全的同时,也可能在关键时刻强制重启中断工作。理解其底层原理,有助于我们灵活控制更新节奏。暂停更新本质上是通过注册表中的时间字段设置一个定时窗口,系统据此决定是否检查或安装更新。通过修改注册表、配置组策略或使用PowerShell脚本,用户可以在家庭版和专业版上突破默认35天的限制,将暂停时间延长至90天、180天甚至更久。此外,结合组策略延迟更新和流量计费连接等技巧,还能进一步优化更新管理策略,避免突发重启带来的困扰。本文从原理出发,系统梳理了多种实操方案与常见问题排查,帮助你在安全与效率之间找到平衡。
MySQL死锁排查实录:一个缺失索引引发的蝴蝶效应
MySQL · 死锁 · 索引优化
在数据库性能优化中,索引与锁机制始终是核心议题。当一条SQL查询因索引设计不合理而退化为全表扫描时,不仅会拖慢响应速度,更会在高并发场景下放大锁的覆盖范围,延长持锁时间,最终诱发死锁甚至服务雪崩。本文从一次真实的MySQL订单系统事故出发,梳理了一条完整的问题链路:慢查询告警 → 锁等待加剧 → 死锁频发 → 线程池耗尽。通过结合performance_schema工具定位锁等待源头,并采用复合索引、覆盖索引以及业务层重试机制,成功将系统从频繁告警中恢复。文章不仅复盘了故障排查过程,还提供了一套可落地的索引审查与锁监控方案,帮助开发者在面对相似场景时建立起从原理到实战的完整认知,防患于未然。
MySQL从入门到精通:环境搭建、SQL进阶与性能优化避坑指南
MySQL · 数据库 · SQL优化
数据库是后端开发的基础设施,而MySQL以其稳定性和易用性成为绝大多数项目的首选。环境搭建是入门的第一道关卡,版本选择、Windows或Docker部署、客户端连接认证问题,往往是新手卡住时间最久的环节。在完成环境准备后,真正拉开开发效率差距的是SQL掌握深度:建表字段类型决策、ACID事务与隔离级别的理解、存储过程的编写与错误处理,以及关联查询的索引设计,这些技术点直接决定业务代码的稳定性和响应速度。从单表操作到多表JOIN,从基础增删改查再到聚合函数和性能分析工具的使用,每一层都对应着实际项目中的高频场景。本文将完整梳理从0到1的MySQL学习路线,帮助开发者在最短时间内构建扎实的数据库实操能力。
CFD数值仿真选型:FVM与LBM原理对比及颗粒热流实战
CFD · FVM · LBM
计算流体力学(CFD)是工程与科学研究的核心工具,其中有限体积法(FVM)与格子玻尔兹曼方法(LBM)代表了两种截然不同的数值框架。FVM基于宏观守恒方程,通过控制体通量平衡求解流动,依赖成熟的压力速度耦合算法与网格生成流程,在可压缩流、燃烧及工业应用中占据主导地位;LBM则从介观粒子分布函数出发,通过碰撞-迁移规则统计宏观量,天然规避了压力迭代难题,特别适合多相流、颗粒流及多孔介质等复杂场景。理解两者底层原理与工程边界,有助于面向实际需求合理选型。本文从数值模拟工程师视角出发,系统对比两种方法的数学基础与网格逻辑,并深入LBM-DEM耦合的颗粒热流实战,分享参数换算、时间步匹配及典型错误排查经验,为CFD从业者提供可落地的技术参考。
JSP勤工俭学网项目:从环境部署到调试排错全指南
JSP项目 · Servlet · JDBC
JSP是JavaWeb开发中的经典技术,基于Servlet和JDBC构建动态网站。其原理是浏览器请求经Tomcat容器解析,由Servlet处理业务逻辑,通过JDBC访问MySQL数据库,最终由JSP渲染页面。在高校课程设计与毕业设计中,JSP技术栈因其结构简单、易于理解,仍是主流选择。以昆明城市学院勤工俭学网为例,涵盖岗位发布、学生申请、管理员审核等核心业务,是典型的“程序+源码+数据库+调试部署”项目。本文从环境版本配置、数据库初始化、IDE导入部署,到常见中文乱码、端口占用、数据不显示等排查链路,完整梳理了JSP项目从零跑通的全流程,帮助开发者快速上手类似工程。
rm -rf误删文件怎么恢复?三套方案从lsof到extundelete再到git回滚
rm -rf恢复 · Linux文件恢复 · lsof
在Linux日常运维与开发中,rm -rf是高风险命令的代名词,误删后文件看似彻底消失,实际只是目录项与inode标记被清除,数据块内容仍可能残留在磁盘上。理解文件系统删除原理是恢复的前提:只要进程未退出,可通过lsof从/proc文件描述符直接复制;若进程已退出且分区未被大量写入,可用extundelete或debugfs进行块级扫描重建;若提前使用git管理目录或配置了LVM、btrfs快照,则能通过reflog或快照实现秒级回滚。本文面向服务器管理员、DevOps与开发者,覆盖从应急处理、只读挂载到工具选择的完整恢复链路,并延伸至虚拟机删除文件后宿主机空间不释放的清理场景,帮助你在“跑路三连”发生后冷静应对、最小化数据损失。
会议室签到系统开发详解:基于Python+tkinter+SQLite的课程设计实践
Python · tkinter · SQLite
数据库设计是桌面应用开发中的核心环节,对于课程设计类项目尤为关键。合理的表结构、状态字段设计,能显著提升签到系统等管理类应用的扩展性与维护性。Python作为入门友好的编程语言,配合标准库tkinter可快速搭建图形界面,而SQLite嵌入式数据库则提供轻量级的数据持久化方案,无需独立服务端配置。本文从需求边界梳理入手,深入剖析员工表、会议表、签到记录表的设计原理,讲解登录验证、防重复签到、统计报表等核心代码的工程实现,并总结常见踩坑点与优化方向,旨在帮助初学者理解桌面应用开发的完整链路,为团队协作或企业会议管理提供可靠的自建系统参考。
编码器对接NVR没信号?一份从网络协议到编码参数的排障指南
编码器 · NVR · ONVIF
视频监控系统由模拟向网络化演进的过程中,编码器作为连接模拟摄像机与NVR的关键桥梁,常因配置不当导致“没信号”问题。实际故障往往并非硬件损坏,而是IP网段、接入协议、编码参数等细节错位。理解H.264/H.265等编码格式的兼容性差异,掌握ONVIF与RTSP等主流协议的配置原理,能大幅提升排查效率。无论是在老旧模拟项目利旧改造,还是集中转码上墙场景中,从设备自检、VLC拉流到NVR日志分析,形成系统化的排障链路,都能帮助工程人员快速定位根因。本文结合真实案例,梳理了从网络层、协议层到物理链路的完整排查思路,为安防集成与视频监控运维提供可直接落地的参考。
免费无广告计时提醒工具实测:倒计时、番茄钟与多端配置
计时器 · 倒计时 · 番茄钟
在现代效率工具中,计时提醒看似基础,却是高频刚需。无论是厨房烹饪、会议控场还是番茄工作法,一个可靠的倒计时器能显著提升时间管理效率。这类工具的核心原理依赖系统后台任务与通知机制,但很多免费App通过植入广告和过度采集数据来变现,反而干扰专注。真正的技术价值在于:核心功能本地化、通知可配置、无广告且尊重隐私。从应用场景看,手机端适合移动计时,桌面端可通过浏览器标签页实现常驻提醒,系统自带计时器则作为稳定备胎。基于这些考量,一套免费无广告的计时提醒方案可供直接上手,功能覆盖倒计时、正计时、番茄钟与重复提醒,并包含多端配置与常见问题避坑。
AI时代制高点:判断力×数据质量×工程化落地
AI工程实践 · AI时代制高点 · 模型评测
人工智能技术迭代加速,单一模型或算法很难构成长期壁垒。真正决定AI项目成败的,是围绕业务场景构建系统化工程能力:既要做出精准的技术选型判断,也要把数据治理和模型评测贯穿始终。从大模型部署、量化压缩到推理性能调优,从标注质量管控到Agent多轮任务编排,每一项工程实践都直接影响线上效果与成本。结合营销视频生成、SQL生成助手、智能客服等典型场景,解析如何通过多维评测体系识别模型优劣,如何用RAG与校验机制抑制幻觉,以及如何搭建复合型AI人才梯队。当技术回归工程本质,持续正确的决策与快速迭代的执行,才是智能时代最坚实的护城河。
从CPU缓存到KV Cache:一文看懂各种Cache的底层逻辑与清理策略
缓存 · Cache · CPU缓存
缓存(Cache)是计算机系统中无处不在的加速机制,从CPU的L1/L2缓存到Linux页缓存,再到浏览器HTTP缓存,底层都依赖局部性原理与缓存一致性协议(如MESI)。理解缓存的工作原理,有助于开发者排查性能问题、处理缓存清理的常见陷阱。在工程实践中,从pip cache、Gradle cache到huggingface cache,不同工具的缓存管理方式各异;而在AI推理领域,KV Cache的显存优化更是高性能部署的关键。系统梳理从硬件到LLM的各类Cache场景,帮助你辨别哪些缓存能删、哪些不能乱动,并掌握对应的排查与优化方法。
用Python分析Spotify听歌历史:从数据导出到可视化完整指南
Spotify数据分析 · Python · 音频特征
在数字化生活中,个人行为数据的价值日益凸显。Spotify作为主流音乐平台,允许用户导出完整的听歌历史JSON日志,这为数据分析爱好者提供了一个绝佳的实践入口。通过Python对播放记录进行清洗、挖掘与可视化,我们不仅能还原官方年终总结背后的统计口径,更能发现个人口味演变的深层规律。本文从数据获取方式讲起,对比导出文件与Web API的适用场景,深入解析时间字段的时区陷阱、播放时长归一化、噪音记录过滤等数据清洗关键技术。进一步利用音频特征字段,如energy、valence、danceability,构建个人音乐口味画像,并结合热力图、条形图等可视化手段,将行为数据转化为直观洞察。该实践融合了数据采集、清洗、特征工程、可视化全链路,既适用于个人生活复盘,也为音乐推荐系统等更广泛的数据分析任务提供了可复用的方法框架。
读报错学英语:6个开发高频词,让你少查翻译器
开发英语 · 报错信息 · git
技术文档和报错信息构成了开发者日常的英文语境。报错并非随机字符,而是由一系列高频词组成:git 要求 explain 合并原因,身份配置问题会提示 identity 或 identify,进程或应用无法启动时报 failed to launch,建议替代方案时使用 instead,页面头部常见 meta 标签。这些词在不同工具间反复出现,理解其核心含义与固定搭配,能快速定位报错指向的环节,减少对翻译工具的依赖。从 explain 到 meta,每个词都对应一个典型的开发场景:提交信息、用户认证、数据库排序、程序启动、配置推荐和元信息声明。依托真实报错语境积累词汇,比孤立背单词更高效,这正是开发者提升技术英语阅读能力的关键路径。
多租户系统开发实战:从数据隔离到上下文传递的关键设计
多租户 · 租户隔离 · 数据隔离
在SaaS与云原生应用快速普及的当下,多租户架构已成为支撑规模化服务的基础能力。其核心思想是通过数据隔离与资源共享,让一套系统安全地为多个租户提供服务,从而显著降低部署与运维成本。实现多租户并非简单增加租户ID字段,而需要围绕租户识别、上下文传递、数据访问路由、缓存隔离等关键链路进行系统化设计。基于Java技术体系,可借助ThreadLocal传递租户上下文,并结合MyBatis拦截器自动改写SQL,确保数据访问层的强制隔离。同时,文件存储、定时任务、权限模型与资源配额也都需纳入租户维度,才能构建稳定可靠的企业级应用。从独立部署走向租户化改造,正是许多开源平台与商业产品的演进路径,掌握系统化的多租户设计方法具有重要的工程实践价值。
Spring Boot集成YOLOv8 ONNX推理的Docker容器化部署实践
YOLOv8 · ONNX Runtime · Spring Boot
目标检测模型的工程化落地是算法交付的关键环节。训练完成的YOLOv8权重无法直接被Java后端调用,需要通过ONNX格式转换。本实践基于ONNX Runtime Java API,在Spring Boot框架中完成模型推理服务化封装,并利用Docker容器实现跨环境一致性部署。这一技术路线将Python推理环境隔离在容器之外,使业务方通过标准HTTP接口即可获得检测结果。该方法适用于需要高并发、可维护的AI服务场景,为算法团队与后端工程团队提供了统一的模型服务接入方案。围绕YOLOv8、ONNX Runtime、Spring Boot及Docker的技术整合,本文给出从模型导出到接口测试的完整参考。
RN for OpenHarmony 收藏功能实战:从数据存储到状态同步
React Native · OpenHarmony · AsyncStorage
跨平台开发已成为移动应用降本增效的主流方案,React Native 凭借一套 JavaScript 代码即可覆盖多端。随着 OpenHarmony 生态逐步完善,React Native for OpenHarmony 让同一套业务逻辑可以无缝运行在鸿蒙设备上。以资讯应用中的“我的收藏”功能为切入点,详细讲解如何利用 AsyncStorage 实现本地持久化,并通过 React Context 进行跨页面状态同步。同时,针对长按菜单、点击外部关闭等交互细节,分享在 OpenHarmony 上的适配经验。无论你是跨端开发新手,还是正在适配 OpenHarmony 的工程师,都能从中获得可复用的实践方案。
PyTorch实战:CNN实现MNIST图像分类,准确率突破99%
卷积神经网络 · CNN · PyTorch
图像分类是深度学习最经典的应用场景之一,而MNIST手写数字识别正是入门该领域的标准任务。传统全连接网络在处理图像时需要将像素展平为一维向量,不仅造成参数爆炸,还丢失了像素间的空间结构信息,导致准确率难以突破95%。卷积神经网络(CNN)通过局部感受野、权值共享和池化三大机制,有效提取图像局部特征并显著降低参数规模,成为图像任务的主流选择。本文基于PyTorch框架,从数据加载和预处理出发,逐步实现一个LeNet-5风格的CNN模型,详解卷积、池化后的维度变化与训练细节,并借助混淆矩阵和错误样本进行误差分析。最终在MNIST测试集上达到99%以上的准确率,同时介绍数据增强、BatchNorm等进一步提升精度与速度的实用技巧。这一过程不仅掌握了CNN的核心原理,也为迁移到真实图像任务打下坚实基础。
MinIO + Nginx:企业级对象存储文件服务搭建与实战
MinIO · Nginx · 对象存储
对象存储已成为现代应用处理海量非结构化数据的基础设施,S3协议则成为事实上的标准接口。MinIO作为一款开源的S3兼容对象存储服务器,通过纠删码保护数据安全,支持多版本控制与预签名URL;Nginx反向代理则为其提供统一入口、HTTPS终止和负载均衡。二者组合既能解决传统文件系统在路径迁移、备份、水平扩容上的痛点,又能满足企业内部文件服务的高可用与安全隔离要求。本文从容量规划、Docker Compose部署、Nginx关键参数配置到安全加固与故障排查,完整梳理一套可直接落地的企业级文件服务架构。
已经到底了哦
精选内容
热门内容
最新内容
安卓手机添加音乐全攻略:从有线传输到本地整理
在移动办公与日常娱乐场景中,将音乐文件高效存入安卓手机并让播放器正确识别,是很多用户常遇到的痛点。其核心不在于单纯的文件拷贝,而在于理解Android系统的存储访问机制与媒体库扫描原理。从Android 10开始的分区存储策略,使得应用只能访问公共媒体目录或被授权的特定文件夹,若文件落入App私有沙盒,系统媒体库便不会收录,自然无法被播放器发现。掌握这一底层逻辑后,无论是通过USB数据线进行大批量导入,还是利用局域网工具实现无线传输,都能有效避开“传完找不到文件”的陷阱。进一步地,合理规划Music目录结构、补全音频文件的元数据标签,还能让曲库排列有序。本文以本地音乐管理为切入点,系统梳理了有线传输、无线传输、手机端直接获取及后续整理的全流程,帮助用户在各类场景下快速实现音乐入库与清爽管理。
JVM进程缓存实战:从Caffeine选型到Full GC避坑指南
缓存是提升系统吞吐与响应速度的核心手段,从Redis等分布式缓存到应用内JVM进程缓存,本质是在网络开销与内存成本之间做权衡。JVM进程缓存将数据直接驻留于堆内,省去序列化与网络IO,尤其适合读多写少、允许短暂不一致的热点数据。然而,它并非简单的Map替换,需要理解Caffeine的W-TinyLFU淘汰机制、expireAfterWrite与refreshAfterWrite的配合,以及容量规划时对堆内存的真实占用估算。同时,进程缓存天然面临缓存击穿、多实例数据一致性、Full GC风险等工程挑战,合理设计过期抖动、回源合并与主动失效机制是稳定运行的关键。本文结合真实故障案例,提供从选型、参数配置到内存调优的完整实践框架,帮助开发者在高并发场景下安全落地本地缓存,避免因不当使用引发的性能雪崩。
张家界武陵源一日游最优路线:袁家界+天子山+金鞭溪
武陵源作为典型的喀斯特地貌自然遗产,其核心景区的游览动线设计一直是自由行游客关注的焦点。合理规划一日行程,需要在垂直落差巨大的峰林峡谷中高效衔接山顶观景平台与谷底徒步步道。袁家界、天子山、金鞭溪分别代表山顶、山腰、谷底三种视角,依托百龙天梯和天子山索道的垂直交通,可形成闭环路线。该方案适用于时间有限的游客,既能体验金鞭溪的峡谷徒步,又能观赏袁家界的悬浮山奇观和天子山的西海峰林,同时有效规避排队高峰。本文以实操经验为基础,梳理出从森林公园门票站进山、经水绕四门至袁家界、再赴天子山的详细行程,为计划一日游览武陵源的游客提供可执行的时间分配与避坑指南。
OpenClaw云上部署实战:从环境搭建到微信飞书接入全攻略
AI智能体正在从对话工具演化为能自主执行任务的数字管家,其核心是智能体编排框架。这类框架通过运行时、模型服务与渠道网关三层协同工作,实现对消息的解析、工具调用和结果回传。在工程实践中,借助Docker容器化部署可以显著降低环境依赖带来的复杂度,而模型层则可灵活接入NVIDIA NIM、Ollama本地模型或DeepSeek等API服务。落地场景通常包括将智能体接入微信、飞书等IM平台,实现定时任务、信息检索等自动化操作。然而,实际部署中常会遇到运行时找不到、模型未授权、回调地址校验失败等高频故障,需要系统化的排查思路。本文以OpenClaw为例,完整梳理从云主机准备、跨平台部署到模型与渠道对接的全流程,帮助开发者快速搭建稳定可用的个人智能体。
RCE-labs靶场实战:命令注入与代码执行绕过全解析
远程代码执行(RCE)是Web安全领域最具破坏力的漏洞类型之一,攻击者通过注入恶意代码即可直接控制服务器。理解RCE的触发原理与绕过手法,是安全测试与代码审计的必备技能。命令注入作为RCE的常见入口,常因过滤不严而被利用;而代码执行则涉及eval、assert等危险函数。在实际攻防场景中,面对空格、关键字、函数名过滤以及无回显环境,安全人员需要掌握符号拼接、编码绕过、变量函数、时间盲打和外带数据等多种技巧。RCE-labs作为一套专注于远程代码执行训练的靶场,通过由浅入深的关卡设计,系统覆盖了命令注入、代码执行、变量覆盖、弱类型比较及open_basedir绕过等核心考点。本文基于通关实战,梳理了从环境部署到高级绕过的完整思路,帮助安全学习者构建RCE知识体系,提升实战能力。
视频号12月带货榜深度拆解:加权逻辑、爆款策略与2025趋势信号
在直播电商的数据生态中,第三方带货榜单的排名往往融合了多维度的加权逻辑,而非简单的成交总额排序。理解预估销售额与实际成交的差异、统计口径的变化,是读懂榜单价值的前提。这套数据评估机制不仅服务于达人复盘,更成为商家筛选合作对象、判断品类冷热、识别刷单信号的重要工具。从12月视频号带货榜来看,头部达人普遍依赖短视频引流与私域联动,商品组合遵循引流款、利润款、形象款的搭配逻辑,食品生鲜、服饰鞋包等品类因季节与送礼场景集中爆发。与此同时,平台规则收紧小店评分和内容质量门槛,倒逼从业者从粗放低价转向内容信任驱动。榜单背后折射出的趋势,为2025年知识付费、中腰部达人合作以及本地生活入局提供了清晰的参考方向。
华为交换机路由器防火墙缺省账号密码与忘记密码恢复指南
在网络设备运维中,缺省密码是登录管理的第一道门槛。华为企业级交换机、路由器和防火墙随VRP版本演进,默认账号密码从早期的admin/admin逐渐收紧为Admin@huawei等复杂组合,部分老设备Console口甚至空密码直进。理解不同版本与交付形态下的密码策略差异,是高效排查登录故障的基础。当密码遗忘导致无法进入设备时,通过Console线连接并进入BootROM菜单清除密码,是保留配置的常用恢复手段,但需警惕恢复出厂设置等高危选项。日常运维中,提前备份配置、规范Console口与远程管理密码、建立交接文档,比事后应急更为重要。本文从基础概念出发,梳理华为设备缺省凭据速查表,并详解密码恢复与安全加固的实操路径,适合网工与运维人员参考。
链表算法题核心技巧:反转、快慢指针与虚拟头节点实战解析
在数据结构与算法学习中,链表因其非连续的内存布局和指针操作特性,成为面试与工程实践的常客。理解链表节点的指针指向、边界条件处理以及虚拟头节点的设计思路,是解决各类链表题目的基础。从最常见的单链表逆序,到利用快慢指针检测环形链表、寻找相交节点,再到合并有序链表与归并排序,这些经典问题都围绕指针操作和节点连接展开。掌握迭代与递归两种反转写法,熟悉快慢指针的数学原理,学会用哨兵节点简化头节点操作,能够显著提升编码正确率。实际应用中,链表思想广泛用于内存池、LRU缓存和任务队列等场景。本文系统梳理链表题型的核心框架与调试方法,帮助读者建立从基础概念到综合应用的完整知识体系,轻松应对笔试面试中的高频考点。
技术进阶的尽头是底层原理:从HashMap到MySQL的实战剖析
在技术迭代加速的今天,表面技巧快速过时,底层原理却始终稳固。以HashMap为例,理解哈希冲突解决、负载因子设计与扰动函数,不仅能避免扩容引发的性能尖刺,更能指导并发容器选型。同理,MySQL的B+树与Buffer Pool机制决定了索引与冷热分离策略的设计边界,而队列削峰则依托生产者-消费者模型。掌握这些底层机制,你就能在架构选型、性能排查中拥有推导能力。本文结合HashMap、MySQL冷热分离、OpenFeign调用链等实战场景,展示原理思维落地为进阶套路的完整路径。
多主体综合能源系统主从博弈优化调度:从建模到求解
在综合能源系统优化调度中,集中式模型常因忽略各主体利益诉求而难以落地。主从博弈(Stackelberg game)通过上层定价与下层需求响应的层级决策,还原了运营商与用户间的真实博弈关系。需求响应机制让用户根据电价调整负荷,电能交互则实现多主体间的功率互济,二者共同构成博弈框架的双主线。为便于求解,可利用KKT条件将下层优化问题等价转化为约束,嵌入上层模型形成单层混合整数线性规划(MILP),并通过Yalmip调用Cplex高效求解。该技术路线适用于园区级电热联供、微电网群协调、虚拟电厂定价等场景,兼顾各方利益与全局效率,是解决多主体协调优化问题的实用方案。
已经到底了哦