96SEO 2026-08-02 19:41 0
GitHub仓库github.com//…说起来,
|前端人第一次打开 Spring Boot 项目。应该先看哪里,

|从 pnpm dev 到 Spring Boot 启动:后端服务到底怎么跑起来?
|Axios 请求进了后端之后:Controller、Request、Response 是怎么接住它的?
|前端状态为什么不够用?从页面数据到 MySQL 持久化
|不手写一堆 SQL,后端怎么操作数据库?MyBatis-Plus 入门
|登录后端到底在做什么?JWT、Spring Security 和权限链路
|为什么后端也要缓存?从前端缓存思维理解 Redis
|一个商品详情接口背后的完整链路:HTTP、Redis、MySQL 与 JSON
|商品为什么不能随便上下架?后端状态机思维入门
|库存扣减为什么最容易出事故?SKU、并发与原子更新
使用者痛点:
userId 被信任导致越权。skuId 而不是只保存 productId。
UserId 必须来自登录态,不能由前端提交;SkuId,而不是只保存 ProductId;CARTAddRequest 只有 SkuId 和 TotalQty ;User Pain Point: “我在页面里维护一个数组就行了啊”。这个数组只能在当前页面/浏览器有效,一旦换设备或刷新,就会丢失。下面先展示典型的 “仅靠数组” 实现,接下来对比后面的持久化方案。
// 前端侧示意代码
interface CartItemView {
skuId的观点是,number;productTitle: string;specText: string;currentUnitPrice: string;不过,quantity: number;checked: boolean;availableStock: number;saleable: boolean;}
const cartItems = ref;
// 本地去重示例
function addLocalCartItem {
const exists = cartItems.value.some;if {
toast,return;}
cartItems.value.push;}
User Pain Point: “我把 userId 放进请求体里就行了”。再看实际情况是,The backend never trusts a user‑id that comes from client.
The API path is simply /api/cart/items. No {userId}. The server extracts current user from JWT token:
sequenceDiagram
participant UI as 前端页面
participant API as CartController
participant Auth as CurrentUserService
participant DB as mall_cart_item
UI->API: GET /api/cart/items
Authorization: Bearer token
API->Auth: requireCurrentUserId
Auth-->API: userId=当前登录使用者
API->DB: SELECT * FROM mall_cart_item WHERE user_id=?怎么说呢,DB-->API: 当前使用者购物车行
API-->UI: CartResponse
This design prevents a malicious client from changing path to anor user's ID. It mirrors front‑end routing like “/profile” – you never need to pass user id because it’s already known from login state.
a product_id were stored,back‑end could not know which specification user actually wants.| 文件方法 & 名称 | 职责说明 | 关联痛点 | |
|---|---|---|---|
/backend/service/src/main/java/com/example/fullstackmall/service/cart/CartController.java | - HTTP入口 - 提供「加入」和「查询」两个方法 | - 痛点1:为何没有userId参数 | |
/backend/contract/src/main/java/com/example/fullstackmall/contract/cart/ICartFacade.java | - 用例契约 - 明确业务入口必须使用登录态 | - 痛点2:userId来源 | |
/backend/service/src/main/java/com/example/fullstackmall/service/cart/CartFacade.java | - 主要业务编排 - 校验 SKU、商品状态 - 检查重复并写入 DB | - 痛点4/5/6 | |
/backend/contract/src/main/java/com/example/fullstackmall/contract/cart/CartAddRequest.java | - DTO。仅包含 skuId 与 quantity | - 痛点3:为何不让客户端传price/userI d | |
/backend/contract/src/main/java/com/example/fullstackmall/contract/cart/CartItemResponse.java | - 响应结构,包括实时 price 、stock 、saleable 标记 | - 痛点7:显示不可售行 | |
/backend/service/src/main/java/com/example/fullstackmall/service/cart/entity/CartItemEntity.java | - 对应表 | - 痛点8:为何只存 sku_id 和 qty | |
/backend/service/src/main/java/com/example/fullstackmall/service/cart/service/CartItemDbService.java | |||
sequenceDiagram participant UI as 商品详情页 participant Sec as Spring Security participant C as CartController participant F as CartFacade participant User as CurrentUserService participant Sku as SkuDbService participant Prod as ProductDbService participant Cart as CartItemDbService UI->Sec: POST /api/cart/items {skuId,quantity} Sec->Sec: 校验 JWT 登录态 从Sec->C来看,addItem C->C的观点是,@Valid 参数校验 C->F的观点是,addItem F->User: requireCurrentUserId 至于F->Sku,requireById F->Prod: requireById 说到F->F,校验商品上架 + price + stock 可售性 F->Cart: existsByUserIdAndSkuId F->DB的观点是,INSERT mall_cart_item F-->C的观点是,CartItemResponse C--&g t;返回 Created + ApiResponse 查询我的购物车链路
mermaid sequenceDiagram participant UI as 前段页 participant C as CartController participant F as CartFacade participant U s er as CurrentUserService participant CI t emDbService participant S ku as SkuDbService participant P roductDbService UI -> C : GET /api/cart/items C -> F : queryMyCart F -> U : requireCurrentUserI d F -> CI t emDbS ervice : listByUserI d F -> S ku : listExistingByIds F -> P roductD bServi ce : listExistingByIds F -> F : assemble item + saleable + lineAmount F --> C : CartResponse C --> UI : ApiResponse. 源码逐段阅读与痛点对应解释
Controller — 方法即资源。不接受 user_id 参数
@RestController @RequestMapping @Tag public class CartController { @PostMapping @Operation public ResponseEntityaddItem( @Valid @RequestBody CartAddRequest request,HttpServletRequest servletRequest) { CartItemResponse response = cartFacade.addItem;return ResponseEntity.status .body));} @GetMapping @Operation public ApiResponse . queryMyCart { return ApiResponse.success( cartFacade.queryMyCa rt,TraceI dContext.get); } }
- 两条接口共用同一方法,仅通过 HTTP 方法区分新增 vs 查询。
- **Pain point** — 如果你习惯在请求体里传 `userID`。这里根本没有对应字段,说明设计者已把安全边界提前锁定。
- **Pain point** — 忘记加 `@Valid` 会导致请求体错误未被捕获,这正是很多新人忽视的数据校验层。
Request — 为什么只有 sku_id 与 quantity?
java public class CartAddRequest { @NotNull private Long skuId;
@NotNull @Min @Max private Integer quantity;}
- 没有 `userID` —— **pain point** — 前段再传也会被抹掉。
- 没有 `price` —— **pain point** — 前段若尝试改价根本无效,因为价格来源于 SKU 的实时数据。说起来,
- 没有 `checked` —— **pain point** — 新增默认勾选。让 UI 能立即看到已选效果,而不需要额外字段。
至于前段示例代码,
ts interface CartAddPayload { skuId:number;quantity:number } async function addToCart{ return request.post;}Entity – 极简表结构防止冗余不一致
java @Data @TableName public class CartItemEntity { @TableI d private Long id;
@TableField private Long userI d;@TableField private Long skuI d;private Integer quantity;private Integer checked;@TableField private LocalDateTime createdAt;老实说,@TableField private LocalDateTime updatedAt;}
- 只保存最小事实—— **pain point** — 开发者常想把标题、价格等直接写进去,却会导致数据不同步。不过,
- `checked` 默认值为已勾选—— **pain point** — 如果忘记设置默认值。会出现未勾选却计入合计的问题。
SQL – 唯一索引保障并发安全
sql CREATE TABLE IF NOT EXISTS mall_cart_item( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '购物车行 ID',user_id BIGINT UNSIGNED NOT NULL COMMENT '所属使用者 ID',sku_id BIGINT UNSIGNED NOT NULL COMMENT 'SKU ID',quantity INT UNSIGNED NOT NULL COMMENT '购买数量'。checked TINYINT UNSIGNED NOT NULL DEFAULT 1 COMMENT '是否勾选',created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,PRIMARY KEY,UNIQUE KEY uk_mall_cart_item_user_sku,KEY idx_mall_cart_item_user_updated_at,CONSTRAINT chk_mall_cart_item_quantity CHECK,CONSTRAINT chk_mall_cart_item_checked CHECK ) );
- `uk_mall_cart_item_user_sku` 防止同一使用者重复添加相同 SKU—— **pain point** — 即使业务层已检查,仍需靠 DB 索引防止高并发冲突。
- `idx_..._updated_at` 调整「我的购物车」查询—— **pain point** — 忘记加索引会导致分页慢甚至超时。
Facade – 加入逻辑完整实现
java @Override public CartItemResponse addItem{
Long userId = currentUserService.requireCurrentUserI d;
SkuEntity sku = skuDbService.requireBy Id);
ProductEntity product= productDbServi ce.requireBy Id);
requireSaleableProductAndSku;
if>sku.getAvailableStock){ throw conflict;}
if)){ throw conflict;}
LocalDateTime now=LocalDateTime.now;CartIt emEntity entity=new Car tIt emEntity;entity.setU serI d;entity.setSku I d); entity.setQuantity); entity.setChecked;entity.setCreatedAt;entity.setUpdatedAt; 怎么说呢,
try{ cartIt emD bSe rvice.save;}catch{ throw conflict;} return assembleItem;}
- `requireCurrentUser...` 把 *登录态* 当成唯一可信来源—— **pain point** — 切勿把 token 换成自定义 header 再自行解析,否则失去统一鉴权保障。
- `requireSaleable...` 确认商品上架+价格+库存—— **pain point** — 前段隐藏按钮并不代表后台可以省略校验。
- `exists...` + 捕获 `DataIntegrityViolationException` 双保险—— **pain point** — 并发场景下两次检查仍可能冲突,此时依赖唯一索引抛异常回滚事务即可。
可售校验细节
java private void requireSaleableProductAndSku{ boolean productOnSale=ProductStatus.ON_SALE.name.equals);boolean positivePrice=sku.getSalePrice!=null && sku.getSalePrice.compareTo>0;boolean hasStock=sku.getAvailableStock!=null && sku.getAvailableStock>0;if{ throw conflict;}} *Pain points*: 商品草稿 → 不可加;价格为负 → 不可加,库存为零 → 不可加。
java Long userI d=currentUserSer vice.requireCurrentUs erI d;Listdlist);if){ return CardRes ponse.builder .items) .totalQuantity .estimatedTotal) .build;} 批量获取 SKU 与 Product
java Sets k uIds=cartItems.stream .map.collect);Map s k uMap= s k uD bSer v ice.listExistingB yIds.stream .collect));Set P roductIds=s k uMap.values.stream .map.collect);Map P roductMap= p roductD bSer vice.listExistingB yIds.stream .collect));*Pain point*: 若忘记批量查。只用 `for each cartRow -> queryOneSku` 将产生 N 次 SQL,严重拖慢响应。老实说, 行组装
java private CardIt emResp onse assembleIt em{ BigDecimal unitPrice=s ku==null?null:s ku.getSa lePri ce;BigDecimal lineAmt=?null: unitPrice.multiply)) .setScale;boolean saleable=s ku!=null && p rod uct!=null && ProductStatus.ON_SALE.name.equals) && unitPrice!=null && unitPrice.compareTo>0 && s ku.getAvailableSto ck!=null && s ku.getAvailableSto ck>=item.ge tQuantity;*Pain points*:return Card ItEmResp onse.builder .id) .sku Id) .product Id) .productTitle) .skuCode) .specText) .currentUnitPri ce .quantity) .availableSto ck) .checked.equals)) .saleable .lineAmount .updatedAt) .build;}
saleable=false会在 UI 中展示「已下架」或「库存不足」提示,而不是直接删掉行。lineAmount=null表示当前不可买,不参与estimatedTotal。汇总统计
java int totalQty=respon ses.stream.mapToInt.sum;BigDecimal estTotal=respon ses.stream .filter)) .filter)) .map .reduce,BigDecimal::add);说起来,Pain points: 未勾选或不可售行不计入合计。否则使用者会看到「看不到但被算进去」的错误金额。
DbService – 所有查询必须携带 UserID
java /** * Shopping‑cart DB Service – every query MUST contain a user_id condition */
public List
dlistByUs erIde{ return lambdaQuery.eq .orderDesc . list;} public Boolean existsByUs erIdeAn dsKuIde{ return lambdaQuery.eq . eq . ne xists;}
Pain point: 若遗漏
eq就会出现跨使用者数据泄漏,是最常见的安全漏洞。
本地运行 &,amp;amp,amp;amp,amp;amp,amp;amp,amp;amp,amp;a curl 验证
登录获取 token
bash curl -sS -X POST http://localhost:/api/auth/login \ -H 'Content-Type: application/json' \ -d '{"username":"xiaoming","password":"Mall123456"}'
export USER_TOKEN='eyJhbGciOiJIUz...'
未登录尝试加入
bash curl -i -X POST http://localhost:/api/cart/items \ -H 'Content-Type: application/json' \ -d '{"skuId":101。"quantity":1}'
正常加入并验证 userId 不可信
SELECT id,userid,skuid FROM mallcartitem WHERE sku_id=102;
数量超限验证
重复加入报错
商品下架后的表现
将商品下架后 查询这方面,
bash curl -i http://localhost:/api/cart/items \ -H "Authorization: Bearer $USER_TOKEN"
常见错误清单
错误场景 对应痛点 正确做法 在 Request 中接收 userId痛点① 永远使用 CurrentUserService.requireCurrentUserId把 productId写进 cart 表痛点② 保存 sku_id因为规格决定价格与库存前端自行提交 price 并依赖它 痛点③ 后端统一从 SKU 实时读取 未检查商品上架状态就写入 痛点④ 调用 requireSaleableProductAndSku完全依赖本地去重。不建唯一索引 痛点⑤ 在业务层提前判断,同时建唯一索引防并发 循环单条查询 SKU/Product 导致 N+ 痛点⑥ 批量收集 ID,一次性查询 删除不可售行而非标记 痛点⑦ 保留记录并返回 saleable=false给前端提示把 estimatedTotal当作订单最终金额提交痛点⑧ 创建订单阶段重新计算并锁定库存
小练习与自检
- 绘制关系图画出
MALL_CART_ITEM ↔ MALL_PRODUCT_SKU ↔ MALL_PRODUCT. 标明外键方向还有为何 cart 表只保存 sku_id。- 解释 UserID 来源阅读
CartAddRequest,CartFacade.addItem。测试shouldAddSaleableSkuForCurrentUserAndIgnoreSubmittedUserI d. JWT 如何成为唯一可信来源。- Curl 验证未登录限制执行未带 Authorization 的 POST。请记录返回码与业务码,对比普通参数错误码 的区别。
- 验证唯一索引防重复对同一使用者多次发送相同 sku 的请求。观察返回码,并解释数据库约束如何保证幂等性。
- EstimatedTotal 算法拆解阅读
queryMyCart中对流的过滤条件,说明只有 checked 且 saleable 的行才计入预估总额。
下一章预告 — 从“意向”到“承诺”
本章节已经完成:
- 使用者身份安全边界;
- 最小化持久化模型;
- 完整的可售校验;
- 幂等性与唯一约束;
- 高效批量查询;
- 金额预估 vs 实际成交;
下一篇将进入真正交易链路:
1️⃣ 从已勾选的购物车项生成订单;2️⃣ 在事务中重新校验价格 &,amp;amp,amp;a 库存,3️⃣ 锁定库存 并生成订单快照;4️⃣ 成功后删除已下单的 cart 行。
这一步骤是保证“一致性”和“防止半成功状态”的关键,也是从“页面交互”迈向“业务事务”的必经之路。
本章要点
1️⃣ 后端购物车是 使用者维度 持久化资源,而非浏览器内存数组;2️⃣
/api/cart/items不接受任何来自客户端的userID;当前使用者只能从 JWT 中解析;3️⃣ 保存S Ku_ID;因为规格决定实际购买属性;4️⃣ 加入请求仅允许提交S Ku_ID + quantity;所有其它信息均由服务器生成或组装;5️⃣ 加入之前必须校验 SKU 是否存在、商品是否上架、价格是否正数还有库存是否充足;6️⃣ 同一使用者同一 SKU 不得重复插入——业务层提前判断 + 数据库唯一索引双保险;说起来,7️⃣ 查询时一次性批量获取所有相关 SKU 与商品。以避免 N+ 查询,8️⃣ 商品下架或库存不足时保留记录但标记S ale able=false;怎么说呢,前段负责展示原因而非直接删除记录;不过,9️⃣TotalQuantity>包含全部行数。用于角标显示,E stimatedTotal>则仅统计已勾选且可售的行;10️⃣ Shopping‑cart 金额仅作 预估 使用,真正成交金额必须在创建订单阶段重新计算并持久化为快照。如果你能够清晰回答:“为什么即使请求体里有
UserID=我们仍然把物品放进当前登录使用者的购物车?”还有 “为什么不可售行仍然保留但不会计入E stimatedTotal>” 那么你已经彻底掌握了本章节所强调的两大主要能力——使用者资源隔离 与 动态组装视图数据。祝贺你迈出了从 Front‑End 到 Full‑Stack 的关键一步!
作为专业的SEO优化服务提供商,我们致力于通过科学、系统的搜索引擎优化策略,帮助企业在百度、Google等搜索引擎中获得更高的排名和流量。我们的服务涵盖网站结构优化、内容优化、技术SEO和链接建设等多个维度。
| 服务项目 | 基础套餐 | 标准套餐 | 高级定制 |
|---|---|---|---|
| 关键词优化数量 | 10-20个核心词 | 30-50个核心词+长尾词 | 80-150个全方位覆盖 |
| 内容优化 | 基础页面优化 | 全站内容优化+每月5篇原创 | 个性化内容策略+每月15篇原创 |
| 技术SEO | 基本技术检查 | 全面技术优化+移动适配 | 深度技术重构+性能优化 |
| 外链建设 | 每月5-10条 | 每月20-30条高质量外链 | 每月50+条多渠道外链 |
| 数据报告 | 月度基础报告 | 双周详细报告+分析 | 每周深度报告+策略调整 |
| 效果保障 | 3-6个月见效 | 2-4个月见效 | 1-3个月快速见效 |
我们的SEO优化服务遵循科学严谨的流程,确保每一步都基于数据分析和行业最佳实践:
全面检测网站技术问题、内容质量、竞争对手情况,制定个性化优化方案。
基于用户搜索意图和商业目标,制定全面的关键词矩阵和布局策略。
解决网站技术问题,优化网站结构,提升页面速度和移动端体验。
创作高质量原创内容,优化现有页面,建立内容更新机制。
获取高质量外部链接,建立品牌在线影响力,提升网站权威度。
持续监控排名、流量和转化数据,根据效果调整优化策略。
基于我们服务的客户数据统计,平均优化效果如下:
我们坚信,真正的SEO优化不仅仅是追求排名,而是通过提供优质内容、优化用户体验、建立网站权威,最终实现可持续的业务增长。我们的目标是与客户建立长期合作关系,共同成长。
Demand feedback