首页
壁纸
统计
友链
Search
1
Nestjs概述-中文
10 阅读
2
ExpressAPI
10 阅读
3
NestJS新手入门核心模块对比表笔记
9 阅读
4
JavaScript企业数据处理实用指南
9 阅读
5
Vue2详细笔记
8 阅读
Nodejs
Vue
Java
Msql
登录
Search
Wasnl
累计撰写
12
篇文章
累计收到
1
条评论
首页
栏目
Nodejs
Vue
Java
Msql
页面
壁纸
统计
友链
搜索到
12
篇与
的结果
2026-03-30
MySQL SQL性能调优实战指南
# MySQL SQL性能调优实战指南一、核心思想:系统化调优四步法SQL调优不是盲目加索引,而是一个系统化工程,应遵循 测量 -> 分析 -> 优化 -> 验证 的闭环流程。测量:通过慢查询日志、监控系统定位需要优化的慢SQL。分析:使用EXPLAIN等工具分析SQL的执行计划,找到性能瓶颈。优化:根据分析结果,采取索引优化、SQL重写、架构调整等手段。验证:在测试环境验证优化效果,并上线监控。二、性能诊断与分析工具1. 慢查询日志作用:记录执行时间超过阈值的SQL语句。配置:-- 开启慢查询日志 SET GLOBAL slow_query_log = ON; -- 设置阈值(如0.1秒) SET GLOBAL long_query_time = 0.1; -- 查看日志文件路径 SHOW VARIABLES LIKE 'slow_query_log_file';2. EXPLAIN 执行计划分析作用:查看MySQL如何执行你的SQL,是调优的核心工具。关键字段解读:type:访问类型,性能从好到差:system > const > eq_ref > ref > range > index > ALL。ALL:全表扫描,性能最差,必须优化。range:范围扫描,使用了索引,常见于BETWEEN、>、LIKE 'abc%'。ref:非唯一性索引查找。possible_keys:可能用到的索引。key:实际使用的索引。rows:MySQL估计需要扫描的行数,这个值越小越好。Extra:额外信息,关键提示:Using index:覆盖索引,查询的列都在索引中,无需回表,性能高。Using where:需要回表过滤数据。Using filesort:文件排序,需要优化(通常需要为ORDER BY字段建立索引)。Using temporary:使用临时表,常见于GROUP BY或DISTINCT,性能差。使用方式:EXPLAIN SELECT * FROM users WHERE age > 25; EXPLAIN FORMAT=JSON SELECT * FROM users WHERE age > 25; -- 更详细的结构化信息 -- MySQL 8.0+ 可以查看实际执行计划 EXPLAIN ANALYZE SELECT * FROM users WHERE age > 25;三、索引优化策略(核心)索引是提升查询性能的关键,但也是一把双刃剑(占用空间、影响写入性能)。1. 索引设计原则高选择性字段优先:字段的唯一性越高,索引效果越好。-- 计算选择性:值越接近1,选择性越高 SELECT COUNT(DISTINCT user_id) / COUNT(*) FROM orders;最左前缀原则:复合索引 (a, b, c) 会创建 (a), (a,b), (a,b,c) 三个索引。查询条件必须包含索引的最左列,否则索引失效。索引列顺序:等值查询放前面,范围查询放后面。避免冗余索引:尽量复用现有索引,删除无用索引。-- 查看表上的索引 SHOW INDEX FROM table_name;2. 索引优化实战覆盖索引:查询的列都在索引中,避免回表。-- 假设有复合索引 idx_status_covering(status, user_id, order_no) SELECT user_id, order_no FROM orders WHERE status = 'pending'; -- 走覆盖索引范围查询优化:-- 原始(慢):索引可能只用到create_time CREATE INDEX idx_time ON orders(create_time); SELECT * FROM orders WHERE create_time BETWEEN '2024-01-01' AND '2024-01-31' AND status = 'completed'; -- 优化后:将等值条件放在前面 CREATE INDEX idx_status_time ON orders(status, create_time);3. 索引失效的常见场景(避坑指南)函数操作:对索引列使用函数或计算。-- ❌ 失效 SELECT * FROM users WHERE DATE(create_time) = '2024-01-01'; -- ✅ 优化 SELECT * FROM users WHERE create_time >= '2024-01-01' AND create_time < '2024-01-02';隐式类型转换:数据类型不匹配。-- ❌ 失效(phone是varchar类型) SELECT * FROM users WHERE phone = 13800138000; -- ✅ 优化 SELECT * FROM users WHERE phone = '13800138000';前导通配符:LIKE以%开头。-- ❌ 失效 SELECT * FROM users WHERE name LIKE '%john%'; -- ✅ 可能有效(后缀通配符) SELECT * FROM users WHERE name LIKE 'john%';OR条件:如果OR连接的条件中有一个没有索引,整个查询都可能不走索引。NOT IN 或 !=:可能导致索引失效(或效果变差)。四、SQL语句优化技巧1. 查询重写用JOIN代替子查询:子查询会创建临时表,JOIN通常性能更好。-- ❌ 低效的子查询 SELECT * FROM orders WHERE user_id IN (SELECT id FROM users WHERE vip_level > 3); -- ✅ 高效JOIN SELECT o.* FROM orders o JOIN users u ON o.user_id = u.id WHERE u.vip_level > 3;用EXISTS代替IN:当外表数据量大时,EXISTS可能更优。避免SELECT *:只查询需要的字段,特别是对于TEXT/BLOB等大字段。HAVING改为WHERE:HAVING用于分组后过滤,尽量将过滤条件放在WHERE中提前过滤。2. 分页查询优化问题:LIMIT 1000000, 20 会扫描100万条记录,性能极差。方案一:游标分页:基于有序且唯一的列(如自增ID)进行翻页。-- 每次记录上一页最后一条的ID SELECT * FROM orders WHERE id > 1000000 ORDER BY id LIMIT 20;方案二:延迟关联:先通过覆盖索引查询出主键,再回表获取其他字段。SELECT * FROM orders o JOIN (SELECT id FROM orders ORDER BY id LIMIT 1000000, 20) t ON o.id = t.id;3. 大数据量处理分批处理:避免一次性操作大量数据,应分批次循环处理。-- 每次处理1000条 UPDATE orders SET status = 'processed' WHERE status = 'pending' LIMIT 1000;分区表:对于历史数据,可以使用分区表进行物理隔离,按时间或键值分区。CREATE TABLE logs ( id BIGINT, log_date DATE ) PARTITION BY RANGE (YEAR(log_date)) ( PARTITION p2023 VALUES LESS THAN (2024), PARTITION p2024 VALUES LESS THAN (2025) );五、数据库架构与配置优化1. 架构优化读写分离:主库负责写入,从库负责读取,分散压力。分库分表:当单表数据量过大(如超过千万级)时,垂直分库(按业务拆分)或水平分表(按ID取模)。引入缓存:热点数据(如用户信息、商品详情)使用Redis等缓存,减少数据库压力。2. 配置参数调优(关键)在my.cnf或my.ini中调整:InnoDB缓冲池(最重要):设置为物理内存的50%-80%。innodb_buffer_pool_size = 16G日志缓冲:innodb_log_buffer_size = 64M连接数:max_connections = 1000排序/临时表:sort_buffer_size = 2M tmp_table_size = 64M六、实战调优案例:电商订单查询场景查询用户近3个月内状态为pending或shipped的订单,按时间倒序取前20条。SELECT * FROM orders WHERE user_id = 100 AND status IN ('pending', 'shipped') AND create_time BETWEEN '2024-01-01' AND '2024-03-01' ORDER BY create_time DESC LIMIT 20;问题分析原表可能没有索引,导致全表扫描。ORDER BY导致Using filesort。优化步骤分析:EXPLAIN 显示 type=ALL,Extra=Using where; Using filesort。建立复合索引:遵循最左前缀原则,将等值查询的字段user_id和status放在前面,范围查询字段create_time放在后面,且确保排序的字段也在索引中。CREATE INDEX idx_user_status_time ON orders(user_id, status, create_time);改写查询:利用覆盖索引,只查需要的字段,减少回表。SELECT id, user_id, order_no, amount, create_time FROM orders WHERE user_id = 100 AND status IN ('pending', 'shipped') AND create_time BETWEEN '2024-01-01' AND '2024-03-01' ORDER BY create_time DESC LIMIT 20;验证:再次EXPLAIN,type变为range或ref,rows大幅减少,Extra中不再有Using filesort,且出现Using index(覆盖索引)。七、调优检查清单[ ] 慢查询日志:是否已开启并定期分析?[ ] 索引设计:高频查询条件是否有索引?复合索引顺序是否合理?[ ] 索引失效:是否对索引列使用了函数、计算、隐式转换?[ ] SQL写法:是否避免了SELECT *?分页是否使用了大偏移量?[ ] 表结构:字段类型是否选择合适(如能用int就不用varchar)?[ ] 架构层面:读写分离、缓存、分库分表是否按需引入?[ ] 配置参数:缓冲池大小是否合理?连接数是否充足?总结MySQL SQL调优的核心是通过EXPLAIN理解SQL的执行路径,并利用索引减少需要扫描的数据行数。它是一个持续优化的过程,需要结合业务场景、数据规模和系统架构来综合考量。记住口诀:测量先行,索引为王,SQL避坑,架构护航。
2026年03月30日
2 阅读
0 评论
0 点赞
2026-03-27
答辩
校园二手交易系统答辩问题及回答一、系统架构与技术选型1. 请介绍一下系统的整体架构设计回答:本系统采用B/S三层架构模式构建,包括前端展示层、后端服务层和数据持久化层。前端展示层:基于Vue 3框架构建,使用Vant移动端组件库开发响应式界面。采用单页面应用(SPA)模式,通过Vue Router管理路由导航,使用Pinia进行全局状态管理,采用组件化开发模式提升代码复用性。后端服务层:基于NestJS框架开发,采用模块化方式组织代码结构。核心业务模块包括认证授权模块、用户信息模块、商品资源模块、交易订单模块、信用档案模块、后台管理模块等。每个模块内部遵循控制器-服务-数据访问对象的分层设计。数据持久化层:采用MySQL 8.0作为关系型数据库,引入Redis作为缓存层存储会话Token和热点数据,提升系统响应效率。代码位置:后端入口:back-end/src/main.ts模块配置:back-end/src/app.module.ts前端路由:frontEnd/src/router/index.ts2. 为什么选择NestJS作为后端框架?与Express有什么区别?回答:选择NestJS主要基于以下考虑:架构规范性:NestJS提供了完整的架构支撑体系,包括依赖注入、模块化组织、切面编程等特性,强制开发者遵循最佳实践,代码结构更清晰。TypeScript原生支持:NestJS完全采用TypeScript编写,与前端Vue 3的TypeScript开发形成统一的技术栈,类型定义一致,降低沟通成本。内置功能丰富:Guards组件实现权限访问控制Interceptors组件统一处理日志输出与响应格式标准化Pipes组件完成请求参数校验Filters组件统一异常处理与Express的关系:NestJS底层使用Express作为HTTP服务器,但在此基础上提供了更高层次的抽象和结构化支持。代码示例:// back-end/src/app.module.ts - 模块化组织 @Module({ imports: [ AuthModule, UsersModule, GoodsModule, OrdersModule, // ... ], providers: [ { provide: APP_FILTER, useClass: ApiExceptionFilter }, { provide: APP_INTERCEPTOR, useClass: ApiResponseInterceptor }, ], })3. 系统是如何实现前后端分离的?API是如何设计的?回答:系统采用RESTful API设计风格实现前后端分离:API设计规范:统一前缀:/api/v1资源命名:使用复数名词(如/goods、/orders)HTTP方法语义:GET查询、POST创建、PATCH更新、DELETE删除统一响应格式:{ code, message, data }接口文档:集成Swagger自动生成API文档,访问/api/docs查看认证机制:采用JWT双令牌认证,Access Token用于接口访问,Refresh Token用于令牌刷新代码位置:API文档配置:back-end/src/main.ts统一响应拦截器:back-end/src/common/interceptors/api-response.interceptor.ts统一异常过滤器:back-end/src/common/filters/api-exception.filter.ts4. 为什么选择Vue 3而不是Vue 2?组合式API有什么优势?回答:选择Vue 3的原因:性能提升:Vue 3重写了虚拟DOM算法,编译优化更智能,运行时性能更好组合式API优势:更好的逻辑复用:通过自定义Hook提取可复用逻辑更好的TypeScript支持:类型推断更准确更灵活的代码组织:相关逻辑可以放在一起,不再受选项式API的分散限制Tree-shaking支持:按需引入API,打包体积更小代码示例:// frontEnd/src/stores/auth.ts - 组合式API风格 export const useAuthStore = defineStore('auth', () => { const user = ref<User | null>(null) const token = ref<string | null>(localStorage.getItem('token')) const isAuthenticated = computed(() => !!token.value) const login = async (params: LoginParams) => { const response = await authApi.login(params) setToken(response.accessToken) // ... } return { user, token, isAuthenticated, login } })二、认证授权模块5. 请详细说明JWT双令牌认证机制的实现回答:系统采用Access Token + Refresh Token双令牌机制:Access Token:有效期:2小时载荷:用户ID、角色、唯一标识符(jti)用途:接口访问鉴权Refresh Token:有效期:7天载荷:用户ID、唯一标识符存储:Redis中存储,key为refresh:token:{userId}用途:刷新会话令牌旋转策略:每次刷新生成新的Refresh Token,旧Token立即失效,防止令牌被窃取后长期有效黑名单机制:用户登出时将Access Token的jti加入Redis黑名单,防止已登出的Token继续使用代码位置:back-end/src/modules/auth/auth.service.ts核心代码:// 签发Access Token private async signAccessToken(userId: number, role: number) { const jti = randomUUID(); const expiresInSeconds = 60 * 60 * 2; // 2小时 const accessToken = await this.jwt.signAsync( { sub: userId, role, jti }, { expiresIn: expiresInSeconds }, ); return { accessToken, expiresIn: expiresInSeconds, jti }; } // 用户登出 - 加入黑名单 async logout(userId: number, accessJti: string, refreshToken: string) { await this.redis.set(`jwt:blacklist:${accessJti}`, '1', 'EX', 60 * 60 * 24); // 删除Refresh Token await this.redis.del(`refresh:token:${userId}`); }6. 密码是如何加密存储的?为什么选择bcrypt?回答:系统使用bcrypt算法对密码进行加密存储:bcrypt特点:自带盐值:每次加密自动生成随机盐,防止彩虹表攻击计算成本可调:通过cost factor控制计算复杂度,抵御暴力破解单向加密:无法反向解密,只能通过比较验证实现方式:// 注册时加密 const passwordHash = await bcrypt.hash(dto.password, 10); // cost factor = 10 // 登录时验证 const ok = await bcrypt.compare(dto.password, user.password);安全性:即使数据库泄露,攻击者也无法获取原始密码代码位置:back-end/src/modules/auth/auth.service.ts 第62行、第132行7. 如何实现用户权限控制?管理员和普通用户如何区分?回答:系统通过角色字段实现权限控制:角色定义:role=1:普通用户role=2:管理员JWT载荷携带角色信息:{ sub: userId, role, jti }Guard守卫实现:JwtAuthGuard:验证JWT有效性AdminGuard:验证是否为管理员角色路由级别控制:// back-end/src/modules/admin/admin.guard.ts canActivate(context: ExecutionContext): boolean { const request = context.switchToHttp().getRequest(); const user = request.user; if (user.role !== 2) { throw new ForbiddenException('需要管理员权限'); } return true; }前端路由守卫:// frontEnd/src/router/index.ts if (roles && roles.includes('admin') && userRole !== 2) { next('/home') return }三、商品资源模块8. 商品状态流转是如何设计的?有哪些状态?回答:商品状态采用状态机模式设计,共7种状态:状态值状态名称说明0PENDING_AUDIT待审核1ONLINE在售2SOLD已售空3OFFLINE下架4REJECTED审核拒绝5VIOLATION违规商品6DELETED软删除状态流转规则:发布 → 待审核(0)审核通过 → 在售(1)审核拒绝 → 审核拒绝(4)库存为0 → 已售空(2)卖家主动下架 → 下架(3)管理员标记违规 → 违规商品(5)代码位置:back-end/src/modules/goods/goods.service.ts 第51-79行9. 商品浏览量是如何实现的?为什么采用异步累加?回答:商品浏览量采用异步累加机制:实现方式:// 异步累加浏览量(不阻塞响应) this.goodsRepo.increment({ id: goodsId }, 'viewCount', 1).catch(() => {});为什么异步:浏览量是非关键业务数据,不需要同步等待异步处理可以提升接口响应速度即使累加失败也不影响用户查看商品详情优化考虑:高并发场景可改为Redis计数器,定时批量同步到数据库可添加防刷机制(同一用户短时间内重复访问只计一次)代码位置:back-end/src/modules/goods/goods.service.ts 第181行10. 商品搜索是如何实现的?支持哪些筛选条件?回答:商品搜索通过TypeORM的QueryBuilder实现:支持的筛选条件:分类筛选:categoryId关键词搜索:标题或描述模糊匹配价格区间:minPrice、maxPrice成色等级:condition状态筛选:status实现代码:const qb = this.goodsRepo .createQueryBuilder('goods') .where('goods.status NOT IN (6, 7)'); // 排除软删除 if (query.keyword) { qb.andWhere('(goods.title LIKE :keyword OR goods.description LIKE :keyword)', { keyword: `%${query.keyword}%` }); } if (query.minPrice != null) { qb.andWhere('goods.price >= :minPrice', { minPrice: query.minPrice }); } // ...性能优化:分页查询,默认每页20条按创建时间倒序排列关键字段建立索引代码位置:back-end/src/modules/goods/goods.service.ts 第89-147行四、交易订单模块11. 订单状态机是如何设计的?状态如何流转?回答:订单状态采用状态机模式,共7种状态:状态值状态名称说明1CREATED待付款2PAID待发货3SHIPPED待收货4COMPLETED已完成5REFUNDING退款中6REFUNDED已退款7CANCELLED已取消状态流转规则:创建订单 → 待付款(1)支付成功 → 待发货(2)卖家发货 → 待收货(3)买家确认收货 → 已完成(4)申请退款 → 退款中(5)退款成功 → 已退款(6)取消订单 → 已取消(7)代码位置:back-end/src/modules/orders/orders.service.ts 第28-43行12. 订单创建时如何保证数据一致性?回答:订单创建使用数据库事务保证数据一致性:事务处理流程:await this.ordersRepo.manager.transaction(async (em) => { // 1. 扣减库存 goods.stock -= quantity; if (goods.stock <= 0) goods.status = 2; // 已售空 await em.save(goods); // 2. 创建订单 await em.save(this.ordersRepo.create({ orderNo, buyerId, sellerId: goods.sellerId, goodsId: goods.id, price: price.toFixed(2), // ... })); });一致性保证:库存扣减和订单创建在同一事务中任一步骤失败,整个事务回滚避免超卖问题代码位置:back-end/src/modules/orders/orders.service.ts 第183-227行13. 订单超时自动取消是如何实现的?回答:系统设计了订单超时机制:超时规则:待付款订单:30分钟超时自动取消待发货订单:72小时超时提醒待收货订单:7天自动确认收货前端倒计时:if (order.status === OrderStatus.CREATED) { const expireAt = new Date(order.createTime.getTime() + 30 * 60 * 1000); const seconds = Math.max(0, Math.floor((expireAt.getTime() - now.getTime()) / 1000)); countDown = { type: 'payment', expireAt, seconds }; }后端处理:可通过定时任务扫描超时订单或使用消息队列延迟队列实现代码位置:back-end/src/modules/orders/orders.service.ts 第388-400行14. 退款流程是如何设计的?回答:退款流程设计如下:退款申请条件:已付款、已发货、已完成(7天内)的订单可申请退款已完成订单超过7天无法申请退款处理流程:买家申请退款 → 订单状态变为退款中(5)卖家同意 → 金额退回买家钱包,恢复商品库存卖家拒绝 → 填写拒绝原因,订单恢复原状态退款代码实现:async handleRefund(userId: number, orderNo: string, dto: RefundHandleDto) { if (dto.action === RefundAction.AGREE) { await this.dataSource.transaction(async (em) => { // 1. 更新订单状态 order.status = OrderStatus.REFUNDED; await em.save(order); // 2. 退款到买家钱包 buyerWallet.balance = (Number(balanceBefore) + Number(order.price)).toFixed(2); await em.save(buyerWallet); // 3. 恢复商品库存 goods.stock += order.quantity; await em.save(goods); }); } }代码位置:back-end/src/modules/orders/orders.service.ts 第708-801行五、信用评价模块15. 信用评级体系是如何设计的?有什么作用?回答:信用评级体系是本系统的创新点:信用分规则:初始值:600分分值范围:0-1000分六个等级:极好(≥900)、优秀(≥800)、良好(≥700)、一般(≥600)、较差(≥500)、极差(<500)加分规则:实名认证:+50分订单完成:+5分收到好评:+3分扣分规则:取消订单:-10分超时违约:-20分违规处理:-50分周期限制:每条规则设定每日、每周、每月的积分变化上限,防止恶意刷分作用:为买家提供卖家信用参考约束用户行为,降低交易风险信用过低可限制部分功能代码位置:back-end/src/modules/credit/credit.service.ts16. 信用分的周期限制是如何实现的?回答:周期限制通过信用历史表查询实现:private async checkPeriodLimit( userId: number, ruleCode: string, dailyLimit: number, weeklyLimit: number, monthlyLimit: number, ): Promise<boolean> { const now = new Date(); const todayStart = new Date(now); todayStart.setHours(0, 0, 0, 0); // 检查每日上限 if (dailyLimit > 0) { const dailyCount = await this.creditHistoryRepo .createQueryBuilder('ch') .where('ch.userId = :userId', { userId }) .andWhere('ch.reason LIKE :reason', { reason: `%[${ruleCode}]%` }) .andWhere('ch.changeScore > 0') .andWhere('ch.createTime >= :todayStart', { todayStart }) .getCount(); if (dailyCount >= dailyLimit) return false; } // 类似检查每周、每月上限... }代码位置:back-end/src/modules/credit/credit.service.ts 第152-207行六、钱包支付模块17. 钱包充值流程是如何设计的?回答:钱包充值采用两阶段设计:阶段1:创建充值订单校验充值渠道(支付宝/微信)校验充值限额(单次≤10000元,单日≤50000元)生成支付单号,创建payment_record记录返回支付参数给前端阶段2:支付成功回调更新payment_record状态为PAID写入wallet_transaction流水更新wallet_account余额发送充值成功通知代码位置:back-end/src/modules/wallet/wallet.service.ts 第223-315行18. 提现功能有哪些限制?如何保证资金安全?回答:提现功能设计了多重限制:金额限制:单次提现上限:5000元单日提现上限:20000元最低提现金额:10元次数限制:每日最多提现3次余额校验:可用余额必须≥提现金额账户状态:钱包账户必须为启用状态资金安全:使用数据库事务悲观锁锁定账户先冻结余额,审核通过后扣减代码位置:back-end/src/modules/wallet/wallet.service.ts 第436-561行19. 如何防止钱包并发操作导致的数据不一致?回答:系统采用悲观锁机制防止并发问题:await this.dataSource.transaction(async (em) => { // 悲观锁锁定钱包账户 const account = await em.findOne(WalletAccountEntity, { where: { userId }, lock: { mode: 'pessimistic_write' }, }); // 执行余额操作 account.balance = newBalance.toFixed(2); await em.save(account); });原理:悲观锁在读取数据时加锁,其他事务必须等待结合数据库事务,保证操作的原子性避免超扣、重复扣款等问题代码位置:back-end/src/modules/wallet/wallet.service.ts 第504-515行七、数据库设计20. 数据库表是如何设计的?核心表有哪些?回答:系统核心包含7张数据表:表名说明核心字段user用户表id, student_id, username, password, credit_score, rolegoods商品表id, seller_id, title, price, stock, status, view_countorders订单表id, order_no, buyer_id, seller_id, goods_id, status, pricewallet_account钱包账户表id, user_id, balance, frozen_balancecategory商品分类表id, name, icon, sort_ordercredit_history信用历史表id, user_id, change_score, reasoncredit_rule信用规则表id, rule_code, rule_name, score_change表关系设计:用户-商品:1:N(一个用户可发布多个商品)用户-订单:1:N(用户可作为买家或卖家参与订单)用户-钱包:1:1(每个用户拥有唯一钱包账户)商品-分类:N:1(多个商品归属于同一分类)SQL文件位置:整体架构文档/campus_secondhand.sql21. 如何优化数据库查询性能?回答:系统从多个方面优化查询性能:索引设计:主键索引:所有表的id字段唯一索引:学号、手机号、订单号外键索引:seller_id、buyer_id、goods_id查询索引:status、create_time查询优化:使用QueryBuilder构建高效查询分页查询,避免全表扫描只查询需要的字段缓存策略:Redis缓存热点数据(如分类列表)JWT Token存储在Redis异步处理:浏览量累加异步执行通知发送异步处理八、系统安全性22. 系统有哪些安全防护措施?回答:系统实现了多层次的安全防护:认证安全:JWT双令牌机制密码bcrypt加密存储Token黑名单机制授权安全:基于角色的访问控制(RBAC)接口级别权限校验(Guard)输入安全:请求参数校验(ValidationPipe)XSS过滤SQL注入防护(TypeORM参数化查询)业务安全:信用评级约束用户行为交易金额限制举报机制代码位置:参数校验:back-end/src/main.ts 第14-20行XSS过滤:back-end/src/common/utils/xss.util.ts23. 如何防止XSS攻击?回答:系统通过以下方式防止XSS攻击:输入过滤:// xss.util.ts import validator from 'validator'; export function sanitizeXss(input: string): string { return validator.escape(input); }参数校验:// main.ts app.useGlobalPipes( new ValidationPipe({ transform: true, whitelist: true, forbidNonWhitelisted: true, }), );前端转义:Vue默认对插值表达式进行HTML转义九、前端实现24. 前端状态管理是如何实现的?回答:前端使用Pinia进行状态管理:Store设计:auth.ts:用户认证状态(token、用户信息、登录状态)app.ts:应用全局状态组合式API风格:export const useAuthStore = defineStore('auth', () => { const user = ref<User | null>(null) const token = ref<string | null>(null) const isAuthenticated = computed(() => !!token.value) const login = async (params) => { /* ... */ } const logout = async () => { /* ... */ } return { user, token, isAuthenticated, login, logout } })持久化:Token和用户信息存储在localStorage代码位置:frontEnd/src/stores/auth.ts25. 前端路由守卫是如何实现的?回答:前端通过Vue Router的导航守卫实现权限控制:router.beforeEach(async (to, from, next) => { const authStore = useAuthStore() const { requiresAuth, requiresGuest, roles } = to.meta // 需要登录但未登录 if (requiresAuth && !authStore.isAuthenticated) { next('/login') return } // 登录页但已登录 if (requiresGuest && authStore.isAuthenticated) { next('/home') return } // 角色权限校验 if (roles && roles.includes('admin') && authStore.user?.role !== 2) { next('/home') return } next() })代码位置:frontEnd/src/router/index.ts 第261-285行十、项目难点与解决方案26. 项目开发过程中遇到了哪些难点?如何解决的?回答:难点1:订单状态流转的复杂性问题:订单涉及多种状态和状态转换,容易出现非法状态转换解决:采用状态机模式,定义明确的状态枚举和转换规则,每次状态变更前校验当前状态是否允许转换难点2:并发场景下的数据一致性问题:钱包余额操作、库存扣减等场景存在并发风险解决:使用数据库事务+悲观锁机制,保证操作的原子性难点3:信用评级体系的规则配置问题:信用规则需要灵活配置,同时要防止恶意刷分解决:设计信用规则配置表,支持动态配置;实现周期限制检查机制难点4:前后端Token同步问题:Token过期后需要无感刷新解决:实现Token刷新机制,前端拦截401错误自动刷新Token27. 系统还有哪些可以改进的地方?回答:实时通讯:新增买卖双方实时在线沟通功能,降低沟通成本智能推荐:基于用户浏览历史、校园身份标签进行商品推荐搜索优化:引入Elasticsearch实现全文搜索,提升搜索体验消息推送:集成WebSocket实现实时消息推送安全升级:敏感数据加密存储,引入风控系统性能优化:引入消息队列处理异步任务,提升系统吞吐量十一、代码定位速查28. 如果老师问某个功能的代码在哪里,如何快速定位?回答:请参考本文档附件《后端代码架构快速定位指南》,按照以下方式快速定位:按模块定位:根据功能所属模块,在back-end/src/modules/下找到对应目录按层次定位:控制器(Controller):处理HTTP请求服务(Service):处理业务逻辑实体(Entity):数据库表映射按功能定位:登录注册 → modules/auth/商品管理 → modules/goods/订单交易 → modules/orders/钱包支付 → modules/wallet/信用评价 → modules/credit/后台管理 → modules/admin/附件:后端代码架构快速定位指南一、项目目录结构总览back-end/ ├── src/ │ ├── common/ # 公共模块 │ │ ├── constants/ # 常量定义(错误码) │ │ ├── decorators/ # 自定义装饰器 │ │ ├── exceptions/ # 自定义异常 │ │ ├── filters/ # 异常过滤器 │ │ ├── interceptors/ # 拦截器 │ │ ├── types/ # 类型定义 │ │ └── utils/ # 工具函数 │ │ │ ├── infrastructure/ # 基础设施 │ │ ├── database/ # 数据库模块 │ │ ├── logger/ # 日志配置 │ │ └── redis/ # Redis模块 │ │ │ ├── modules/ # 业务模块(核心) │ │ ├── admin/ # 后台管理模块 │ │ ├── auth/ # 认证授权模块 │ │ ├── categories/ # 商品分类模块 │ │ ├── comments/ # 评论模块 │ │ ├── credit/ # 信用评价模块 │ │ ├── goods/ # 商品资源模块 │ │ ├── notifications/ # 消息通知模块 │ │ ├── orders/ # 交易订单模块 │ │ ├── payments/ # 支付模块 │ │ ├── users/ # 用户信息模块 │ │ └── wallet/ # 电子钱包模块 │ │ │ ├── app.module.ts # 应用主模块 │ ├── app.controller.ts # 应用控制器 │ ├── app.service.ts # 应用服务 │ └── main.ts # 应用入口 │ ├── dist/ # 编译输出目录 ├── coverage/ # 测试覆盖率报告 ├── .env # 环境变量配置 └── package.json # 项目依赖配置二、核心模块详细定位1. 认证授权模块 (auth)目录: src/modules/auth/文件功能关键代码位置auth.controller.ts认证接口控制器登录、注册、刷新Token、登出接口auth.service.ts认证业务逻辑密码加密、Token签发、黑名单机制jwt.strategy.tsJWT策略Token验证策略jwt-auth.guard.tsJWT守卫接口权限拦截dto/auth.dto.ts数据传输对象登录、注册参数定义关键功能代码定位:用户注册:auth.service.ts → register() 方法(第40-96行)用户登录:auth.service.ts → login() 方法(第109-164行)Token刷新:auth.service.ts → refresh() 方法(第176-224行)用户登出:auth.service.ts → logout() 方法(第234-244行)2. 商品资源模块 (goods)目录: src/modules/goods/文件功能关键代码位置goods.controller.ts商品接口控制器商品CRUD接口goods.service.ts商品业务逻辑发布、查询、上下架、举报goods.entity.ts商品实体数据库表映射report.entity.ts举报实体举报记录表映射dto/goods.dto.ts数据传输对象商品参数定义关键功能代码定位:商品列表:goods.service.ts → list() 方法(第89-147行)商品详情:goods.service.ts → detail() 方法(第156-227行)发布商品:goods.service.ts → create() 方法(第236-270行)更新商品:goods.service.ts → update() 方法(第280-319行)删除商品:goods.service.ts → delete() 方法(第329-367行)下架商品:goods.service.ts → offline() 方法(第379-418行)举报商品:goods.service.ts → report() 方法(第520-560行)商品状态说明:// 状态枚举(goods.service.ts 第24-32行) // 0: 待审核 (PENDING_AUDIT) // 1: 在售 (ONLINE) // 2: 已售空 (SOLD) // 3: 下架 (OFFLINE) // 4: 审核拒绝 (REJECTED) // 5: 违规商品 (VIOLATION) // 6: 软删除 (DELETED)3. 交易订单模块 (orders)目录: src/modules/orders/文件功能关键代码位置orders.controller.ts订单接口控制器订单CRUD接口orders.service.ts订单业务逻辑创建、支付、发货、确认、退款order.entity.ts订单实体数据库表映射dto/orders.dto.ts数据传输对象订单参数定义关键功能代码定位:创建订单:orders.service.ts → createOrder() 方法(第148-241行)订单列表:orders.service.ts → listOrders() 方法(第250-324行)订单详情:orders.service.ts → detail() 方法(第369-443行)取消订单:orders.service.ts → cancel() 方法(第454-491行)订单发货:orders.service.ts → ship() 方法(第502-533行)确认收货:orders.service.ts → confirm() 方法(第544-642行)申请退款:orders.service.ts → refund() 方法(第653-695行)处理退款:orders.service.ts → handleRefund() 方法(第708-801行)订单状态说明:// 状态枚举(orders.service.ts 第28-43行) export enum OrderStatus { CREATED = 1, // 待付款 PAID = 2, // 待发货 SHIPPED = 3, // 待收货 COMPLETED = 4, // 已完成 REFUNDING = 5, // 退款中 REFUNDED = 6, // 已退款 CANCELLED = 7, // 已取消 }4. 电子钱包模块 (wallet)目录: src/modules/wallet/文件功能关键代码位置wallet.controller.ts钱包接口控制器余额查询、充值、提现接口wallet.service.ts钱包业务逻辑充值、提现、交易流水wallet-account.entity.ts钱包账户实体账户表映射wallet-transaction.entity.ts交易流水实体流水表映射dto/wallet.dto.ts数据传输对象钱包参数定义关键功能代码定位:钱包信息:wallet.service.ts → getMe() 方法(第114-128行)交易流水:wallet.service.ts → getTransactions() 方法(第140-181行)充值功能:wallet.service.ts → topup() 方法(第223-315行)提现功能:wallet.service.ts → withdraw() 方法(第436-561行)交易类型说明:// 交易类型(wallet.service.ts 第16-21行) // TOPUP: 充值 // ORDER_PAY: 订单支付 // ORDER_REFUND: 订单退款 // WITHDRAW: 提现 // ADMIN_ADJUST: 管理员调账5. 信用评价模块 (credit)目录: src/modules/credit/文件功能关键代码位置credit.service.ts信用业务逻辑加分、扣分、周期限制检查credit.module.ts信用模块定义模块配置关键功能代码定位:增加信用分:credit.service.ts → addCredit() 方法(第39-94行)扣减信用分:credit.service.ts → deductCredit() 方法(第104-140行)周期限制检查:credit.service.ts → checkPeriodLimit() 方法(第152-207行)按规则加分:credit.service.ts → addCreditByRule() 方法(第216-235行)按规则扣分:credit.service.ts → deductCreditByRule() 方法(第244-262行)信用等级:credit.service.ts → getCreditLevel() 方法(第269-283行)信用等级划分:// 信用等级(credit.service.ts 第269-283行) // ≥900: 极好 (excellent) // ≥800: 优秀 (great) // ≥700: 良好 (good) // ≥600: 一般 (normal) // ≥500: 较差 (poor) // <500: 极差 (bad)6. 用户信息模块 (users)目录: src/modules/users/文件功能关键代码位置users.controller.ts用户接口控制器用户信息接口users.service.ts用户业务逻辑用户信息管理、实名认证user.entity.ts用户实体用户表映射credit-history.entity.ts信用历史实体信用历史表映射receiver.entity.ts收货地址实体地址表映射dto/users.dto.ts数据传输对象用户参数定义7. 后台管理模块 (admin)目录: src/modules/admin/子目录/文件功能admin.controller.ts后台管理控制器admin.service.ts后台管理服务admin.guard.ts管理员权限守卫users/用户管理goods/商品审核管理orders/订单管理reports/举报管理credit/信用规则管理categories/分类管理stats/数据统计audit-log.entity.ts审计日志实体三、公共模块定位1. 错误码定义文件: src/common/constants/error-codes.ts// 错误码分段 // 1000-1999: 认证与授权 // 2000-2999: 用户域 // 3000-3999: 商品域 // 4000-4999: 订单域 // 5000-5999: 支付域 // 8000-8999: 钱包域 // 9000-9999: 系统与基础设施2. 异常过滤器文件: src/common/filters/api-exception.filter.ts功能:统一捕获和处理异常,返回标准格式的错误响应3. 响应拦截器文件: src/common/interceptors/api-response.interceptor.ts功能:统一包装响应数据,返回标准格式{ code, message, data }四、快速定位技巧1. 按功能关键词搜索在IDE中使用全局搜索功能,搜索以下关键词:接口路径:如/auth/login、/goods、/orders方法名:如createOrder、handleRefund、addCredit状态值:如OrderStatus.CREATED、GoodsStatus.ONLINE2. 按API路径定位在控制器文件中搜索路由装饰器找到对应的处理方法跟踪到服务层查看业务逻辑3. 按数据库表定位在实体文件中查看表结构定义在服务文件中查看对该表的操作五、前端代码定位1. 目录结构frontEnd/src/ ├── api/ # API接口定义 ├── components/ # 公共组件 ├── hooks/ # 自定义Hook ├── router/ # 路由配置 ├── stores/ # 状态管理 ├── styles/ # 样式文件 ├── types/ # 类型定义 ├── utils/ # 工具函数 └── views/ # 页面组件 ├── admin/ # 后台管理页面 ├── auth/ # 登录注册页面 ├── goods/ # 商品详情页面 ├── order/ # 订单页面 ├── publish/ # 发布页面 └── user/ # 用户中心页面2. 关键文件定位功能文件位置路由配置router/index.ts认证状态stores/auth.tsAPI请求封装utils/request.ts登录页面views/auth/Login.vue商品详情views/goods/GoodsDetail.vue订单列表views/order/OrderList.vue后台管理views/admin/文档版本: v1.0 更新日期: 2026年3月 适用项目: 基于Node+Vue融合信用评级的校园二手交易系统
2026年03月27日
3 阅读
0 评论
0 点赞
2026-03-26
JavaScript 核心知识笔记
# JavaScript 核心知识笔记一、异步编程核心1. Promise 基础Promise 是 JavaScript 异步编程的基础,代表一个异步操作的最终完成(或失败)及其结果值。new Promise():创建一个异步任务。执行器函数接收 resolve 和 reject 两个参数。then() / catch():用于处理Promise成功或失败的结果。then 接收成功回调,catch 接收失败回调。Promise.resolve():将一个值包装成一个已成功的Promise对象。Promise.reject():将一个值包装成一个已失败的Promise对象。Promise.all():接收一个Promise数组,等待所有Promise都成功,返回一个包含所有结果的数组。只要有一个失败,整体就失败。Promise.race():接收一个Promise数组,等待第一个完成的Promise(无论成功或失败),返回其结果。2. async/await 语法糖async/await 是基于Promise的语法糖,让异步代码写起来像同步代码一样清晰。async函数:声明一个异步函数,它会隐式地返回一个Promise对象。await关键字:在 async 函数内部使用,用于等待一个Promise完成。await 会暂停函数的执行,直到Promise状态变为 fulfilled(成功)或 rejected(失败)。成功时直接返回结果值,失败时需要用 try...catch 捕获。// 示例:使用 async/await 替代 then/catch async function fetchUserData() { try { const user = await fetch('/api/user').then(res => res.json()); const posts = await fetch(`/api/posts/${user.id}`).then(res => res.json()); console.log('用户和帖子数据:', { user, posts }); } catch (error) { console.error('请求失败:', error); } }二、数组核心处理函数数组方法是JavaScript中最常用的工具集,掌握它们是高效处理数据的关键。2.1 every() - 全真才为真作用:测试一个数组内的所有元素是否都能通过指定函数的测试。它返回一个布尔值。场景:表单校验:检查所有输入项是否都通过了校验。权限校验:判断用户是否拥有全部所需的权限。批量数据的合规性检查:如所有商品数量是否都大于0。// 表单校验示例 const formFields = [ { name: 'username', valid: true, value: '张三' }, { name: 'email', valid: true, value: 'zhangsan@example.com' }, { name: 'phone', valid: false, value: '123' } ]; const isFormValid = formFields.every(field => field.valid); // false2.2 some() - 一真即为真作用:测试数组中是否至少有一个元素通过了指定函数的测试。它返回一个布尔值。场景:判断列表中是否存在未完成/异常的状态项。快速判断搜索是否命中结果。权限判断:只要用户拥有一个相关权限就能访问。// 搜索命中示例 const users = [{ name: '张三' }, { name: '李四' }, { name: '王五' }]; const keywords = '张三'; const hasMatch = users.some(user => user.name.includes(keywords)); // true2.3 filter() - 筛选过滤作用:创建一个新数组,其包含通过所提供函数实现的测试的所有元素。不会改变原数组。场景:数据筛选:根据条件过滤列表数据(如筛选出价格大于100的商品)。清理无效数据:过滤掉数组中的 null、undefined、空字符串等。数据分类:根据条件将原数组分成不同的子集。// 过滤空值示例 const strings = ["苹果", "", "香蕉", " ", "橙子", null]; const validStrings = strings.filter(s => s && s.trim()); // ["苹果", "香蕉", "橙子"]2.4 map() - 一对一映射作用:创建一个新数组,其结果是该数组中的每个元素调用一次提供的函数后的返回值。常用于数据转换。场景:接口数据适配:把后端返回的字段名(如 user_name)转成前端需要的(如 name)。渲染列表:把数据转成UI组件所需的格式。批量修改数组中的每一项。// 接口数据适配示例 const apiData = [{ user_name: '张三', user_age: 25 }]; const frontData = apiData.map(item => ({ name: item.user_name, age: item.user_age })); // 结果: [{ name: '张三', age: 25 }]2.5 reduce() - 万能汇总工具作用:对数组中的每个元素执行一个reducer函数(升序执行),将其结果汇总为单个返回值。reduce 是函数式编程中最强大的工具之一。参数:回调函数:(accumulator, currentValue) => newAccumulator初始值:initialValue,为累加器提供初始值。关键点:必须 return 新的累加器,否则下次迭代时累加器为 undefined。acc 可以是数字、对象、数组等任何类型。场景:聚合统计:求和、求平均值、统计出现次数。数据分组:把列表按某个字段分成不同的组。复杂的数据变换:数组转对象、多层数据处理。// 按类别分组并统计总价示例 const orderItems = [ { category: '食品', price: 20 }, { category: '服装', price: 100 }, { category: '食品', price: 30 } ]; const totalByCategory = orderItems.reduce((acc, item) => { acc[item.category] = (acc[item.category] || 0) + item.price; return acc; // 必须 return acc }, {}); // 初始值是一个空对象 {} // 结果: { 食品: 50, 服装: 100 }2.6 find() & findIndex() - 精准查找find():返回数组中第一个满足提供的测试函数的元素。找不到返回 undefined。findIndex():返回数组中第一个满足提供的测试函数的元素的索引。找不到返回 -1。与 indexOf 的区别:indexOf 只能用于简单值(如数字、字符串)的严格相等比较,而 find / findIndex 支持复杂的自定义判断条件。场景:根据ID查找特定的用户/数据项,或找到元素位置以进行后续的删除/更新操作。// 根据 ID 查找用户 const users = [{ id: 1, name: '张三' }, { id: 2, name: '李四' }]; const user = users.find(user => user.id === 2); // { id: 2, name: '李四' } const index = users.findIndex(user => user.id === 2); // 12.7 flat() & flatMap() - 数组扁平化flat(depth):按照一个可指定的深度递归遍历数组,并将所有元素与遍历到的子数组中的元素合并为一个新数组。depth 默认是1,可以传 Infinity 展开所有层级。flatMap():先执行 map,再对结果执行 flat(1),一步到位。场景:处理多层嵌套的数据结构。在 map 操作后直接扁平化,如将句子数组拆分成单词数组。// flatMap 示例:将句子拆分成单词 const lines = ["Hello world", "JavaScript is good"]; const words = lines.flatMap(line => line.split(' ')); // 结果: ["Hello", "world", "JavaScript", "is", "good"]2.8 includes() - 判断是否包含作用:判断一个数组是否包含一个指定的值,根据情况返回 true 或 false。用于简单值的判断。场景:检查用户角色、状态值等是否在允许的列表中。const roles = ['admin', 'editor']; const isAdmin = roles.includes('admin'); // true2.9 sort() - 排序(会修改原数组!)作用:对数组的元素进行排序,并返回原数组。注意:sort 方法会改变原数组。默认排序顺序是将元素转换为字符串,然后比较它们的UTF-16代码单元值序列。安全用法:使用 [...arr].sort() 先拷贝,再排序,避免修改原数组。数字排序:必须传入比较函数 (a, b) => a - b 来升序排序,(a, b) => b - a 来降序排序。const nums = [3, 1, 2]; const sortedNums = [...nums].sort((a, b) => a - b); // 新数组 [1, 2, 3] console.log(nums); // 原数组 [3, 1, 2],未改变2.10 reverse() - 反转数组(会修改原数组!)作用:反转数组中的元素,并返回原数组。同样会改变原数组。如需不可变操作,先拷贝。const arr = [1, 2, 3]; const reversedArr = [...arr].reverse(); // 新数组 [3, 2, 1]2.11 slice() - 截取数组作用:返回一个新的数组对象,这一对象是一个由 start 和 end 决定的原数组的浅拷贝(包括 start,不包括 end)。不会修改原数组。场景:提取数组的子集,或浅拷贝整个数组 slice(0) 或 slice()。const arr = [1, 2, 3, 4]; const newArr = arr.slice(1, 3); // [2, 3]2.12 splice() - 删除/插入/替换(会修改原数组!)作用:通过删除或替换现有元素或者原地添加新的元素来修改数组,并以数组形式返回被修改的内容。会修改原数组。应尽量避免在复杂逻辑中直接使用,或用不可变方式替代。参数:(start, deleteCount, item1, item2, ...)const arr = [1, 2, 3]; const removed = arr.splice(1, 1); // 从索引1开始删1个,removed = [2], arr = [1, 3] arr.splice(1, 0, 99); // 在索引1处插入99,arr = [1, 99, 3]三、对象核心处理函数3.1 Object.keys() / values() / entries() - 对象遍历三剑客Object.keys(obj):返回一个由对象的键名组成的数组。Object.values(obj):返回一个由对象的值组成的数组。Object.entries(obj):返回一个由对象的键值对组成的二维数组,每个内部项是 [key, value]。场景:遍历对象的属性,替代过去的 for...in。把对象转成数组,方便使用 map、filter 等数组方法处理。渲染键值对形式的详情表格。// 对象过滤示例:只保留指定属性 const obj = { name: '张三', age: 25, gender: '男' }; const picked = Object.fromEntries( Object.entries(obj).filter(([key]) => ['name', 'age'].includes(key)) ); // 结果: { name: '张三', age: 25 }3.2 Object.fromEntries() - 键值对转回对象作用:把键值对列表(如 Map 或 entries 返回的二维数组)转换成一个对象。是 Object.entries() 的逆操作。场景:解析URL查询参数。过滤/转换对象的属性,先转成 entries 处理,再转回来。把 Map 转成普通对象。3.3 Object.assign() - 对象合并/浅拷贝作用:将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。场景:对象合并。在现代JavaScript中,更推荐使用展开运算符 ... 来实现,因为它更简洁、更直观。const obj1 = { a: 1 }; const obj2 = { b: 2 }; const newObj = Object.assign({}, obj1, obj2); // 等价于 const newObj = { ...obj1, ...obj2 }; // newObj: { a: 1, b: 2 }四、实用操作符与技巧4.1 可选链 ?. 与空值合并 ??可选链 ?.:当访问嵌套对象的属性时,如果中间某个属性不存在(是 null 或 undefined),不会报错,直接返回 undefined。语法:obj?.prop、obj?.[expr]、func?.()。空值合并 ??:只有当左边的值是 null 或 undefined 时,才返回右边的默认值。与 || 的区别在于,?? 不会把 0、''、false 这些假值当成无效值。场景:安全地访问深层嵌套属性,给表单字段设置默认值。const user = { profile: { name: '张三' } }; console.log(user?.profile?.age); // undefined,不会报错 console.log(user?.address?.city); // undefined const value = 0; const defaultValue = value || 'default'; // 'default',因为 0 是假值 const safeDefault = value ?? 'default'; // 0,因为 0 不是 null/undefined4.2 剩余运算符与展开运算符 (...)剩余运算符 (Rest):用在函数参数或解构赋值中,将剩余的参数或属性收集到一个数组或对象中。展开运算符 (Spread):用在数组或对象字面量中,将一个可迭代对象(如数组)或对象“展开”到新的数组或对象中。// 解构:收集剩余属性 const currentNode = { children: [], id: 1, name: 'root', type: 'folder' }; const { children, ...editMenuData } = currentNode; // editMenuData 为 { id: 1, name: 'root', type: 'folder' } // 对象合并 const obj1 = { a: 1 }; const obj2 = { b: 2, ...obj1 }; // { b: 2, a: 1 } // 数组去重 const arr = [1, 2, 2, 3]; const uniqueArr = [...new Set(arr)]; // [1, 2, 3]4.3 深拷贝与浅拷贝浅拷贝:只拷贝对象的第一层属性,如果属性值是引用类型,则拷贝的是其引用。方法:展开运算符 ...、Object.assign()。深拷贝:递归地拷贝对象的所有层级,生成一个完全独立的副本。structuredClone():现代浏览器和Node.js推荐的原生API,支持大多数类型(如 Date、Map、Set)。JSON.parse(JSON.stringify(obj)):简单粗暴,但只适用于纯数据对象(不含函数、undefined、循环引用等)。Lodash _.cloneDeep:工业级最稳方案。五、函数式编程高级实战5.1 函数组合 pipe / compose - 数据流水线函数组合是将多个简单函数组合成一个复杂函数,数据从左到右(pipe)或从右到左(compose)依次流过每个函数。这是函数式编程的核心思想,能极大提升代码的可读性和可维护性。// 从左到右执行(pipe,更符合人类阅读习惯,企业更常用) const pipe = (...fns) => (initValue) => fns.reduce((acc, fn) => fn(acc), initValue); // 从右到左执行(compose,经典函数式写法) const compose = (...fns) => (initValue) => fns.reduceRight((acc, fn) => fn(acc), initValue); // 实战场景:后端接口数据全链路处理 const apiData = [ { goods_id: 1, goods_name: '手机', price: 5000, status: 1 }, { goods_id: 2, goods_name: '充电器', price: 50, status: 1 }, { goods_id: 3, goods_name: '耳机', price: 0, status: 0 } // 无效数据 ]; // 步骤1:过滤有效数据(价格>0 且 状态=1) const filterValid = (list) => list.filter(item => item.price > 0 && item.status === 1); // 步骤2:字段映射(后端字段转前端字段) const mapField = (list) => list.map(({ goods_id: id, goods_name: name, price }) => ({ id, name, price })); // 步骤3:按价格排序 const sortByPrice = (list) => [...list].sort((a, b) => a.price - b.price); // 步骤4:分组统计 const groupByPriceRange = (list) => list.reduce((acc, item) => { const range = item.price < 100 ? '低价' : item.price < 500 ? '中价' : '高价'; acc[range] = [...(acc[range] || []), item]; return acc; }, {}); // 把所有步骤串成流水线,代码线性、可读性拉满 const processGoodsData = pipe(filterValid, mapField, sortByPrice, groupByPriceRange); // 直接使用 const result = processGoodsData(apiData); console.log(result); // 输出: { 高价: [ { id: 1, name: '手机', price: 5000 } ], 低价: [ { id: 2, name: '充电器', price: 50 } ] }5.2 柯里化 (Currying)柯里化是将一个接受多个参数的函数转换为一系列接受单个参数的函数的技术。它允许你部分应用函数,预置一些参数。const curry = (fn) => (...args) => { if (args.length >= fn.length) { return fn(...args); } else { return curry(fn.bind(null, ...args)); } }; // 示例:一个计算价格的函数 const calcPrice = (price, tax, discount) => price * (1 + tax) * discount; const curriedCalc = curry(calcPrice); const priceWithTax = curriedCalc(100); // 预置价格100,返回一个新函数 const finalPrice = priceWithTax(0.1)(0.9); // 再传入税率0.1和折扣0.9,结果:99 console.log(finalPrice);六、异步编程高级实战6.1 带限流的异步并发控制 (Async Pool)解决批量请求并发过高导致服务端压力大或浏览器限制的问题。核心思想是始终维持最多 limit 个异步任务同时执行。/** * 异步任务并发控制 * @param {Array<Function>} tasks 返回 Promise 的任务函数数组 * @param {number} limit 最大并发数 * @returns {Promise<Array>} 所有任务的结果数组(按原顺序) */ const asyncPool = async (tasks, limit = 3) => { const results = []; const running = []; // 存放正在执行的 Promise for (const task of tasks) { // 包装任务,确保它是一个 Promise const promise = Promise.resolve().then(() => task()); results.push(promise); // 任务执行完后,从 running 数组中移除它 const finishPromise = promise.then(() => { const index = running.indexOf(finishPromise); if (index > -1) running.splice(index, 1); }); running.push(finishPromise); // 当并发数达到限制时,等待任意一个任务完成 if (running.length >= limit) { await Promise.race(running); } } // 等待所有任务完成 return Promise.all(results); }; // 使用示例 const createTask = (id, delay) => () => new Promise(resolve => { console.log(`任务 ${id} 开始`); setTimeout(() => { console.log(`任务 ${id} 结束`); resolve(id); }, delay); }); const tasks = [ createTask(1, 1000), createTask(2, 500), createTask(3, 800), createTask(4, 300), createTask(5, 200) ]; asyncPool(tasks, 2).then(results => console.log('所有任务结果:', results));七、数据处理与性能优化7.1 不可变数据更新 (Immutable Update)在React/Vue等框架开发中,直接修改状态可能导致视图不更新或难以追踪Bug。必须遵循“不修改原数据”的原则。改深层对象:用 ... 逐层展开复制。改数组元素:用 map,在需要修改的元素上返回新对象。删数组元素:用 filter,保留不需要删除的元素。加数组元素:用 [...arr, newItem]。// 原始状态 const state = { user: { name: '张三', profile: { age: 25, city: '北京' } }, posts: [{ id: 1, title: 'Post 1' }, { id: 2, title: 'Post 2' }] }; // 1. 修改深层属性 (user.profile.city 改为 '上海') const newState1 = { ...state, user: { ...state.user, profile: { ...state.user.profile, city: '上海' } } }; // 2. 修改数组元素 (将 id 为 2 的帖子标题改为 'New Title') const newState2 = { ...state, posts: state.posts.map(post => post.id === 2 ? { ...post, title: 'New Title' } : post ) }; // 3. 删除数组元素 (删除 id 为 1 的帖子) const newState3 = { ...state, posts: state.posts.filter(post => post.id !== 1) }; // 4. 添加数组元素 (在末尾添加新帖子) const newPost = { id: 3, title: 'Post 3' }; const newState4 = { ...state, posts: [...state.posts, newPost] };7.2 函数记忆化 (Memoization)缓存函数执行结果,对于相同参数,直接返回缓存值,避免重复计算。const memoize = (fn) => { const cache = new Map(); return (...args) => { const key = JSON.stringify(args); if (cache.has(key)) { return cache.get(key); } const result = fn(...args); cache.set(key, result); return result; }; }; // 一个昂贵的计算函数 const expensiveFibonacci = (n) => { if (n <= 1) return n; return expensiveFibonacci(n - 1) + expensiveFibonacci(n - 2); }; const memoizedFibonacci = memoize(expensiveFibonacci); console.time('第一次'); memoizedFibonacci(40); // 耗时较长 console.timeEnd('第一次'); console.time('第二次'); memoizedFibonacci(40); // 瞬间返回,从缓存读取 console.timeEnd('第二次');7.3 全局错误捕获在生产环境中,需要捕获未处理的Promise异常和同步错误,防止应用崩溃,并向用户提供友好的提示或记录错误日志。// 捕获未处理的 Promise rejection window.addEventListener('unhandledrejection', (event) => { console.error('未捕获的 Promise 错误:', event.reason); // 在这里可以调用后端接口上报错误,或向用户展示错误提示 alert('系统出错了,请刷新页面重试。'); }); // 捕获未处理的同步错误 window.addEventListener('error', (event) => { console.error('未捕获的同步错误:', event.error); // 上报错误 }); // React 中的错误边界 (Error Boundary) 概念类似7.4 防抖 (Debounce) 与节流 (Throttle)防抖 (Debounce):在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。适用于输入框搜索。节流 (Throttle):规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。适用于滚动事件、resize事件。// 防抖函数 function debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } // 节流函数 function throttle(func, limit) { let inThrottle; return function executedFunction(...args) { if (!inThrottle) { func(...args); inThrottle = true; setTimeout(() => inThrottle = false, limit); } }; }
2026年03月26日
4 阅读
0 评论
0 点赞
2026-03-24
JavaScript企业数据处理实用指南
在日常的企业级前端开发中,我们绝大多数时间都在和数据打交道:处理接口返回的列表、转换表单数据、优化高频事件、构建树形菜单... 熟练掌握这些数据处理技巧,能让你的代码更简洁、高效、易维护。本文将系统梳理企业开发中最常用的内置函数与实用算法,针对每一个知识点,都会从帮助理解、实战例子、如何记忆、什么时候用四个维度帮你彻底掌握。一、数组核心处理函数数组是前端数据处理最常用的数据结构,ES5 + 提供的一系列函数式方法,彻底改变了我们过去写for循环的模式。1.1 every:全真才为真帮助理解检查数组中所有元素是否都满足指定条件。只要发现有一个元素不满足,就会立即停止遍历,直接返回false;只有全部满足,才返回true。实战例子// 表单校验:检查所有输入项是否都通过了校验const formFields = [ { name: 'username', valid: true, value: '张三' }, { name: 'email', valid: true, value: 'zhangsan@example.com' }, { name: 'phone', valid: false, value: '123' }];// 只有所有字段都valid为true,表单才可以提交const isFormValid = formFields.every(field => field.valid);console.log(isFormValid); // false,因为phone字段校验失败// 权限判断:用户是否拥有访问某个功能的所有权限const requiredPerms = ['user:read', 'user:edit'];const userPerms = ['user:read', 'user:edit', 'admin'];const hasAccess = requiredPerms.every(perm => userPerms.includes(perm));console.log(hasAccess); // true如何记忆every的英文意思就是 “每一个”,你可以这么记:每一个都要满足条件,才算通过。就像期末考试,必须所有科目都及格,才算全部通过。什么时候用表单整体校验,判断所有字段是否合法权限校验,判断用户是否拥有全部所需权限批量数据的合规性检查1.2 some:一真即为真帮助理解检查数组中是否存在至少一个元素满足指定条件。只要找到第一个满足条件的元素,就会立即停止遍历,直接返回true;如果全部都不满足,才返回false。实战例子// 任务列表:判断是否有未完成的任务const tasks = [ { id: 1, name: '写文档', done: true }, { id: 2, name: '改Bug', done: false }, { id: 3, name: '开例会', done: true }];const hasPendingTask = tasks.some(task => !task.done);console.log(hasPendingTask); // true,说明还有未完成的任务// 搜索功能:判断列表中是否存在包含关键词的条目const keywords = '张三';const hasMatch = users.some(user => user.name.includes(keywords));如何记忆some的英文意思是 “一些、若干”,你可以这么记:只要有一些(哪怕一个)满足条件,就算通过。就像期末考试,只要有一科及格,就不算全挂科。什么时候用判断列表中是否存在未完成 / 异常的状态项快速判断搜索是否命中结果权限判断:只要有一个相关权限就能访问1.3 filter:筛选过滤,生成新数组帮助理解遍历数组,把所有满足条件的元素筛选出来,返回一个全新的数组。原数组不会被修改。实战例子// 过滤掉无效的空字符串const strings = ["苹果", "", "香蕉", " ", "橙子", null];const validStrings = strings.filter(s => s && s.trim());// 结果:["苹果", "香蕉", "橙子"]// React中瀑布流布局:筛选出偶数索引的图片放到左列const leftColumnImages = images .filter((_, index) => index % 2 === 0) .map(img => );如何记忆filter的意思就是 “过滤”,顾名思义,就是把符合条件的留下来,不符合的过滤掉。什么时候用数据筛选,比如根据条件过滤列表数据清理无效数据,过滤掉空值、无效项数据分类,把数组分成不同的子集1.4 map:一对一映射,生成新数组帮助理解对数组的每一个元素执行同一个转换函数,返回一个长度和原数组完全一致的全新数组。原数组不会被修改。实战例子// 把字符串数组转成数字数组const strNums = ['1', '2', '3', '4'];const numbers = strNums.map(Number);// 结果:[1, 2, 3, 4]// React中渲染列表:把用户数据转成JSX {users.map(user => ( {user.name} - {user.age} ))} // 接口数据适配:把后端返回的字段名转成前端需要的const apiData = [{ user_name: '张三', user_age: 25 }];const frontData = apiData.map(item => ({ name: item.user_name, age: item.user_age}));如何记忆map的意思是 “映射”,就是一一对应,原数组有多少个,新数组就有多少个,每个元素都做一次转换。什么时候用数据格式转换,比如字段名映射、类型转换渲染列表,把数据转成 UI 组件批量修改数组中的每一项数据1.5 reduce:累计器,万能的汇总工具帮助理解这是数组方法中最强大的一个,它可以把数组中的所有元素,通过一个累加函数,最终汇总成一个结果。这个结果可以是数字、对象、数组,任何类型都可以。它的两个核心参数:回调函数:(累加器acc, 当前元素curr) => 新的累加器累加器的初始值实战例子// 1. 基础求和const numbers = [1, 2, 3, 4];const sum = numbers.reduce((acc, curr) => acc + curr, 0);console.log(sum); // 10// 2. 按分类分组统计const orderItems = [ { category: '食品', price: 20 }, { category: '服装', price: 100 }, { category: '食品', price: 30 }];// 按分类统计总销售额const totalByCategory = orderItems.reduce((acc, item) => { acc[item.category] = (acc[item.category] || 0) + item.price; return acc;}, {});// 结果:{ 食品: 50, 服装: 100 }// 3. 数组扁平化const nestedArr = [[1,2], [3,4], [5,6]];const flatArr = nestedArr.reduce((acc, curr) => acc.concat(curr), []);// 结果:[1,2,3,4,5,6]如何记忆reduce的意思是 “归约、缩减”,就是把一个数组,缩减成一个最终的结果。你可以这么记:只要你需要把多个元素汇总成一个结果,就用 reduce。什么时候用聚合统计:求和、求平均值、统计次数数据分组:把列表按某个字段分成不同的组复杂的数据变换:比如数组转对象、多层数据处理函数式管道:把多个处理步骤串起来1.6 find & findIndex:精准查找帮助理解find:找到数组中第一个满足条件的元素,找不到返回undefinedfindIndex:找到数组中第一个满足条件的元素的索引,找不到返回-1和indexOf的区别是,它们支持自定义复杂的判断条件,而不是只能判断严格相等。实战例子const users = [{ id: 1, name: '张三' },{ id: 2, name: '李四' },{ id: 3, name: '王五' }];// 查找id为2的用户const targetUser = users.find(user => user.id === 2);console.log(targetUser); // { id: 2, name: '李四' }// 查找名字是王五的用户的索引const targetIndex = users.findIndex(user => user.name === '王五');console.log(targetIndex); // 2如何记忆顾名思义,find就是找元素,findIndex就是找元素的索引,找到第一个就停。什么时候用根据 ID 查找特定的用户 / 数据项查找符合特定条件的第一个元素找到元素的位置,用于后续的删除 / 更新操作1.7 flat & flatMap:数组扁平化帮助理解flat:把嵌套的数组展开,参数是展开的深度,传Infinity可以展开所有层级flatMap:相当于先执行map,再对结果执行flat(1),一步到位实战例子// 展开多层嵌套数组const nestedArr = [1, [2, [3, [4]]], 5];const flatArr = nestedArr.flat(Infinity);console.log(flatArr); // [1, 2, 3, 4, 5]// flatMap:处理每行的单词const lines = ["Hello world", "JavaScript is good"];const words = lines.flatMap(line => line.split(' '));// 结果:["Hello", "world", "JavaScript", "is", "good"]如何记忆flat就是 “拍平”,把嵌套的数组拍平成一维的。flatMap就是 map + 拍平,一步搞定。什么时候用处理后端返回的嵌套数组结构处理文本拆分,比如把多行文本拆成单词列表处理树形结构的扁平化数组方法核心对比表方法返回值是否短路典型用途forEachundefined❌执行副作用(打印、修改外部变量)everyboolean✅检查所有元素是否都满足条件someboolean✅检查是否存在至少一个满足条件的元素filter新数组❌筛选出符合条件的元素map新数组❌一对一转换每个元素reduce累积值❌汇总、分组、聚合统计find元素 /undefined✅查找第一个符合条件的元素findIndex索引 /-1✅查找第一个符合条件的元素的位置flat新数组❌展开嵌套数组二、对象核心处理函数除了数组,对象也是我们最常用的数据结构,ES6 + 提供了一系列非常实用的对象处理方法。2.1 Object.keys/values/entries:对象遍历三剑客帮助理解这三个方法把对象转换成数组,方便我们用上面的数组方法来处理:Object.keys(obj):返回对象的所有键名组成的数组Object.values(obj):返回对象的所有值组成的数组Object.entries(obj):返回对象的所有键值对组成的二维数组,每个项是[key, value]实战例子const user = {name: '张三',age: 25,role: 'admin'};// 遍历键名console.log(Object.keys(user)); // ["name", "age", "role"]// 遍历值console.log(Object.values(user)); // ["张三", 25, "admin"]// 遍历键值对,用来渲染表格const tableRows = Object.entries(user).map(([key, value]) => ( <td>{key}</td> <td>{value}</td>));如何记忆顾名思义,keys 拿键,values 拿值,entries 拿键值对。什么时候用遍历对象的属性,替代过去的for...in把对象转成数组,方便用数组的 filter、map 等方法处理渲染键值对形式的详情表格2.2 Object.fromEntries:键值对转回对象帮助理解它是Object.entries的逆操作,把一个键值对的二维数组,转回一个对象。实战例子// 1. 把URL参数转成对象const search = '?name=张三&age=25';const params = new URLSearchParams(search);const query = Object.fromEntries(params);console.log(query); // { name: '张三', age: '25' }// 2. 过滤对象的属性const obj = { a: 1, b: 2, c: 3, d: 4 };// 过滤掉值小于3的属性const filteredObj = Object.fromEntries( Object.entries(obj).filter(([key, value]) => value >= 3));console.log(filteredObj); // { c: 3, d: 4 }如何记忆fromEntries就是 “从 entries 来”,把 entries 的结果转回去。什么时候用解析 URL 查询参数过滤 / 转换对象的属性,先转成 entries 处理,再转回来把 Map 转成普通对象2.3 可选链?.与空值合并??帮助理解可选链?.:访问嵌套对象的属性时,如果中间某个属性不存在,不会报错,直接返回undefined空值合并??:只有当左边的值是null或undefined时,才返回右边的默认值,不会把0、''、false这些假值当成无效值实战例子// 可选链:避免层层判断const user = { profile: { address: { city: '北京' } } };// 传统写法const street = user.address && user.address.street;// 可选链写法const street2 = user.profile?.address?.street; // undefined,不会报错// 空值合并:精准的默认值const age = 0;// 错误写法:||会把0当成假值const wrongAge = age || 18; // 18,不对,0是合法的年龄// 正确写法:??只处理null/undefinedconst rightAge = age ?? 18; // 0,正确如何记忆?.就是 “如果前面的不存在,就别往下找了,直接返回 undefined”??就是 “只有当左边是空的(null/undefined),才用右边的默认值”什么时候用访问嵌套对象的深层属性,防止报错给表单字段设置默认值,避免覆盖合法的假值(0、false)安全的调用可能不存在的函数:api?.getData?.()三、企业级常用数据处理算法除了内置函数,企业开发中还有一些高频使用的自定义算法,用来解决特定的业务问题。3.1 防抖(Debounce):只认最后一次触发帮助理解防抖的核心逻辑是:触发事件后,等待 n 秒,如果 n 秒内没有再次触发,才执行函数;如果触发了,就重新计时。就像电梯:电梯门要关了,这时候有人进来,电梯就会重新倒计时关门,直到没人进来了,才真正关门。实战例子// 防抖函数实现function debounce(fn, delay = 300) { let timer = null; return function(...args) {// 每次触发,清除之前的定时器 clearTimeout(timer); // 重新设置定时器 timer = setTimeout(() => { fn.apply(this, args); }, delay);};}// 实战:搜索框输入联想function search(keyword) { // 调用接口搜索 console.log(搜索:${keyword});}// 包装成防抖函数,用户停止输入500ms后才执行搜索const debouncedSearch = debounce(search, 500);// 绑定输入事件input.addEventListener('input', (e) => { debouncedSearch(e.target.value);});如何记忆防抖,就是 “防止抖动”,用户连续的操作就像抖动,我们只认最后一次稳定的操作。什么时候用搜索框输入联想,避免每次输入都发请求按钮防重复点击,防止用户连续点击提交多次窗口resize事件,避免窗口大小改变时频繁触发重排滚动结束后触发的操作3.2 节流(Throttle):固定频率执行帮助理解节流的核心逻辑是:规定时间内,无论触发多少次,函数只执行一次。就像水龙头:不管你开多大,水都只能每隔固定时间滴一滴,不会因为你开的大就滴的更快。实战例子// 节流函数实现function throttle(fn, limit = 300) { let inThrottle = false; return function(...args) {// 如果不在冷却期,才执行 if (!inThrottle) { fn.apply(this, args); inThrottle = true; // 冷却期结束后重置 setTimeout(() => { inThrottle = false; }, limit); }};}// 实战:页面滚动懒加载function handleScroll() { // 检查滚动位置,加载新的内容 console.log('处理滚动...');}// 包装成节流函数,每200ms最多执行一次const throttledScroll = throttle(handleScroll, 200);window.addEventListener('scroll', throttledScroll);如何记忆节流,就是 “节制流量”,把高频的触发,稀释成固定频率的执行,保证不会太频繁。什么时候用页面滚动事件,比如懒加载、滚动位置监听鼠标移动事件游戏中的射击冷却高频的 DOM 事件处理3.3 扁平数组转树形结构帮助理解后端经常会返回一个扁平的列表,每个元素有id和parentId,前端需要把它转换成带children的树形结构,用来渲染菜单、部门树、分类树等组件。最优的实现是用 Map 做映射,时间复杂度 O (n),比递归的 O (n²) 高效很多。实战例子/**扁平数组转树形结构@param {Array} items 扁平数组@param {Object} config 配置,兼容不同的字段名 */function arrayToTree(items, config = {}) { const { id = 'id', pid = 'parentId', children = 'children' } = config; // 用Map存所有节点,方便快速查找 const nodeMap = new Map(); const result = [];// 先给每个节点加上children属性,并存到Map里 for (const item of items) {nodeMap.set(item[id], { ...item, [children]: [] });}// 遍历每个节点,找到它的父节点,把自己塞到父节点的children里 for (const item of items) {const node = nodeMap.get(item[id]); if (item[pid] === null || item[pid] === 0) { // 根节点,直接放到结果里 result.push(node); } else { // 找到父节点 const parent = nodeMap.get(item[pid]); if (parent) { parent[children].push(node); } }}return result;}// 实战:后端返回的部门列表const flatDepartments = [ { id: 1, name: '总公司', parentId: null }, { id: 2, name: '研发部', parentId: 1 }, { id: 3, name: '产品部', parentId: 1 }, { id: 4, name: '前端组', parentId: 2 }, { id: 5, name: '后端组', parentId: 2 },];const tree = arrayToTree(flatDepartments);console.log(tree);/* 结果:[ {id: 1, name: '总公司', parentId: null, children: [ { id: 2, name: '研发部', parentId: 1, children: [...] }, { id: 3, name: '产品部', parentId: 1, children: [] } ]}]*/如何记忆两步走:先把所有节点存到 Map 里,建立 ID 到节点的映射每个节点找自己的爸爸,把自己塞到爸爸的孩子里最后把没有爸爸的根节点拿出来什么时候用渲染 ElementUI/AntD 的树形组件构建系统菜单、权限菜单处理商品分类、部门组织架构3.4 深拷贝:完全复制对象帮助理解引用类型的赋值只是复制了引用,修改新对象会影响原对象。深拷贝会创建一个完全独立的新对象,无论嵌套多深,两个对象都完全隔离。我们需要处理特殊类型(Date、RegExp)和循环引用,避免 JSON 方法的缺陷。实战例子/**完整的深拷贝实现,支持循环引用、Date、RegExp等 */function deepClone(obj, map = new WeakMap()) { // 基本类型,直接返回 if (obj === null || typeof obj !== 'object') {return obj;}// 处理循环引用:如果已经拷贝过这个对象,直接返回缓存的副本 if (map.has(obj)) {return map.get(obj);}// 处理特殊对象类型 if (obj instanceof Date) {return new Date(obj.getTime());} if (obj instanceof RegExp) {return new RegExp(obj.source, obj.flags);}// 创建新的对象/数组 const cloneObj = Array.isArray(obj) ? [] : {}; // 缓存已经拷贝的对象 map.set(obj, cloneObj);// 递归拷贝每个属性 for (const key in obj) {if (obj.hasOwnProperty(key)) { cloneObj[key] = deepClone(obj[key], map); }}return cloneObj;}// 实战:编辑表单时,不修改原数据const originalUser = await api.getUserInfo();// 深拷贝一份,用来修改表单,不会影响原数据const formData = deepClone(originalUser);// 修改表单数据formData.name = '新名字';// originalUser不会被修改如何记忆递归遍历每个属性:基本类型直接返回特殊类型(Date、正则)单独处理用 WeakMap 存已经拷贝过的对象,防止循环引用对象 / 数组就递归继续挖什么时候用表单编辑,需要修改数据但不想影响原接口数据Redux/Vuex 中的不可变状态更新需要完全隔离两个对象的场景3.5 表格数据处理流水线企业后台系统中最常见的场景:对表格数据做过滤→排序→分页的流水线处理,用函数式的链式调用,一行搞定。实战例子function processTableData(data, options) { const { filterKey, sortKey, sortAsc, page, pageSize } = options; return data// 1. 过滤:模糊搜索名字 .filter(item => item.name.includes(filterKey)) // 2. 排序:根据指定字段排序 .sort((a, b) => { const diff = a[sortKey] - b[sortKey]; return sortAsc ? diff : -diff; }) // 3. 分页:截取当前页的数据 .slice((page - 1) * pageSize, page * pageSize);}// 使用const tableData = processTableData(rawData, { filterKey: '张', sortKey: 'age', sortAsc: false, page: 1, pageSize: 10});什么时候用后台管理系统的表格数据处理前端本地的列表筛选排序分页总结企业开发中的数据处理,核心就是用好原生的函数式方法,配合常用的通用算法,避免写大量的循环和判断。记住一个简单的选择口诀:想判断?every(全对)/some(有对的)想筛选?filter想转换?map想汇总?reduce想查找?find/findIndex高频事件?防抖(最后一次)/ 节流(固定频率)树形数据?扁平转树改对象怕影响原数据?深拷贝掌握这些,你就能应对 90% 以上的企业级数据处理场景了。
2026年03月24日
9 阅读
0 评论
0 点赞
2026-03-21
Java 8 实战开发与 Spring Boot 学习笔记
目录第一部分:Java 8 核心特性1. Lambda 表达式2. 函数式接口3. Stream API4. Optional 类5. 日期时间 API6. 接口默认方法与静态方法第二部分:Spring Boot 实战开发1. Spring Boot 快速入门2. 自动配置原理3. Web 开发4. 数据访问5. 安全认证6. 日志与监控7. 测试8. 部署与运维第一部分:Java 8 核心特性1. Lambda 表达式1.1 什么是 Lambda 表达式Lambda 表达式是 Java 8 引入的一种简洁的函数式编程语法,允许将函数作为方法参数传递。语法格式:(parameters) -> expression 或 (parameters) -> { statements; }1.2 Lambda 表达式示例// 传统方式 Runnable r1 = new Runnable() { @Override public void run() { System.out.println("Hello World!"); } }; // Lambda 方式 Runnable r2 = () -> System.out.println("Hello World!"); // 带参数的 Lambda Comparator<Integer> cmp1 = (a, b) -> a.compareTo(b); // 代码块形式 Comparator<Integer> cmp2 = (a, b) -> { System.out.println("Comparing: " + a + " and " + b); return a.compareTo(b); };1.3 变量作用域Lambda 表达式可以访问外部变量,但外部变量必须是 final 或 effectively final 的:String prefix = "Hello, "; List<String> names = Arrays.asList("Alice", "Bob", "Charlie"); // 正确:prefix 是 effectively final names.forEach(name -> System.out.println(prefix + name)); // 错误:不能修改外部变量 // prefix = "Hi, "; // 编译错误2. 函数式接口2.1 函数式接口定义函数式接口是只包含一个抽象方法的接口,使用 @FunctionalInterface 注解标记。@FunctionalInterface interface MyFunction { void apply(String s); }2.2 Java 8 内置函数式接口接口抽象方法描述Predicate<T>boolean test(T t)断言型接口Function<T, R>R apply(T t)函数型接口Consumer<T>void accept(T t)消费型接口Supplier<T>T get()供给型接口2.3 内置函数式接口示例// Predicate:判断 Predicate<String> isLongerThan5 = s -> s.length() > 5; System.out.println(isLongerThan5.test("Hello")); // false // Function:转换 Function<String, Integer> stringToLength = s -> s.length(); System.out.println(stringToLength.apply("Hello")); // 5 // Consumer:消费 Consumer<String> printer = s -> System.out.println(s); printer.accept("Hello World"); // Supplier:供给 Supplier<Double> randomSupplier = () -> Math.random(); System.out.println(randomSupplier.get());2.4 方法引用方法引用是 Lambda 表达式的简写形式:// 静态方法引用 Function<String, Integer> parseInt = Integer::parseInt; // 实例方法引用 List<String> list = Arrays.asList("a", "b", "c"); list.forEach(System.out::println); // 对象方法引用 String str = "Hello"; Predicate<String> startsWith = str::startsWith; // 构造方法引用 Supplier<List<String>> listSupplier = ArrayList::new;3. Stream API3.1 Stream 概述Stream 是 Java 8 中处理集合的关键抽象概念,用于对集合进行函数式操作。Stream 操作流程:创建 Stream中间操作(返回 Stream)终端操作(产生结果)3.2 创建 Stream// 从集合创建 List<String> list = Arrays.asList("a", "b", "c"); Stream<String> stream1 = list.stream(); // 从数组创建 String[] array = {"a", "b", "c"}; Stream<String> stream2 = Arrays.stream(array); // 使用 Stream.of Stream<String> stream3 = Stream.of("a", "b", "c"); // 无限流 Stream<Integer> stream4 = Stream.iterate(0, n -> n + 2); // 偶数 Stream<Double> stream5 = Stream.generate(Math::random);3.3 中间操作List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); // filter:过滤 numbers.stream() .filter(n -> n % 2 == 0) .forEach(System.out::println); // map:转换 numbers.stream() .map(n -> n * n) .forEach(System.out::println); // sorted:排序 numbers.stream() .sorted((a, b) -> b - a) .forEach(System.out::println); // distinct:去重 List<Integer> duplicates = Arrays.asList(1, 2, 2, 3, 3, 3); duplicates.stream() .distinct() .forEach(System.out::println); // limit:限制数量 numbers.stream() .limit(5) .forEach(System.out::println);3.4 终端操作// collect:收集 List<String> words = Arrays.asList("apple", "banana", "cherry"); List<String> result = words.stream() .filter(w -> w.length() > 5) .collect(Collectors.toList()); // count:计数 long count = words.stream().count(); // reduce:归约 Optional<Integer> sum = numbers.stream().reduce((a, b) -> a + b); // forEach:遍历 words.stream().forEach(System.out::println); // anyMatch / allMatch / noneMatch boolean hasLongWord = words.stream().anyMatch(w -> w.length() > 10); boolean allShort = words.stream().allMatch(w -> w.length() < 10); // findFirst / findAny Optional<String> first = words.stream().findFirst(); Optional<String> any = words.stream().findAny();3.5 Collectors 工具类// 转 List List<String> list = words.stream().collect(Collectors.toList()); // 转 Set Set<String> set = words.stream().collect(Collectors.toSet()); // 转 Map Map<Integer, String> map = words.stream() .collect(Collectors.toMap(String::length, w -> w)); // 分组 Map<Integer, List<String>> groupByLength = words.stream() .collect(Collectors.groupingBy(String::length)); // 分区 Map<Boolean, List<String>> partition = words.stream() .collect(Collectors.partitioningBy(w -> w.length() > 5)); // 统计 IntSummaryStatistics stats = numbers.stream() .collect(Collectors.summarizingInt(Integer::intValue)); System.out.println("Count: " + stats.getCount()); System.out.println("Sum: " + stats.getSum()); System.out.println("Avg: " + stats.getAverage()); System.out.println("Max: " + stats.getMax()); System.out.println("Min: " + stats.getMin()); // 连接字符串 String joined = words.stream().collect(Collectors.joining(", "));3.6 并行流// 顺序流 long start = System.currentTimeMillis(); numbers.stream().forEach(n -> { try { Thread.sleep(10); } catch (InterruptedException e) {} }); System.out.println("Sequential: " + (System.currentTimeMillis() - start) + "ms"); // 并行流 start = System.currentTimeMillis(); numbers.parallelStream().forEach(n -> { try { Thread.sleep(10); } catch (InterruptedException e) {} }); System.out.println("Parallel: " + (System.currentTimeMillis() - start) + "ms");4. Optional 类4.1 Optional 概述Optional 是一个容器对象,用于优雅地处理空值(NullPointerException)。4.2 创建 Optional// 创建非空 Optional Optional<String> opt1 = Optional.of("Hello"); // 创建允许空的 Optional Optional<String> opt2 = Optional.ofNullable(null); // 创建空 Optional Optional<String> opt3 = Optional.empty();4.3 Optional 常用方法Optional<String> optional = Optional.of("Hello"); // isPresent:判断是否有值 if (optional.isPresent()) { System.out.println(optional.get()); } // ifPresent:如果有值则执行 optional.ifPresent(s -> System.out.println(s)); // orElse:如果为空则返回默认值 String result1 = optional.orElse("Default"); // orElseGet:如果为空则调用供给函数 String result2 = optional.orElseGet(() -> "Default"); // orElseThrow:如果为空则抛出异常 String result3 = optional.orElseThrow(IllegalArgumentException::new); // map:转换 Optional<Integer> length = optional.map(String::length); // flatMap:扁平转换 Optional<String> upper = optional.flatMap(s -> Optional.of(s.toUpperCase())); // filter:过滤 Optional<String> filtered = optional.filter(s -> s.length() > 5);4.4 Optional 最佳实践// 传统方式 String name = user != null ? user.getName() : "Unknown"; // Optional 方式 String name = Optional.ofNullable(user) .map(User::getName) .orElse("Unknown"); // 多层嵌套 String city = Optional.ofNullable(user) .map(User::getAddress) .map(Address::getCity) .orElse("Unknown");5. 日期时间 API5.1 新日期时间 API 概述Java 8 引入了全新的日期时间 API,位于 java.time 包下。类描述LocalDate日期(年-月-日)LocalTime时间(时:分:秒)LocalDateTime日期时间ZonedDateTime带时区的日期时间Instant时间戳Duration时间间隔(秒/纳秒)Period日期间隔(年/月/日)DateTimeFormatter日期格式化5.2 LocalDate / LocalTime / LocalDateTime// LocalDate LocalDate today = LocalDate.now(); LocalDate date = LocalDate.of(2024, 1, 1); LocalDate parsedDate = LocalDate.parse("2024-01-01"); int year = date.getYear(); Month month = date.getMonth(); int dayOfMonth = date.getDayOfMonth(); DayOfWeek dayOfWeek = date.getDayOfWeek(); // 日期运算 LocalDate tomorrow = today.plusDays(1); LocalDate lastWeek = today.minusWeeks(1); // LocalTime LocalTime now = LocalTime.now(); LocalTime time = LocalTime.of(14, 30, 0); LocalTime parsedTime = LocalTime.parse("14:30:00"); // LocalDateTime LocalDateTime nowDateTime = LocalDateTime.now(); LocalDateTime dateTime = LocalDateTime.of(2024, 1, 1, 14, 30);5.3 Instant 与 Duration / Period// Instant:时间戳 Instant now = Instant.now(); Instant timestamp = Instant.ofEpochMilli(System.currentTimeMillis()); // Duration:时间间隔 Instant start = Instant.now(); Thread.sleep(1000); Instant end = Instant.now(); Duration duration = Duration.between(start, end); System.out.println("Seconds: " + duration.getSeconds()); System.out.println("Millis: " + duration.toMillis()); // Period:日期间隔 LocalDate date1 = LocalDate.of(2024, 1, 1); LocalDate date2 = LocalDate.of(2024, 12, 31); Period period = Period.between(date1, date2); System.out.println("Years: " + period.getYears()); System.out.println("Months: " + period.getMonths()); System.out.println("Days: " + period.getDays());5.4 日期格式化与解析// 预定义格式 DateTimeFormatter isoDate = DateTimeFormatter.ISO_LOCAL_DATE; String dateStr = LocalDate.now().format(isoDate); LocalDate date = LocalDate.parse(dateStr, isoDate); // 自定义格式 DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); String formatted = LocalDateTime.now().format(formatter); LocalDateTime parsed = LocalDateTime.parse("2024-01-01 14:30:00", formatter);5.5 时区处理// ZonedDateTime ZonedDateTime nowBeijing = ZonedDateTime.now(ZoneId.of("Asia/Shanghai")); ZonedDateTime nowNewYork = ZonedDateTime.now(ZoneId.of("America/New_York")); // 时区转换 ZonedDateTime beijingTime = ZonedDateTime.of(2024, 1, 1, 14, 30, 0, 0, ZoneId.of("Asia/Shanghai")); ZonedDateTime newYorkTime = beijingTime.withZoneSameInstant(ZoneId.of("America/New_York"));6. 接口默认方法与静态方法6.1 默认方法interface MyInterface { void abstractMethod(); default void defaultMethod() { System.out.println("Default method"); } } class MyClass implements MyInterface { @Override public void abstractMethod() { System.out.println("Abstract method implementation"); } } public class Main { public static void main(String[] args) { MyClass obj = new MyClass(); obj.abstractMethod(); obj.defaultMethod(); } }6.2 静态方法interface MathUtil { static int add(int a, int b) { return a + b; } static int multiply(int a, int b) { return a * b; } } public class Main { public static void main(String[] args) { System.out.println(MathUtil.add(1, 2)); System.out.println(MathUtil.multiply(3, 4)); } }第二部分:Spring Boot 实战开发1. Spring Boot 快速入门1.1 什么是 Spring BootSpring Boot 是一个快速开发框架,基于 Spring 框架,简化了 Spring 应用的初始搭建和开发过程。核心特性:自动配置独立运行(内嵌 Tomcat)起步依赖(Starters)生产级特性(监控、健康检查等)1.2 创建第一个 Spring Boot 项目使用 Spring Initializr 创建:访问 https://start.spring.io/,选择:Project: Maven 或 GradleLanguage: JavaSpring Boot: 最新稳定版本Dependencies: Spring Webpom.xml 示例:<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.2.0</version> <relativePath/> </parent> <groupId>com.example</groupId> <artifactId>demo</artifactId> <version>1.0.0</version> <name>demo</name> <description>Demo project for Spring Boot</description> <properties> <java.version>17</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>1.3 主类与启动package com.example.demo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }1.4 第一个 REST 控制器package com.example.demo.controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/api") public class HelloController { @GetMapping("/hello") public String hello() { return "Hello, Spring Boot!"; } }1.5 配置文件 application.ymlserver: port: 8080 servlet: context-path: /demo spring: application: name: demo-service2. 自动配置原理2.1 @SpringBootApplication 注解@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @SpringBootConfiguration @EnableAutoConfiguration @ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class), @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) }) public @interface SpringBootApplication { }包含三个核心注解:@SpringBootConfiguration:标记为配置类@EnableAutoConfiguration:启用自动配置@ComponentScan:组件扫描2.2 自动配置流程Spring Boot 启动时加载 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports根据条件注解(@ConditionalOnClass、@ConditionalOnMissingBean 等)决定是否加载配置自动配置类创建所需的 Bean2.3 自定义自动配置创建自定义自动配置类:@Configuration @ConditionalOnClass(MyService.class) @EnableConfigurationProperties(MyProperties.class) public class MyAutoConfiguration { @Bean @ConditionalOnMissingBean public MyService myService(MyProperties properties) { return new MyService(properties.getMessage()); } }配置属性类:@ConfigurationProperties(prefix = "my.service") public class MyProperties { private String message = "default message"; public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } }3. Web 开发3.1 RESTful API 开发@RestController @RequestMapping("/api/users") public class UserController { private final UserService userService; @Autowired public UserController(UserService userService) { this.userService = userService; } @GetMapping public ResponseEntity<List<User>> getAllUsers() { List<User> users = userService.findAll(); return ResponseEntity.ok(users); } @GetMapping("/{id}") public ResponseEntity<User> getUserById(@PathVariable Long id) { return userService.findById(id) .map(ResponseEntity::ok) .orElse(ResponseEntity.notFound().build()); } @PostMapping public ResponseEntity<User> createUser(@Valid @RequestBody User user) { User createdUser = userService.save(user); URI location = URI.create("/api/users/" + createdUser.getId()); return ResponseEntity.created(location).body(createdUser); } @PutMapping("/{id}") public ResponseEntity<User> updateUser(@PathVariable Long id, @Valid @RequestBody User user) { return userService.findById(id) .map(existingUser -> { user.setId(id); User updatedUser = userService.save(user); return ResponseEntity.ok(updatedUser); }) .orElse(ResponseEntity.notFound().build()); } @DeleteMapping("/{id}") public ResponseEntity<Void> deleteUser(@PathVariable Long id) { if (userService.existsById(id)) { userService.deleteById(id); return ResponseEntity.noContent().build(); } return ResponseEntity.notFound().build(); } }3.2 参数验证public class User { @NotBlank(message = "Name is required") @Size(min = 2, max = 50, message = "Name must be between 2 and 50 characters") private String name; @Email(message = "Invalid email format") @NotBlank(message = "Email is required") private String email; @Min(value = 18, message = "Age must be at least 18") private Integer age; // getters and setters }3.3 全局异常处理@RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(ResourceNotFoundException.class) public ResponseEntity<ErrorResponse> handleResourceNotFound(ResourceNotFoundException ex) { ErrorResponse error = new ErrorResponse(HttpStatus.NOT_FOUND.value(), ex.getMessage()); return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error); } @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity<ErrorResponse> handleValidationException(MethodArgumentNotValidException ex) { List<String> errors = ex.getBindingResult() .getFieldErrors() .stream() .map(error -> error.getField() + ": " + error.getDefaultMessage()) .collect(Collectors.toList()); ErrorResponse error = new ErrorResponse(HttpStatus.BAD_REQUEST.value(), "Validation failed", errors); return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(error); } @ExceptionHandler(Exception.class) public ResponseEntity<ErrorResponse> handleGenericException(Exception ex) { ErrorResponse error = new ErrorResponse( HttpStatus.INTERNAL_SERVER_ERROR.value(), "An unexpected error occurred" ); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error); } }3.4 文件上传@RestController @RequestMapping("/api/files") public class FileController { @PostMapping("/upload") public ResponseEntity<String> uploadFile(@RequestParam("file") MultipartFile file) { try { String fileName = file.getOriginalFilename(); Path path = Paths.get("uploads/" + fileName); Files.copy(file.getInputStream(), path, StandardCopyOption.REPLACE_EXISTING); return ResponseEntity.ok("File uploaded successfully: " + fileName); } catch (IOException e) { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body("Failed to upload file: " + e.getMessage()); } } @GetMapping("/download/{filename}") public ResponseEntity<Resource> downloadFile(@PathVariable String filename) { Path path = Paths.get("uploads/" + filename); try { Resource resource = new UrlResource(path.toUri()); if (resource.exists() && resource.isReadable()) { return ResponseEntity.ok() .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + resource.getFilename() + "\"") .body(resource); } else { return ResponseEntity.notFound().build(); } } catch (MalformedURLException e) { return ResponseEntity.badRequest().build(); } } }4. 数据访问4.1 Spring Data JPA实体类:@Entity @Table(name = "users") public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false, unique = true) private String email; @Column(nullable = false) private String name; @Column(nullable = false) private String password; @Column(name = "created_at") private LocalDateTime createdAt; @Column(name = "updated_at") private LocalDateTime updatedAt; @PrePersist protected void onCreate() { createdAt = LocalDateTime.now(); updatedAt = LocalDateTime.now(); } @PreUpdate protected void onUpdate() { updatedAt = LocalDateTime.now(); } // getters and setters }Repository 接口:public interface UserRepository extends JpaRepository<User, Long> { Optional<User> findByEmail(String email); List<User> findByNameContainingIgnoreCase(String name); boolean existsByEmail(String email); @Query("SELECT u FROM User u WHERE u.createdAt > :since") List<User> findUsersCreatedAfter(@Param("since") LocalDateTime since); @Modifying @Query("UPDATE User u SET u.password = :password WHERE u.id = :id") int updatePassword(@Param("id") Long id, @Param("password") String password); }Service 层:@Service @Transactional public class UserService { private final UserRepository userRepository; @Autowired public UserService(UserRepository userRepository) { this.userRepository = userRepository; } public List<User> findAll() { return userRepository.findAll(); } public Optional<User> findById(Long id) { return userRepository.findById(id); } public User save(User user) { if (userRepository.existsByEmail(user.getEmail())) { throw new RuntimeException("Email already exists"); } return userRepository.save(user); } public void deleteById(Long id) { userRepository.deleteById(id); } public boolean existsById(Long id) { return userRepository.existsById(id); } }4.2 分页与排序@GetMapping("/page") public ResponseEntity<Page<User>> getUsersPage( @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "10") int size, @RequestParam(defaultValue = "id") String sortBy, @RequestParam(defaultValue = "ASC") Sort.Direction direction) { Pageable pageable = PageRequest.of(page, size, Sort.by(direction, sortBy)); Page<User> usersPage = userRepository.findAll(pageable); return ResponseEntity.ok(usersPage); }4.3 配置数据源(application.yml)spring: datasource: url: jdbc:mysql://localhost:3306/mydb?useSSL=false&serverTimezone=UTC username: root password: password driver-class-name: com.mysql.cj.jdbc.Driver jpa: hibernate: ddl-auto: update show-sql: true properties: hibernate: format_sql: true dialect: org.hibernate.dialect.MySQL8Dialect5. 安全认证5.1 Spring Security 基础依赖:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>安全配置:@Configuration @EnableWebSecurity @EnableMethodSecurity public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .csrf(csrf -> csrf.disable()) .authorizeHttpRequests(auth -> auth .requestMatchers("/api/public/**", "/api/auth/**").permitAll() .requestMatchers("/api/admin/**").hasRole("ADMIN") .anyRequest().authenticated() ) .sessionManagement(session -> session .sessionCreationPolicy(SessionCreationPolicy.STATELESS) ); return http.build(); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }5.2 JWT 认证JWT 工具类:@Component public class JwtUtil { @Value("${jwt.secret}") private String secret; @Value("${jwt.expiration}") private long expiration; public String generateToken(String username, List<String> roles) { Date now = new Date(); Date expiryDate = new Date(now.getTime() + expiration); return Jwts.builder() .setSubject(username) .claim("roles", roles) .setIssuedAt(now) .setExpiration(expiryDate) .signWith(SignatureAlgorithm.HS512, secret) .compact(); } public String getUsernameFromToken(String token) { Claims claims = Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody(); return claims.getSubject(); } public boolean validateToken(String token) { try { Jwts.parser().setSigningKey(secret).parseClaimsJws(token); return true; } catch (JwtException | IllegalArgumentException e) { return false; } } }JWT 过滤器:@Component public class JwtAuthenticationFilter extends OncePerRequestFilter { private final JwtUtil jwtUtil; private final UserDetailsService userDetailsService; @Autowired public JwtAuthenticationFilter(JwtUtil jwtUtil, UserDetailsService userDetailsService) { this.jwtUtil = jwtUtil; this.userDetailsService = userDetailsService; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String authHeader = request.getHeader("Authorization"); if (authHeader != null && authHeader.startsWith("Bearer ")) { String token = authHeader.substring(7); if (jwtUtil.validateToken(token)) { String username = jwtUtil.getUsernameFromToken(token); UserDetails userDetails = userDetailsService.loadUserByUsername(username); UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(auth); } } filterChain.doFilter(request, response); } }6. 日志与监控6.1 日志配置application.yml:logging: level: root: INFO com.example.demo: DEBUG org.springframework.web: INFO org.hibernate.SQL: DEBUG pattern: console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n" file: name: logs/application.log max-size: 10MB max-history: 30代码中使用日志:@Service public class UserService { private static final Logger logger = LoggerFactory.getLogger(UserService.class); public User save(User user) { logger.info("Saving user: {}", user.getEmail()); try { User savedUser = userRepository.save(user); logger.debug("User saved successfully with id: {}", savedUser.getId()); return savedUser; } catch (Exception e) { logger.error("Error saving user", e); throw e; } } }6.2 Spring Boot Actuator依赖:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>配置:management: endpoints: web: exposure: include: health,info,metrics,loggers endpoint: health: show-details: always访问端点:/actuator/health - 健康检查/actuator/info - 应用信息/actuator/metrics - 指标/actuator/loggers - 日志配置7. 测试7.1 单元测试@SpringBootTest class UserServiceTest { @MockBean private UserRepository userRepository; @Autowired private UserService userService; @Test void testFindAll() { User user1 = new User(); user1.setId(1L); user1.setName("Alice"); User user2 = new User(); user2.setId(2L); user2.setName("Bob"); when(userRepository.findAll()).thenReturn(Arrays.asList(user1, user2)); List<User> users = userService.findAll(); assertEquals(2, users.size()); verify(userRepository, times(1)).findAll(); } }7.2 Web 层测试@WebMvcTest(UserController.class) class UserControllerTest { @Autowired private MockMvc mockMvc; @MockBean private UserService userService; @Test void testGetAllUsers() throws Exception { User user = new User(); user.setId(1L); user.setName("Alice"); when(userService.findAll()).thenReturn(Arrays.asList(user)); mockMvc.perform(get("/api/users")) .andExpect(status().isOk()) .andExpect(jsonPath("$", hasSize(1))) .andExpect(jsonPath("$[0].name").value("Alice")); } }8. 部署与运维8.1 打包成 JARMaven 命令:mvn clean package运行 JAR:java -jar target/demo-1.0.0.jar指定配置文件运行:java -jar target/demo-1.0.0.jar --spring.profiles.active=prod8.2 Docker 部署Dockerfile:FROM eclipse-temurin:17-jdk-alpine VOLUME /tmp COPY target/*.jar app.jar ENTRYPOINT ["java","-jar","/app.jar"]docker-compose.yml:version: '3.8' services: app: build: . ports: - "8080:8080" depends_on: - db environment: SPRING_DATASOURCE_URL: jdbc:mysql://db:3306/mydb SPRING_DATASOURCE_USERNAME: root SPRING_DATASOURCE_PASSWORD: password db: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: password MYSQL_DATABASE: mydb ports: - "3306:3306" volumes: - mysql-data:/var/lib/mysql volumes: mysql-data:附录A. 常用注解速查表注解描述@SpringBootApplication主应用类注解@RestControllerREST 控制器@RequestMapping请求映射@GetMappingGET 请求映射@PostMappingPOST 请求映射@PutMappingPUT 请求映射@DeleteMappingDELETE 请求映射@PathVariable路径变量@RequestParam请求参数@RequestBody请求体@Autowired自动装配@Service服务层@Repository数据访问层@EntityJPA 实体@Transactional事务@Configuration配置类@BeanBean 定义B. 参考资源Spring Boot 官方文档Java 8 官方文档Spring Security 官方文档Spring Data JPA 官方文档
2026年03月21日
6 阅读
0 评论
0 点赞
1
2
3