news 2026/10/23 6:34:32

Spring Boot购物网站源码实战:下单事务、库存扣减与防超卖

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring Boot购物网站源码实战:下单事务、库存扣减与防超卖

简介:这套购物网站项目源码与数据库面向JavaWeb初学者、课程设计及毕业设计参考者,提供一套功能较完整的类淘宝电商实现,适合用作电商项目入门拆解样本,可帮助读者理解从商品浏览、搜索、购物车到下单、用户管理等业务流程如何落地。压缩包共269个文件,约1.71MB,其中35个jsp页面、30个java源文件与30个class文件构成前端展示和后端逻辑主体,另有2个sql脚本及mdf、ldf数据库文件,配合88张jpg、53张gif和19张png图片及样式脚本,便于还原页面与搭建运行环境。已有631人学习下载。项目采用Servlet、JSP、JavaBean与MVC分层组织,数据库涵盖商品、用户、订单等核心实体,涉及用户登录注册、商品分类展示、购物车与订单处理等环节,适合对照源码梳理请求处理、页面跳转、数据访问和表结构设计,也可作为改造与调试的实战样例。

1. 从一份带 SQL 的购物网站源码说起:它能跑出什么

前阵子有人丢给我一个压缩包,文件名朴素得像是随手存的——购物网站 + 源代码 + 数据库。解压之后能完整跑出一条电商主流程:注册登录、商品列表与搜索、商品详情、加入购物车、填收货地址、下单、模拟支付、后台增删改商品和查订单。对想搞明白电商主干链路的开发来说,这种「三件套齐全」的包比零散的教学 demo 实在得多:SQL 文件在,表结构不用靠猜;后端分层在,接口怎么串一层层看得见;前端页面也在,字段怎么对上能直接对着调。

它适合两类人。一类是刚学完框架、想做个像样的东西放进作品集的,照着跑通再改两处业务,比 CRUD 练习有说服力;另一类是接了个小商城私活、想找套能改的底座先把架子支起来的,重点不在从零写,而在看懂别人的表怎么设计、事务边界划在哪儿。这篇笔记就按「它是什么 → 怎么跑起来 → 核心代码长什么样 → 哪儿容易翻车 → 怎么改成自己的」往下走。

2. 技术栈与目录结构:五个包、三张核心表先把家底摸清

2.1 技术选型拆解:一套典型的 Spring Boot + MyBatis + MySQL 组合

这类带完整 SQL 的购物网站源码,绝大多数走的是「后端 Spring Boot + MyBatis(或 MyBatis-Plus)+ MySQL,前端 Vue + Element UI 前后端分离」这条路。也有相当一批是 Servlet/JSP 或者 Thymeleaf 服务端渲染的老式写法。我下面按 Spring Boot 这条主干讲,如果你手上的包是 JSP 版本,分层思路完全对得上,只是注解要换成web.xml里的 Servlet 映射,理解成本不高。

选这套组合做电商入门是有道理的。商品、购物车、订单这类数据关系明确,天生适合关系型数据库,表结构能把一对多、多对多的关系讲透;MyBatis 把 SQL 写在 XML 里,对新手反而是好事——不用猜 ORM 生成了什么语句,慢查询、字段漏查都能直接看见;Spring 的声明式事务又刚好承接「下单要保证订单、明细、库存三处一致」这个最典型的场景。真正上生产当然还会加 Redis 缓存商品、消息队列削峰、分库分表,但那是后话,先把单体跑明白。

层次常见技术在这个项目里负责什么
视图层Vue 2 / Element UI,或 Thymeleaf、JSP商品列表、详情、购物车、订单页渲染
控制层Spring MVC@RestController接收前端参数、做参数校验、组装返回体
业务层@Service+@Transactional下单、库存扣减、订单状态流转
持久层MyBatis Mapper 接口 + XML手写 SQL,分页、条件查询、批量更新
存储MySQL 5.7 / 8.0用户、商品、SKU、购物车、订单、地址

需要提醒一句:技术栈是手段不是目的。看这类项目最忌讳上来就纠结「为什么不用 Spring Cloud」,一套单体能跑通下单全链路,比一堆注册中心更有学习价值。

2.2 目录结构对照:先认清五个包

拿到压缩包别急着双击启动类,先花十分钟把目录过一遍,后面改代码能省一倍时间。典型的后端结构长这样,前后端分离的包里前端通常是同级的vue-admin或shop-web目录:

路径作用改代码时的关注度
src/main/java/.../controller所有 HTTP 入口,一个类对应一组接口高,接口签名基本都在这改
src/main/java/.../service+impl业务逻辑和事务边界最高,下单、扣库存都在这里
src/main/java/.../mapperMyBatis 接口,方法名对应 XML 的 id中,加查询的时候要动
src/main/resources/mapper/*.xml真正的 SQL 语句高,慢查询和字段问题都在这
src/main/resources/application.yml数据库、端口、上传大小等配置高,跑起来第一关
sql/shop.sql建表 + 初始数据高,先导它
static/或upload/商品图片、静态资源中,图片 404 全在这

有个血泪经验:不少包里的mapper目录和resources/mapper是两个不同的地方,一个是 Java 接口一个是 XML,名字一样但职责不同。mapper-locations配错路径,启动不报错,一调接口就抛Invalid bound statement,新手最容易在这儿卡半天。

2.3 数据库设计:从 SQL 文件反推业务模型

真正的业务逻辑藏在表结构里。与其一行行读 Service,不如先把 SQL 文件里的建表语句过一遍,商品和 SKU 怎么拆、订单和明细怎么关联、金额用什么类型,看完心里就有谱了。核心三张表的简化形态大致如下:

-- 商品表:只放公共信息,价格和库存不在这里 CREATE TABLE t_product ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(128) NOT NULL COMMENT '商品名', category_id BIGINT NOT NULL COMMENT '分类,对应 t_category', main_image VARCHAR(255) DEFAULT NULL COMMENT '主图相对路径', detail TEXT COMMENT '富文本详情', status TINYINT DEFAULT 1 COMMENT '1上架 0下架', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_category (category_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品'; -- SKU 表:真正被下单的是它,价格和库存挂在这一层 CREATE TABLE t_sku ( id BIGINT PRIMARY KEY AUTO_INCREMENT, product_id BIGINT NOT NULL, spec VARCHAR(64) COMMENT '规格,如 黑色/256G', price DECIMAL(10,2) NOT NULL COMMENT '单价,必须用 DECIMAL', stock INT NOT NULL DEFAULT 0 COMMENT '可售库存', version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号,没有它并发下单会超卖', KEY idx_product (product_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品规格'; -- 订单明细:下单瞬间要把商品名、单价、图"拍照"存下来 CREATE TABLE t_order_item ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_id BIGINT NOT NULL, sku_id BIGINT NOT NULL, product_name VARCHAR(128) NOT NULL COMMENT '快照,商品改名不影响历史订单', price DECIMAL(10,2) NOT NULL COMMENT '快照价', num INT NOT NULL, KEY idx_order (order_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单明细';

三处细节值得琢磨。第一,价格和库存放在 SKU 层而不是商品层,因为同一件衣服的不同尺码价格和库存都可能不同,拆开之后加购才能真正落到「买哪一件」上。第二,订单明细里冗余存了product_name和price,这不是设计失误而是刻意为之——商品改价、改名、下架之后,历史订单必须还能原样展示,这类快照字段在电商里叫「留痕」,少了它以后对账要哭。第三,金额一律DECIMAL(10,2),用FLOAT存钱的包我见过,结算时 0.1 + 0.2 算出 0.30000000000000004,对不上账纯属自找。

提示:导库前先确认 SQL 文件里的字符集是不是utf8mb4。是utf8的话,商品名带个表情符号或者生僻字就会插入失败,报错信息还特别含糊。

3. 本地跑通:从建库导 SQL 到后台登录成功的完整命令

3.1 环境准备:JDK、MySQL、Node 三件套的版本对齐

跑不起来十次里有六次是版本问题。后端是 Spring Boot 2.x 的话,JDK 用 8 或 11 最稳,直接上 17 有概率撞上反射相关的启动异常;MySQL 5.7 和 8.0 都能用,但 8.0 的驱动类名是com.mysql.cj.jdbc.Driver,URL 里还得带时区参数,用老的com.mysql.jdbc.Driver会收到一行黄色警告,虽然不致命但看着膈应。前端如果是 Vue 2,Node 版本别超过 16,上了 18 之后node-sass编译经常直接崩,这个坑我踩过不止一次,报错信息长到能刷满一屏,本质就是二进制包和 Node ABI 版本对不上。

依赖管理看有没有pom.xml。Maven 项目第一次mvn clean package会拉一堆包,国内网络环境下建议先把镜像配好;如果包里自带lib目录和.iml文件,那可能是 IDE 直接导出的工程,用 IDEA 打开按提示配 SDK 就行,别硬套 Maven。

3.2 导入 SQL:建库、字符集、初始数据

先看sql目录下有几个文件。有的包会拆成schema.sql(建表)和data.sql(初始数据)两个,有的合成一个,还有的会额外给一份init.sql建库语句。顺序千万别搞反:先建库,再导表,最后灌数据。

# 1. 建库,字符集锁定 utf8mb4,别用默认的 latin1 mysql -uroot -p -e "CREATE DATABASE shop DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;" # 2. 导 SQL,--default-character-set 不加的话中文大概率变问号 mysql -uroot -p --default-character-set=utf8mb4 shop < sql/shop.sql # 3. 验一下:表在不在、数据有没有进来 mysql -uroot -p -e "use shop; show tables; select count(*) from t_product; select count(*) from t_sku;"

如果第三步查出来的商品数是 0,说明 SQL 里只有建表语句没有INSERT,得自己往后台加几条数据才能看到列表页。登录账号通常在t_user表里,密码字段如果是 BCrypt 加密的,明文可能是123456或者admin,具体看包里的说明文档或者 README;如果实在找不到,就本地生成一个 BCrypt 串直接UPDATE覆盖掉,比暴力猜快得多——这是最省事的后悔药。

3.3 改配置与启动验证:三个必改项

配置文件是跑通的最后一道关。Spring Boot 项目找application.yml或application.properties,改完再启动:

server: port: 8080 # 前端 proxy 里的 target 必须和这里一致 spring: datasource: url: jdbc:mysql://127.0.0.1:3306/shop?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false username: root password: 换成你自己的本地密码 driver-class-name: com.mysql.cj.jdbc.Driver servlet: multipart: max-file-size: 5MB # 后台上传商品图,默认 1MB 经常不够用 max-request-size: 10MB mybatis: mapper-locations: classpath:mapper/*.xml # 少这行,XML 里的 SQL 全部找不到 configuration: map-underscore-to-camel-case: true # 数据库 user_name 自动映射成 userName

三个必改项:数据库密码、端口、mapper-locations。尤其是最后一个,很多包里的 XML 放在resources/mapper下但配置写的是classpath*:com/xxx/mapper/*.xml,路径对不上,启动毫无异常,一访问接口就Invalid bound statement (not found)。改完启动,控制台看到 Tomcat 起在 8080 就算过了第一关。

前端那边进目录先npm install,然后看vue.config.js里的devServer.proxy:

devServer: { port: 8081, proxy: { '/api': { target: 'http://127.0.0.1:8080', // 后端地址,端口改了这里也得改 changeOrigin: true, pathRewrite: { '^/api': '' } // 后端接口没有 /api 前缀时必须重写 } } }

pathRewrite这一行是跨域问题的分水岭。后端 Controller 上写的是@RequestMapping("/product"),前端请求/api/product/list,不重写就会打到后端的/api/product/list上,稳稳一个 404。

3.4 冒烟测试:按顺序点五步

别一上来就点下单,按依赖顺序走一遍最快定位问题:注册一个新账号 → 登录看有没有拿到 token → 商品列表能不能出图 → 商品详情加购物车 → 结算填地址提交订单。这五步任何一步断掉,问题范围立刻缩小到对应模块。

后台管理端一般挂在/admin路径或者单独一个端口,登录后能看到商品管理、订单管理、用户管理几个菜单。如果后台进不去但前台正常,八成是角色字段(role或is_admin)没配对,直接改数据库把当前用户的角色改成管理员最直接。

4. 核心链路拆解:加购、下单、鉴权三段代码的写法与坑

4.1 购物车:加购接口怎么处理重复商品

购物车看着简单,实际是整个项目里最容易被写歪的模块。最常见的错误是「每次加购都插一条新记录」,结果用户连点三次同一件商品,购物车里躺着三行一模一样的条目。合格的写法是先查后判:

@Override public void addToCart(Long userId, Long skuId, Integer num) { if (num == null || num <= 0 || num > 99) { throw new BizException("购买数量不合法"); } // 1. 查这个用户车里有没有同一个 SKU CartItem exist = cartMapper.selectOne(new LambdaQueryWrapper<CartItem>() .eq(CartItem::getUserId, userId) .eq(CartItem::getSkuId, skuId)); if (exist != null) { // 2. 有就累加数量,不新增行 exist.setNum(exist.getNum() + num); cartMapper.updateById(exist); return; } // 3. 没有才插入,价格只做展示快照,结算时必须重新查 Sku sku = skuMapper.selectById(skuId); if (sku == null || sku.getStock() <= 0) { throw new BizException("商品已下架"); } CartItem item = new CartItem(); item.setUserId(userId); item.setSkuId(skuId); item.setNum(num); item.setPriceSnapshot(sku.getPrice()); item.setCreateTime(LocalDateTime.now()); cartMapper.insert(item); }

userId绝对不能从请求参数里取,必须从登录态(token 或 session)里解出来,否则改个 URL 就能往别人车里塞东西。priceSnapshot只是给购物车页面展示用的,真正结算时要以数据库当前价重算,不然用户半年前加购、商品涨过价,你还按旧价卖,亏的是自己。另外selectOne+insert这个组合在并发下依然可能插重,稳妥做法是在(user_id, sku_id)上建唯一索引,让数据库兜底。

4.2 下单:一个事务里做完价格重算、库存扣减、订单落库

下单是全项目最该盯紧的一段。它要保证四件事同时成立:订单主表落库、订单明细落库、库存扣减、购物车清空——任何一步失败都必须整体回滚,不然就是「钱扣了没订单」或者「订单生了库存没减」。事务注解只是起点,真正防超卖的是 SQL 里的条件更新:

@Service public class OrderServiceImpl implements OrderService { @Transactional(rollbackFor = Exception.class) // 别用默认的 RuntimeException @Override public Long createOrder(Long userId, List<Long> cartIds, Long addressId) { // 1. 购物车项必须属于当前用户,防止越权结算别人的车 List<CartItem> items = cartMapper.selectList(new LambdaQueryWrapper<CartItem>() .in(CartItem::getId, cartIds) .eq(CartItem::getUserId, userId)); if (items.isEmpty()) { throw new BizException("购物车为空"); } BigDecimal total = BigDecimal.ZERO; List<OrderItem> details = new ArrayList<>(); for (CartItem item : items) { // 2. 价格一律以数据库当前价为准,前端传的金额一概不信 Sku sku = skuMapper.selectById(item.getSkuId()); if (sku == null) { throw new BizException("商品不存在"); } // 3. 条件更新扣库存:stock >= num 写进 where,扣不动就是库存不足 int affected = skuMapper.deductStock(sku.getId(), item.getNum()); if (affected == 0) { throw new BizException("库存不足:" + sku.getSpec()); } total = total.add(sku.getPrice().multiply(BigDecimal.valueOf(item.getNum()))); OrderItem d = new OrderItem(); d.setSkuId(sku.getId()); d.setProductName(sku.getProductName()); // 快照 d.setPrice(sku.getPrice()); // 快照 d.setNum(item.getNum()); details.add(d); } // 4. 落订单主表,状态 0 = 待支付 Order order = new Order(); order.setOrderNo(generateOrderNo(userId)); order.setUserId(userId); order.setAddressId(addressId); order.setTotalAmount(total); order.setStatus(0); orderMapper.insert(order); // 5. 明细补 orderId 后批量插入,再清购物车 details.forEach(d -> d.setOrderId(order.getId())); orderItemMapper.insertBatch(details); cartMapper.deleteBatchIds(cartIds); return order.getId(); } }

配套的 Mapper XML 才是防超卖的核心:

<!-- 扣库存:把"够不够"这个判断交给数据库,天然带行锁 --> <update id="deductStock"> UPDATE t_sku SET stock = stock - #{num} WHERE id = #{skuId} AND stock >= #{num} </update>

先select查库存、再判断、再update,这套写法在并发下必超卖——两个请求同时查到库存 1,都判断通过,最后扣成 -1。改成WHERE stock >= #{num}之后,第二条 update 影响行数为 0,业务层抛异常回滚,问题从根上解决。rollbackFor = Exception.class也不能省,Spring 默认只对RuntimeException回滚,抛受检异常时事务悄悄提交,这类问题排查起来像黑匣子,日志里一点痕迹都没有。

4.3 鉴权:拦截器 + token 的两层校验

前后端分离的项目基本都用 token 而不是 session。核心是一个HandlerInterceptor,在进 Controller 之前把用户身份解出来塞进上下文:

public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) { // 1. 放行非 Controller 请求(静态资源、错误页) if (!(handler instanceof HandlerMethod)) { return true; } // 2. 白名单:登录、注册、商品浏览接口不需要 token HandlerMethod hm = (HandlerMethod) handler; if (hm.hasMethodAnnotation(NoAuth.class)) { return true; } String token = req.getHeader("Authorization"); if (token == null || token.isEmpty()) { resp.setStatus(401); return false; } Long userId = jwtUtil.parseUserId(token.replace("Bearer ", "")); if (userId == null) { resp.setStatus(401); return false; } // 3. 塞进 ThreadLocal,Service 层随时可取,不用层层传参 UserContext.set(userId); return true; } @Override public void afterCompletion(HttpServletRequest req, HttpServletResponse resp, Object handler, Exception ex) { UserContext.clear(); // 线程池复用,不清会串号 } }

两点说明。afterCompletion里的clear()千万别漏——Tomcat 线程是复用的,不清 ThreadLocal,下一个请求可能拿到上一个用户的 id,这种 bug 排查难度极高,表现是「偶尔能看到别人的订单」。第二,商品列表、详情这类浏览接口应该标@NoAuth放行,让未登录用户也能逛,只有加购、下单、订单列表才强制校验,否则用户一进首页就被弹回登录页,体验极差。

5. 常见问题排查:这套购物网站源码最容易翻车的五处

5.1 启动与环境类:连不上库、端口被占、静态资源 404

现象一:启动即报Communications link failure。原因通常有三种:MySQL 服务没起来、URL 里的端口不是 3306、8.0 版本没配serverTimezone。解决顺序是先mysql -uroot -p手动连一下确认服务活着,再核对application.yml里的端口和时区参数,8.0 一律加&serverTimezone=Asia/Shanghai&useSSL=false。

现象二:Port 8080 was already in use。上一个进程没退干净,或者本机跑着别的服务。Windows 用netstat -ano | findstr 8080找到 PID 再taskkill /F /PID,Mac/Linux 用lsof -i:8080后kill -9。图省事也可以直接改server.port,但记得同步改前端proxy.target,只改一头等于没改。

现象三:页面能打开但样式全丢、图片全是裂的。原因多半是静态资源路径写成了绝对路径/upload/xxx.jpg,而项目实际部署在/shop这样的子路径下。解决要么在配置里加server.servlet.context-path,要么把前端引用统一改成相对路径。这个坑在本地跑通常不出现,一部署到测试环境就集体裂图。

5.2 数据与事务类:库存扣成负数、订单金额对不上

现象四:压测时库存出现 -3。原因就是 4.2 节说的「先查后改」,select和update之间存在并发窗口。解决办法有两个方向:轻量的用WHERE stock >= #{num}条件更新,影响行数为 0 就抛异常;要更严谨就加version字段做乐观锁,UPDATE ... SET stock = stock - #{num}, version = version + 1 WHERE id = ? AND version = ?,失败重试两三次。别上synchronized,单机有效,多实例部署立刻失效。

现象五:订单总额和明细加起来对不上,差几分钱。八成是某处用了double或者float做金额运算。把所有金额字段的类型统一成BigDecimal,数据库对应DECIMAL(10,2),乘法用multiply、加法用add,并且在最后一步统一setScale(2, RoundingMode.HALF_UP)收敛小数位。已经存进库的历史脏数据,写个脚本按明细重算一遍总额刷回去。

5.3 前后端联调类:跨域、字段对不上、上传失败

现象六:浏览器控制台报 CORS 错误,Postman 里却正常。这是跨域预检没过,不是接口坏了。开发环境最省事的做法是走devServer.proxy代理,让请求同源;如果非要后端放行,就在WebMvcConfigurer里加addCorsMappings,allowedOriginPatterns别图省事写*又同时开allowCredentials(true),这两个组合在浏览器里会被直接拒绝。

现象七:后端返回的字段是userName,前端拿到的却是undefined。命名风格不一致,数据库user_name、Java 属性userName、前端写username,差一个大小写就拿不到值。统一约定:后端开map-underscore-to-camel-case,前端接口层做一次字段映射,别在每个组件里各写各的。

现象八:后台上传商品图报Maximum upload size exceeded。Spring Boot 默认单文件 1MB,现在的手机拍出来随便都三四兆。改spring.servlet.multipart.max-file-size和max-request-size即可,Nginx 前面还有一层的话,client_max_body_size也得跟着调,只改一头照样 413。

6. 二次开发:把教学级购物网站改成能接真实订单的底座

跑通只是开始,真正值钱的是知道往哪儿改。我按投入产出比排了个序,前两项做完这套源码的「可用度」能上一个台阶:

优先级改造点具体做法收益
P0库存并发条件更新 +version乐观锁 + 失败重试消除超卖,业务可用
P0登录态token 加过期时间、加刷新机制、密码 BCrypt安全底线
P1订单状态机待支付/已支付/已发货/已完成/已取消,用状态枚举约束流转避免状态乱跳
P1支付回调幂等按order_no加唯一约束,回调重复直接返回成功防重复发货
P2商品缓存详情页加 Redis,更新时删缓存首页响应从百毫秒降到十毫秒级

订单状态机是最容易被忽略的一块。教学项目里状态就是0/1/2三个裸数字,随手setStatus(2)满天飞,结果出现「未支付却已发货」这种脏数据。我会把流转规则收进枚举:

public enum OrderStatus { UNPAID(0, "待支付"), PAID(1, "已支付"), SHIPPED(2, "已发货"), FINISHED(3, "已完成"), CANCELED(4, "已取消"); private final int code; private final String desc; OrderStatus(int code, String desc) { this.code = code; this.desc = desc; } // 只允许这几条合法路径,其余一律拒绝 public boolean canTransitTo(OrderStatus target) { switch (this) { case UNPAID: return target == PAID || target == CANCELED; case PAID: return target == SHIPPED; case SHIPPED: return target == FINISHED; default: return false; // 终态不可再变 } } }

Service 里所有改状态的地方统一走if (!current.canTransitTo(target)) throw new BizException("非法状态流转");,一行判断挡掉大量脏数据。支付回调再加一层幂等:回调进来先按order_no查订单,已经是PAID就直接返回成功,别重复扣库存、别重复发货——真实支付平台的回调是会重发的,没做幂等的项目上线第一周必翻车。

验证方法也要跟上。改完库存逻辑别只看页面点着正常,写个简单的并发测试,用 50 个线程同时下单同一件库存 10 的商品,跑十轮,看成功订单数是不是稳定在 10、库存是不是稳定在 0。这个测试跑通,库存这块基本可以放心。改完鉴权则用两种角色交替登录,确认普通用户访问不到/admin/**。

从那以后我每次拿到一份带 SQL 的源码包,第一件事都是先把表结构导出来画一遍关系图,再动代码——因为业务逻辑写在 Service 里,但业务规则藏在表结构里,表看不懂,改哪儿都是瞎猜。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/23 6:34:27

圆瓶贴附路径离散:弦高、首段方向与分段数校核

壹星&#xff08;YIXING&#xff09;技术笔记&#xff5c;东莞市壹星自动化设备有限公司把圆瓶贴附路径画成若干直线段时&#xff0c;第一条弦并不与起点切线重合。段数太少会同时产生径向轮廓误差和首段方向误差&#xff0c;但这属于离散表示误差&#xff0c;不能直接解释真实…

作者头像 李华
网站建设 2026/10/23 6:30:45

在现代前端开发中,UI 组件库的选择直接影响项目的开发效率、维护成本和用户体验。Angular Material 作为 Google 官方团队开发和维护的 UI 组件库

在现代前端开发中&#xff0c;UI 组件库的选择直接影响项目的开发效率、维护成本和用户体验。Angular Material 作为 Google 官方团队开发和维护的 UI 组件库&#xff0c;实现了 Google 的 Material Design 设计规范&#xff0c;为 Angular 开发者提供了一套高质量、可访问、易…

作者头像 李华
网站建设 2026/10/23 6:30:22

如何打通虚拟机和主机网络?实现互ping

第一步&#xff1a;虚拟网络编辑器&#xff0c;还原默认设置&#xff0c;这里什么都不用改&#xff0c;还原默认设置即可特殊说明&#xff1a;桥接是电脑网口用的&#xff0c;比如本节是电脑打通虚拟机&#xff0c;不存在外部设备&#xff0c;用不上第二步&#xff1a;虚拟机增…

作者头像 李华
网站建设 2026/10/23 6:28:42

Python 练习题:用函数绘制字符田字格

前言在学习 Python 编程的过程中&#xff0c;遇到了一道非常经典的函数封装与循环控制练习题。题目要求如下&#xff1a;题目 字符田字格绘制 编写程序&#xff0c;用字符方式打印输出一个简单的田字格。要求采用函数方式&#xff0c;以田字格宽度为参数&#xff0c;能够根据参…

作者头像 李华
网站建设 2026/10/23 6:27:55

让你的EPUB电子书自带查拼音功能

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

作者头像 李华