Java项目拷打(实习+点评+AI+外卖)
一、特斯拉全球供应链协作核心平台(Horizon SRM系统)
1、项目概述
1.1、项目简介
项目结构与核心对象
- 项目(Project):系统的顶层管理单元,代表一个完整的供应链或研发任务。每个项目可配置多个团队成员,团队成员分配不同角色与权限。
- 里程碑(Milestone):项目中的关键节点,用于阶段性目标管理。每个项目下可包含多个里程碑,里程碑用于划分项目进度和阶段性成果。
- 交付物(Deliverable):每个里程碑下包含若干交付物,代表具体的输出成果(如文档、样品、报告等)。
交付物审批流
- 每个交付物拥有独立的审批流(Approval Flow),确保交付物质量和流程合规。
- 审批流程通常包括两级:上级项目经理(PM)审批 → 质量经理(QM)审批。
- 审批流支持状态追踪,审批通过后交付物状态更新,审批被拒绝时需整改后重新发起。
里程碑门评(Gate Review)
- 每个里程碑在交付物全部通过审批后,需进行一次门评(Gate Review)。
- 门评由相关责任人(如PM、QM等)参与,评审内容包括交付物完成情况、质量达标情况、问题整改等。
- 门评结果分为“通过”或“拒绝”,通过后里程碑正式完成,项目进入下一阶段;拒绝则需根据评审意见整改,整改完成后可再次发起门评。
团队与权限管理
- 团队成员(Team Member):每个项目可配置多个团队成员,成员可分配不同角色。
- 常见角色:
项目经理(PM):拥有项目全面管理权限,包括里程碑、交付物的维护、审批发起、团队配置等。
质量经理(QM):负责交付物的质量审批,拥有相关审批与质量把控权限。
普通成员:可查看项目进展、参与协作,但无审批和关键配置权限。 - 权限体系:基于角色和系统权限点(如项目创建、维护、查看、审批、门评等),通过权限点控制用户对项目、里程碑、交付物等对象的操作范围,确保操作合规和数据安全。
业务流程梳理
- 项目立项:PM创建项目,配置团队成员及角色。
- 里程碑规划:为项目设定多个里程碑,明确各阶段目标。
- 交付物分解与分配:在每个里程碑下分解交付物,分配责任人。
- 交付物审批:责任人提交交付物,依次经过PM和QM审批,审批通过后交付物状态更新。
- 门评发起与评审:所有交付物审批通过后,发起里程碑门评。门评通过则里程碑完成,项目进入下一阶段;门评拒绝则需整改。
- 权限与协作:团队成员根据分配的角色和权限点参与项目各环节,系统自动校验操作权限,保障流程规范。
1.2、项目亮点
技术栈:.NET Core、 MediatR、 Redis、 MySQL、 Docker、 Azure
项目介绍: 特斯拉全球供应链协作核心平台(Horizon SRM)— 支持全球工厂项目全生命周期管理,提升供应链项目交付效率与合规性
- 负责多级审批流实现:基于 ASP.NET Core 与 MediatR 实现审批流程,支持同意、拒绝、加签、转交等核心操作
- 领域驱动设计(DDD):参与审批流与权限领域的建模,设计 Approval(审批单)、ApprovalNode(审批节点)等聚合根,将状态流转、权限校验等复杂规则封装于领域服务中,确保业务逻辑清晰与高内聚。
- 构建权限校验体系:参与设计基于 Azure AD 的多层次权限体系(接口权限+资源权限),利用 Redis 缓存用户权限数据,将权限校验性能提升 3 倍,有效防止越权访问。
- 工程协作与质量:对接前端团队制定 RESTful API 契约并通过 Swagger 文档化,参与技术方案评审与代码 Review,确保代码质量。
- 提升研发效能:熟练应用 GitHub Copilot 等 AI 工具辅助代码生成与重构,减少重复编码工作,提升功能开发效率。
2、交付物审批流场景
2.1、交付物审批流场景设计
-
审批流的核心业务目标
审批流的核心目的是:对交付物(Deliverable)或相关事项进行多级、多人、可追溯的流程化审批,确保交付物的合规性、质量和责任到人。 -
主要参与对象
交付物(Deliverable):需要被审批的对象,通常与项目、里程碑、任务等关联。
审批单(Approval):一次审批流程的实例,包含审批节点、状态、发起人等。
审批节点(ApprovalNode):审批流中的每一步,通常对应一个审批人或审批组。
审批人(Approver):实际执行审批操作的用户。
发起人(Applicant):提交审批申请的用户。
审批意见/评论(Comments):审批过程中记录的意见、说明、批注等。 -
审批流的主要业务流程
3.1 发起审批
发起人在系统中选择一个交付物,点击“发起审批”。
系统根据交付物类型、项目模板、配置等,自动生成审批流(即审批单和审批节点)。
审批单状态变为“待审批”,第一个审批节点分配给对应审批人。
3.2 审批操作
审批人收到待办任务后,有如下操作选项:
同意(Approve):同意后,流转到下一个审批节点。如果是最后一个节点,则审批单状态变为“已通过”。
拒绝(Reject):拒绝后,审批单状态变为“已拒绝”,流程终止。
撤回(Withdraw):发起人可在特定节点撤回审批,审批单状态变为“已撤回”。
转交(Transfer):审批人可将当前节点转交给其他人审批。
加签(AddSign):审批人可在当前节点增加额外的审批人。
3.3 审批流的流转
每个节点审批通过后,系统自动推进到下一个节点。
所有节点审批通过后,交付物状态变为“已审批通过”。
任一节点拒绝,审批流终止,交付物状态变为“审批未通过”。
3.4 审批通知与待办
每当审批流进入新节点,系统自动通知下一个审批人。
审批人可在“待办事项”中查看所有待审批的交付物。
审批完成后,发起人和相关人员收到结果通知。 -
关键业务逻辑细节
4.1 审批单与审批节点的状态管理
审批单(Approval)有状态:待审批、已通过、已拒绝、已撤回、已作废等。
审批节点(ApprovalNode)有状态:待处理、已同意、已拒绝、已转交、已加签等。
状态流转严格受控,防止越权操作。
4.2 权限与校验
只有审批人本人可操作当前节点。
发起人只能在允许的节点撤回。
4.3 审批意见与日志
每次审批操作都需填写意见(可选/必填)。
所有操作均记录审批日志,便于追溯。
4.4 交付物与审批流的关系
一个交付物可有多次审批流实例(如补充、变更等)。
审批流与交付物、项目、里程碑等强关联。
5.典型审批流时序图(伪代码)
发起审批
用户提交 → 生成审批单/节点 → 通知第一个审批人
审批人操作
审批人同意/拒绝/转交/加签 → 更新节点/审批单状态 → 通知下一个审批人或发起人
审批流结束
所有节点通过 → 交付物状态变为“已审批通过”
任一节点拒绝 → 交付物状态变为“审批未通过”
2.2 审批流有哪些核心状态?状态之间如何流转?
A:主要状态有:
- 待审批:流程刚发起,等待第一个审批人处理
- 审批中:部分节点已通过,流转到下一节点
- 已通过:所有节点审批通过
- 已拒绝:任一节点拒绝
- 已撤回:发起人主动撤回
状态流转受严格管控,比如只有"待审批"和"审批中"可以撤回。
2.3 你是如何设计审批流数据模型的?
// 可以这样回答核心表结构:
public class Approval // 审批单
{
public string ApprovalId { get; set; }
public string DeliverableId { get; set; }
public ApprovalStatus Status { get; set; }
public List<ApprovalNode> Nodes { get; set; }
}
public class ApprovalNode // 审批节点
{
public string NodeId { get; set; }
public string ApproverId { get; set; }
public NodeStatus Status { get; set; } // 待处理、已同意、已拒绝
public int Order { get; set; } // 审批顺序
}
2.4 如何处理加签和转交这种复杂场景?
A:加签是在当前节点增加额外的审批人,不影响原有流程;
转交是把当前审批任务转给其他人,原审批人失去审批权限。
我们在ApprovalNode中维护了转交关系链,确保操作可追溯。
2.5 审批过程中如何保证数据一致性?
A:我们采用了以下措施:
- 使用数据库事务确保审批状态和节点状态同时更新
- 所有审批操作都记录审计日志,便于问题追踪
2.6 如果审批人很长时间不审批,系统如何处理?
A:我们实现了定时提醒机制:
- 工作日上午8:30自动扫描待办审批
- 通过站内信和邮件通知审批人
- 支持去重,避免重复通知同一批审批
2.7 审批流配置如何支持灵活性?
A:我们设计了可配置的审批模板:
- 支持按项目类型配置不同的审批流程
- 通过ApprovalTemplate实体管理这些配置
3、权限校验场景
3.1、权限校验场景设计
前提:
应用注册:在 Azure AD 注册应用,获取 ClientId、Secret,系统才能对接企业身份认证。
用户管理:只有在 Azure AD 租户下的用户才能登录,确保公司账号安全、统一管理
1、服务端 AAD 配置加载
密钥服务拉取配置:如 Fuze 等密钥服务,保障 ClientId/Secret 不落盘,提升安全性。
环境变量注入:便于本地开发、测试、生产环境的灵活切换。
2 用户身份认证(Azure AD)
SSO 登录:用户访问系统自动跳转 Azure AD,认证通过后回调系统,获得 JWT Token。
Token 内容:包含用户邮箱、UserId、租户信息等,后续所有请求都带上 Token。
-
控制器/接口声明权限
[HorizonAuthorize] 特性:声明接口所需权限点,支持单个或多个权限点。
权限点集中管理:如 IPermissionService 统一定义,便于维护和扩展 -
请求到达时触发权限校验
ASP.NET Core 中间件/过滤器:自动拦截带有 [HorizonAuthorize] 的请求,进入自定义权限校验逻辑。 -
获取当前用户信息
从 Token 解析:获取邮箱、UserId、租户等,作为后续权限校验的依据 -
查询用户权限
优先查缓存:如 Redis,提升性能,减少远程调用。
缓存未命中时远程拉取:通过 IWarpAuthClient 调用权限服务,获取用户所有权限点,并写入缓存。
权限服务:可支持动态授权、角色继承、权限点扩展等。 -
比对权限
权限点比对:只要用户拥有声明的任一权限点即可访问(或根据业务配置为“全部拥有”)。
无权限时拦截:返回 401(未认证)或 403(无权限),前端可据此提示用户。 -
资源级别的更细粒度校验(资源访问控制)
进入 MediatR Handler:只有通过接口权限点校验的请求才会进入 Handler。
GetProtectedResourcesAsync:根据请求参数查找涉及的业务资源(如 Project、Deliverable 等)。
CanAccess 校验:每个资源实现自己的 CanAccess 方法,判断当前用户是否有权访问(如项目成员、角色、类别权限等)。
全资源通过才可执行业务:只要有一个资源未通过校验,直接抛出 ResourceForbiddenException,终止后续业务逻辑。
3.2、系统有哪些核心角色?各自有什么权限?
A:主要角色:
- 项目经理(PM):项目全权管理,包括里程碑维护、团队配置
- 质量经理(QM):交付物质量审批,参与门评审
- 普通成员:查看项目进展,提交交付物,无审批权限
3.3、权限校验的具体代码实现是怎样的?
// 可以展示这样的代码理解:
[HorizonAuthorize([IPermissionService.DeliverableBreakdownApproval])]
[HttpPost("approve")]
public async Task<IActionResult> ApproveAsync(...)
{
// 1. 自动校验用户是否有DeliverableBreakdownApproval权限
// 2. 在MediatR Handler中进行资源级别权限校验
var project = await _projectRepository.GetAsync(projectId);
if (!project.CanAccess(CurrentUser.Id))
throw new ResourceForbiddenException();
}
3.4、资源级别权限校验是如何工作的?
A:每个聚合根(如Project、Deliverable)都实现自己的CanAccess方法,
根据业务规则判断用户是否有权访问。比如Project的CanAccess会检查:
- 用户是否是项目成员
- 用户是否是项目经理
- 用户是否有全局查看权限
3.5、权限数据如何缓存和更新?
A:我们使用Redis缓存用户权限数据:
- 用户登录后,权限点列表被缓存到Redis,key为userId
- 缓存有过期时间,确保权限变更能及时生效
- 权限变更时主动清除相关缓存
3.6、如何防止用户直接调用API绕过前端权限控制?
A:这就是为什么需要资源级别权限校验。即使攻击者拿到接口地址和参数,
系统也会在MediatR Handler中校验他是否有权访问请求的资源。
比如校验Issue时,会追溯到对应的Project进行权限验证。
3.7、权限系统的性能如何优化?
A:主要优化点:
- 权限点缓存:用户权限缓存在Redis,避免每次请求查询数据库
- 批量校验:一次查询校验多个资源权限
- 权限预计算:用户登录时预计算常用权限组合
4、通用问题
4.1、介绍一下这个项目
我上一段在特斯拉实习时,参与开发的是一个名为 Horizon SRM 的全球供应链协作平台。它是一个服务于全球工厂的项目全生命周期管理系统,核心目标是确保供应链上各类研发和制造项目能按时、合规地交付。”
简单来说,这个系统管理的是‘项目 -> 里程碑 -> 交付物’三层结构。项目经理创建一个项目后(比如‘为Model Y采购新一代电池’),会设定几个关键的里程碑(比如‘完成技术验证’、‘通过安全测试),每个里程碑下又包含许多需要审批的交付物,是每个阶段必须提交的具体成果(比如‘技术规格书’、‘测试报告’)
我的工作主要集中在后端业务逻辑的实现,主要包括两个核心部分:
第一,是设计了‘交付物’的审批流水线。 一个交付物提交后,会自动流转给项目经理审批,再流转给质量经理审批。我负责实现了这条流水线的各个功能,让它支持同意、拒绝、转交给他人、临时增加审批人等各种复杂操作。
第二,是构建了精细化的‘权限门禁’系统。 在一个全球协作的平台里,确保‘该看的人能看到,该批的人才能批’至关重要。我参与设计了基于 Azure AD 的双重权限校验:首先,在接口层面校验用户有没有操作权限(比如‘审批’按钮);其次,在业务层面校验用户有没有数据权限(比如他是不是这个项目的成员)。通过将权限数据缓存到 Redis,我们把校验速度提升了3倍,有效防止了数据越权访问。
4.2、遇到最大的技术挑战是什么?如何解决的?
在我参与的这个项目中,遇到的最大技术挑战是 设计并实现一个能够精准映射复杂业务规则、且状态流转高度可靠的审批流引擎。
- 挑战的具体体现(聚焦于业务复杂性):
这个挑战主要来自几个方面:
业务规则复杂且多变:审批流不仅要支持标准的‘同意’和‘拒绝’,更要处理‘加签’(在当前节点临时增加审批人)和‘转交’(把任务完全委托给他人)这类会动态改变审批路径的非标准操作。每一种操作都需要触发不同的状态变更和后续流程,规则非常繁杂。
状态管理的强一致性要求:一次审批操作,需要原子性地更新审批单(Approval) 的整体状态、审批节点(ApprovalNode) 的当前状态,并生成审批日志(Audit Log)。在任何情况下,都不能出现审批单显示‘已通过’但某个节点却记录为‘已拒绝’这类数据不一致的情况,这对系统的可靠性和可信度至关重要。
对未来的扩展性要求:业务方明确表示,未来需要支持如‘会签’(多个审批人全部通过才生效)、‘或签’(多个审批人中一个通过即可)等更复杂的模式。初始的设计必须为这些业务演进预留出清晰、低成本的扩展路径。
- 我的解决思路和方案:
面对这个挑战,我的核心思路是通过清晰的领域建模和严谨的状态管控来驯服业务复杂性。
第一步:运用DDD进行领域建模,明确业务边界
我将‘审批流’作为一个独立的子域,并与产品经理深入沟通,识别出了两个核心的聚合根:Approval(审批单)和 ApprovalNode(审批节点)。我明确了它们的职责和边界:Approval 负责维护流程的全局状态(如进行中、已通过),而 ApprovalNode 负责管理节点级别的状态(如待处理、已同意)和操作。这有效防止了混乱的状态扩散,让逻辑高度内聚。
第二步:设计严谨的状态机与防错校验
我没有引入重型的状态机框架,而是通过枚举和领域服务来清晰定义状态流转规则。我为每个关键操作(如 ApproveCommand, RejectCommand, TransferCommand)创建了独立的命令处理器。在每个处理器中,我首先进行严格的前置校验,例如:
当前用户是否是该节点的合法审批人?
当前节点是否处于‘待处理’状态?
发起‘转交’操作时,目标人是否具备相应权限?
这些校验确保了业务规则在代码层面得到严格执行,从源头杜绝了非法状态流转。
第三步:利用数据库事务保证数据强一致性
为了解决数据一致性问题,我将一次审批操作所涉及的所有数据库更新——包括更新节点状态、更新审批单状态、插入新的节点(如加签时)、记录审计日志——全部包裹在同一个 DbContext 事务中。这确保了这些操作是一个不可分割的单元,完美契合了B端系统对数据准确性的严苛要求。
第四步:通过抽象和策略模式预留扩展点
为了应对未来的变化,我在 ApprovalNode 中设计了 NodeType 字段,为未来的‘串签’、‘会签’等不同类型节点打下基础。同时,我将审批流的生成逻辑抽象为 IApprovalFlowGenerator 接口,未来只需实现新的生成器,就能支持可配置的审批模板,而对现有核心引擎的改动可以降到最低。
- 最终达成的效果:
通过这套设计,我们交付了一个非常稳健的审批流引擎:
业务逻辑清晰可维护:代码真实地反映了业务规则,即使半年后回头看,或者新同事接手,都能快速理解。
高度可靠:上线后,成功支撑了全球供应链上万个项目的审批流程,未出现一例因状态错乱导致的数据事故。
灵活可扩展:团队后来在加入‘会签’功能时,基于现有架构进行扩展,验证了当初设计的前瞻性,大大降低了开发成本。
4.3、为什么选择MediatR实现CQRS?
A:MediatR很好地解耦了Controller和业务逻辑,让代码更清晰。
同时天然支持CQRS,让查询和命令分离,便于针对性地优化。
比如审批查询可以走优化的视图,而审批操作保证事务一致性。
4.4、DDD在这个项目中是如何应用的?
A:我们按照DDD划分了多个聚合根:Project、Deliverable、Approval等。
每个聚合根封装了自己的业务规则,比如Project负责管理成员权限,
Approval负责审批状态流转。这让复杂业务逻辑更易于维护。
二、高并发优惠券秒杀平台(黑马点评)
1、项目概述
1.1、项目简介
本项目是基于SpringBoot+Redis+RabbitMQ的分布式高并发优惠券秒杀系统,针对电商场景下的瞬时大流量抢购需求,设计了完整的秒杀解决方案。通过Redis缓存、异步削峰、分布式锁等技术,解决了传统秒杀系统中常见的超卖问题
1.2、项目亮点
- 使用 Redis 解决了在集群模式下的 Session共享问题,通过双拦截器实现用户的登录校验和权限刷新
- 采用Cache Aside模式解决数据库与缓存的一致性问题,通过主动更新结合超时删除保证最终一致性
- 使用 Redis 对高频访问的信息进行缓存,降低数据库查询压力,解决了缓存穿透、雪崩、击穿问题
- 使用 Redis + Lua脚本实现对用户秒杀资格的预检,同时用乐观锁解决秒杀产生的超卖问题
- 使用 Redisson分布式锁保证一人一单的线程,通过RabbitMQ异步削峰,提高系统吞吐量
1.3、核心功能
用户服务:
- 短信登录(Redis替代Session实现集群共享,双拦截器保障Token刷新)
商户服务:
- 多级缓存优化(Cache Aside模式 + Redisson读写锁保障双写一致性)
秒杀服务:
- 分布式锁(Redisson实现)解决集群环境下“一人一单”线程安全问题
- 异步下单优化:Redis Lua脚本预检库存 + RabbitMQ消息队列异步处理订单
2、用户登录场景
2.1、用户登录场景设计
| 内容分类 | 详情描述 |
|---|---|
| 控制器 | UserController |
| 接口 | 接口 1:code(生成验证码,参数为:手机号) |
| 执行流程 | 1. 校验手机号 2. 若不符合校验规则,返回错误信息 3. 若符合,生成随机验证码 4. 以手机号为 key 、验证码为 value ,将验证码保存到 Redis 5. 向该手机号用户发送验证码 |
| 内容分类 | 详情描述 |
|---|---|
| 控制器 | UserController |
| 接口 | 接口 2:login(登录校验,参数为:手机号,验证码) |
| 执行流程 | 1. 校验手机号格式,若无效则返回错误信息 2. 校验验证码(从 Redis 中根据手机号查询对应验证码,与用户输入的验证码比对),若不一致则返回错误信息 3. 根据手机号查询用户:若用户不存在,创建新用户并保存到数据库- 若用户已存在,直接使用该用户信息 4. 生成随机 UUID 作为 token,以 token 为 key、用户信息(User)为 value 存入 Redis 5. 将 token 返回给前端,供后续请求携带以维持会话 |
| 拦截器类型 | 拦截范围 | 执行阶段 | 流程步骤 | 核心作用 |
|---|---|---|---|---|
| 外拦截器 | 所有请求路径(全局拦截) | preHandle | 1. 从请求头中获取 token,若不存在,直接放行 2. 若 token 存在,从 Redis 中查询对应的用户信息,若 Redis 中无用户,直接放行 3. 若 Redis 中有对应用户,将用户信息存入 ThreadLocal 4. 刷新该 token 在 Redis 中的有效期 5. 放行请求 |
处理 token 解析、用户信息暂存、有效期刷新,为后续需要登录的接口提供用户上下文,不拦截未携带 token 的请求 |
| 外拦截器 | 所有请求路径(全局拦截) | afterCompletion | 从 ThreadLocal 中移除用户信息 | 避免 ThreadLocal 内存泄漏 |
| 内拦截器 | 需要登录的指定路径 | preHandle | 1. 检查 ThreadLocal 中是否存在用户信息 2. 若不存在,拦截请求并返回 401(未授权) 3. 若存在,放行请求 |
强制校验需要登录的接口,确保只有已通过 token 验证的用户才能访问 |
2.2、为什么用 Redis 替代 Session?
-
分布式系统下的 Session 共享
传统 Session 的缺陷:
当应用部署在多台服务器(如负载均衡集群)时,用户的请求可能被分发到不同服务器。若 Session 存储在本地(如文件或内存),其他服务器无法读取该 Session,导致用户频繁登出或状态丢失。
Redis 的解决方案:
作为独立的集中式存储,所有服务器都从同一个 Redis 读写 Session,彻底解决跨服务器 Session 共享问题。 -
性能与扩展性更优
独立存储减轻应用服务器负担,支持集群部署,应对高并发和大容量需求;可通过 Redis 集群横向扩展,应对数据增长,而传统 Session 难以水平扩展。 -
可靠性
Redis 支持 RDB/AOF 持久化,即使重启也不会丢失
2.3、还有其他解决方法吗?
- 基于 Cookie 的 Token 机制,不再使用服务器端保存 Session,而是通过客户端保存 Token(如 JWT)。
- Token 包含用户的认证信息(如用户 ID、权限等),并通过签名验证其完整性和真实性。
- 每次请求,客户端将 Token 放在 Cookie 或 HTTP 头中发送到服务
JWT由三部分组成:header(头部)、payload(载荷)、signature(签名)
- Header(头部) 作用:描述令牌的元数据,如签名算法(如HS256、RS256)和令牌类型(固定为JWT)。
- Payload(负载) 作用:携带实际的数据(声明),分为三类:
预定义声明:标准字段,如 iss(签发者)、exp(过期时间)、sub(主题)等。
公开声明:自定义公共字段,需避免冲突(建议通过IANA 注册)。
私有声明:双方约定的自定义数据。 - Signature(签名)作用:验证令牌的完整性和真实性。生成方式:将编码后的 Header 和 Payload 拼接后通过 Header 中指定的算法(如 HS256)和密钥进行签名。
2.4、为什么需要双拦截器
因为有些不需要登录校验的操作不会走拦截器,如果用户在这些界面浏览,用户权限时间不会刷新。所以系统中设置了两层拦截器:
第一层拦截器是做全局处理,例如获取 Token,查询 Redis 中的用户信息,刷新 Token 有效期等通用操作。
第二层拦截器专注于验证用户登录的逻辑,如果路径需要登录,但用户未登录,则直接拦截请求。
好处:
- 职责分离:这种分层设计让每个拦截器的职责更加单一,代码更加清晰、易于维护
- 提升性能:如果直接在第一层拦截器处理登录验证,可能会对每个请求都进行不必要的检查。而第二层拦截器仅在 “需要登录的路径”中生效,可以避免不必要的性能开销。
- 灵活性:这种机制方便扩展,不需要修改第一层的全局逻辑。
2.5、数据库
user表:存储用户核心、高频使用的基础信息
| 字段 | 解释 |
|---|---|
| id | 用于唯一标识每一条记录的主键,为自增整数 |
| phone | 用户的手机号码,具有唯一性,可用于用户登录、身份验证等场景 |
| password | 用户的登录密码,经过加密处理,保障用户账户安全 |
| nick_name | 用户昵称,用于区分不同用户,增加个性化 |
| icon | 存储用户头像的url |
| create_time | 记录该条用户信息在数据库中创建的具体时间,用于统计用户注册时间等 |
| update_time | 记录用户信息最后一次被修改的时间,用于跟踪用户信息的变更情况 |
user_info表:作为扩展表,存储用户的 “非核心、扩展信息
| 字段名 | 简单解释 |
|---|---|
| user_id | 关联用户主表(user 表)的 id,作为唯一标识(同时作为主键) |
| gender | 用户性别标识,1(男)、2(女) |
| birthday | 用户出生日期,格式为 YYYY-MM-DD |
| province | 用户所在省份(如“广东省”) |
| city | 用户所在城市(如“深圳市”) |
| personal_note | 用户个人备注/简介(如“喜欢摄影和旅行”) |
| create_time | 扩展信息创建时间,记录首次添加用户详情的时间戳 |
| update_time | 扩展信息最后更新时间,记录用户修改详情的时间戳 |
3、商铺缓存场景
3.1、商铺缓存场景设计
1、查缓存:存在且非空,直接返回店铺数据;
2、空缓存:命中缓存但值为空,立即返回"店铺不存在"(防穿透);
3、无缓存:获取分布式锁,进入重建流程(防击穿);
4、查数据库:若数据不存在,缓存空值并设短TTL,返回"店铺不存在";
5、数据存在:重建缓存并添加随机TTL后返回(防雪崩);
6、释放锁:确保其他线程可继续处理。
3.2、什么是主动更新和超时删除
- 主动更新:当数据库数据发生变更时(如新增、修改、删除),通过代码逻辑主动操作 Redis 缓存,保证缓存数据与数据库一致。
- 超时删除:给缓存数据设置过期时间,当数据过期后,Redis 会自动将其删除
3.3、数据库和Redis双写一致问题
读操作
- 未过期直接返回
- 过期加锁,重建缓存
写操作
然后先更新数据库,再把缓存删除。
3.4、缓存穿透、击穿、雪崩怎么解决?
缓存空值、逻辑过期、不同TTL
穿透 (访问不存在的数据): 对于数据库中肯定不存在的优惠券 ID 或用户 ID 请求,在 Redis 中缓存一个空值(如 “NULL”)并设置较短的过期时间(如 30 秒)。同时使用 布隆过滤器 (Bloom Filter) 在查询 Redis 前先快速过滤掉绝对不存在的 key。(项目里可以说:我在查询优惠券详情前,如果 ID 不符合规则或范围,会直接返回错误或查布隆过滤器)
雪崩 (大量 key 同时失效): 给缓存数据设置随机过期时间(如基础时间+随机偏移量),避免大量 key 同时重建。对于核心数据(如秒杀活动配置),采用永不过期+后台定时更新策略。(项目里可以说:优惠券信息缓存过期时间设置为 24小时 + 随机 0-3600秒)
击穿 (热点 key 失效瞬间被大量访问): 使用 互斥锁 (Redis SETNX 或 Redisson Lock)。当发现缓存失效时,只有一个线程能获得锁去数据库加载数据并重建缓存,其他线程等待锁释放后直接读取新缓存。(项目里可以说:在查询优惠券信息的缓存逻辑中,如果 miss,会尝试获取一个分布式锁,只有拿到锁的线程去查 DB 回填缓存,其他线程自旋等待)
3.5、数据库
shop:商铺信息
| 字段名称 | 类型 | 简要解释 |
|---|---|---|
| id | bigint unsigned | 主键,自增唯一标识一条商铺记录 |
| name | varchar(128) | 商铺名称 |
| images | varchar(1024) | 商铺图片地址 |
| area | varchar(128) | 商铺所在商圈(如陆家嘴),可为空 |
| address | varchar(255) | 商铺详细地址 |
| x | double unsigned | 商铺位置的经度 |
| y | double unsigned | 商铺位置的纬度 |
| avg_price | bigint unsigned | 商铺商品均价,取整数,可为空 |
| sold | int(10) unsigned | 商铺商品销量 |
| comments | int(10) unsigned | 商铺的评论数量,无符号整数 |
| score | int(2) unsigned | 商铺评分(1~5分),实际存储为分数乘10的整数(避免小数,如3.5分存为35) |
| open_hours | varchar(32) | 商铺营业时间(如10:00-22:00),可为空 |
| create_time | timestamp | 记录创建时间,默认值为当前时间戳 |
| update_time | timestamp | 记录最后更新时间,默认值为当前时间戳,更新时自动刷新 |
4、秒杀下单场景
4.1、秒杀下单场景设计
- 预热数据:秒杀开始前,把库存信息(key为库存业务前缀+优惠卷id,value为库存量)和订单信息(key为订单业务前缀+优惠卷id,value为记录用户id,set集合实现)放入redis
- 原子化预占库存:使用Lua脚本原子性地完成三项操作,库存检查:通过stockKey检查库存是否 > 0,重复购买检查:通过orderKey集合检查用户是否已购买,预占库存:扣减库存并记录购买用户,返回状态码:0:成功,1:库存不足,2:重复购买
- 构造订单消息:如果返回状态码是1或者2,直接返回错误信息,如果是0,则构造订单信息(订单id,用户id,券id)
- 发送消息队列:将构造的订单信息放入消2息队列,如果成功,返回订单id给前端(此时订单未持久化,但预占完成),前端显示处理中。如果失败则回滚库存,移除用户,返回错误信息
- 消费者处理: 1)对用户ID加分布式锁,避免并发处理同一用户的订单。2)幂等检查:查询数据库是否已存在该订单。3)乐观锁,判断数据库stock>0则扣减数据库库存 4)创建订单:落库完成交易。若任意1)2)3)步骤失败,重试3次 → 转入死信队列 → 人工处理。
4.2、前端用户在秒杀场景下的感知
1、点击抢券,Redis+lua脚本预检之后如果成功,则反馈处理中
2、前端每2s->5s->10s轮询的去查询订单状态,如果消息队列创建成功,则返回抢券成功
3、如果30s还没订单信息,说明抢券失败,返回给用户超时重试
4.2、为什么使用Redis+Lua脚本
原子性需求:"在秒杀场景中,我们需要确保检查库存和扣减是原子操作.
两个请求同时读到库存=1,都执行扣减,导致库存变为-1。
检查库存和扣减是不可分割的原子操作,彻底杜绝超卖
4.3、什么是超卖问题,怎么解决
超卖问题:并发多线程问题,当线程 1 查询库存后,判断前,又有别的线程来查询,从而造成判断错误,超卖。
使用的是乐观锁 CAS 算法。当修改数据时,判断库存时是否>0,大于0则可以修改
4.4、为什么和如何使用分布式锁解决一人一单问题
优惠卷是为了引流,为了防止一个人可以无限制的抢这个优惠卷,所以我们应当增加一层逻辑,让一个用户只能下一个单,而不是让一个用户下多个单
直接加一层对应用户的订单量count>0的判断之后会出现高并发线程安全问题。所以这里需要加锁。而且必须要控制锁的粒度,这里是以用户id为粒度。
因为集群模式需要用Redission分布式锁,保证了在不同JVM之间仍能工作。
4.5、使用 Redisson 分布式锁时,如何避免死锁?
Redisson 实现了 看门狗 (Watchdog) 机制。当客户端加锁成功,它会启动一个后台线程定期(默认锁过期时间的 1/3)去检查持有锁的客户端是否还“活着”(通过维持一个连接心跳)。如果客户端还活着,它会自动续期锁的过期时间。如果客户端崩溃,锁最终会超时释放。
4.6、如果一个人可以下多单,这时候不能用stock>0来判断了,怎么解决
允许单人购买5单的情况总结
在这种情况下,业务需要同时控制 总库存不超卖 和 单人限购5单。解决方案的核心是通过 Redis Lua脚本原子化操作,在一个脚本中校验总库存和用户已购数量,确保两者同时满足条件时才扣减库存。例如:
检查总库存:stock >= 本次购买量
检查用户已购:user_buy + 本次购买量 ≤ 5
原子化执行:若条件通过,则扣减总库存并增加用户计数。
允许单人无限购买的情况总结
当业务允许 单人无限购买 时,只需确保 总库存不超卖,无需限制用户购买次数。解决方案更简单:
纯库存原子扣减:通过Lua脚本保证 检查库存 → 扣减 的原子性,例如:
若 stock >= 本次购买量,则扣减并返回剩余库存;否则返回失败。
异步订单处理:扣减成功后生成临时订单,通过异步任务持久化到数据库,提升响应速度。
4.6、使用消息队列下单有什么好处
流量控制:将Redis拦截后的请求平滑导入数据库,避免击穿
体验优化:响应时间从100ms+降到10ms级,用户无感知排队
数据安全:通过MQ重试+消费者事务,确保预占与落库最终一致
4.7、为什么用RabbitMQ而不是Kafka,RocketMQ
低延迟:秒杀要求快速响应,RabbitMQ微秒级延迟优于Kafka的毫秒级。
高可靠:通过ACK+持久化+死信队列,保证订单消息不丢失。
轻量级:Redis已拦截大部分流量,RabbitMQ单机万级QPS足够,无需Kafka的高吞吐设计。
易运维:相比Kafka的ZooKeeper依赖,RabbitMQ更简单,适合中小规模秒杀场景。
如果未来业务增长到百万QPS,我们可以考虑RocketMQ,但目前RabbitMQ是最优解。
4.8、秒杀限流应该从哪些地方入手
┌─────────────────────────────────────┐
│ 第1层:前端/客户端限流 │ ← 最外层,最早拦截,前端JS限流(防止用户疯狂点击)
├─────────────────────────────────────┤
│ 第2层:Nginx/网关限流 │ ← 网络层防护,限制每秒10个请求(按IP)
├─────────────────────────────────────┤
│ 第3层:应用层限流(Spring Cloud) │ ← 业务服务防护,令牌桶、滑动窗口、Sentinel(阿里)
├─────────────────────────────────────┤
│ 第4层:缓存层防护(Redis) │ ← 数据访问防护,Redis原子操作
├─────────────────────────────────────┤
│ 第5层:数据库防护 │ ← 最后防线,数据库连接池配置
└─────────────────────────────────────┘
4.8、数据库
voucher 券信息
| 字段名 | 类型 | 简要解释 |
|---|---|---|
| id | bigint unsigned | 主键,自增 |
| shop_id | bigint unsigned | 商铺id |
| title | varchar(255) | 代金券标题 |
| rules | varchar(1024) | 使用规则 |
| pay_value | bigint unsigned | 支付金额,单位是分。例如200代表2元 |
| actual_value | bigint | 抵扣金额,单位是分。例如200代表2元 |
| type | tinyint unsigned | 0,普通券;1,秒杀券 |
| status | tinyint unsigned | 1,上架; 2,下架; 3,过期 |
| create_time | timestamp | 创建时间,默认当前时间 |
| update_time | timestamp | 更新时间,默认当前时间,自动更新 |
seckill_voucher 秒杀券信息
| 字段名 | 类型 | 简要解释 |
|---|---|---|
| voucher_id | bigint unsigned | 关联的优惠券的id,主键 |
| stock | int | 库存 |
| create_time | timestamp | 创建时间,默认当前时间 |
| begin_time | timestamp | 生效时间,默认当前时间 |
| end_time | timestamp | 失效时间,默认当前时间 |
| update_time | timestamp | 更新时间,默认当前时间,自动更新 |
voucher_order订单信息
| 字段名 | 类型 | 简要解释 |
|---|---|---|
| id | bigint | 主键 |
| user_id | bigint unsigned | 下单的用户id |
| voucher_id | bigint unsigned | 购买的代金券id |
| pay_type | tinyint unsigned | 支付方式 1:余额支付;2:支付宝;3:微信 |
| status | tinyint unsigned | 订单状态,1:未支付;2:已支付;3:已核销;4:已取消;5:退款中;6:已退款 |
| create_time | timestamp | 下单时间,默认当前时间 |
| pay_time | timestamp | 支付时间 |
| use_time | timestamp | 核销时间 |
| refund_time | timestamp | 退款时间 |
| update_time | timestamp | 更新时间,默认当前时间,自动更新 |
5、项目难点
难点一:高并发下的库存精准控制(超卖问题)
Redis+lua、Redisson+CAS
难点二:高并发请求的流量管控
RabbitMQ解耦、削峰
三、SpringAI智能应用开发平台
1、项目概述
1.1、项目简介
本项目是基于Spring AI构建的多功能智能交互系统,核心包含对话机器人、课程智能客服和PDF问答(ChatPDF)三大模块。系统整合大语言模型(LLM)能力,结合Prompt/RAG/文本向量化等技术,实现了自然语言驱动的信息查询、事务处理与文档理解功能。
1.2、项目亮点
- 集成Spring AI框架统一调用OpenAI、本地LLM(如Ollama)等后端模型,支持动态切换模型引擎,降低30%外部API调用成本
- 构建Function Calling调度框架,通过LLM解析用户意图后MyBatis精准查询数据库的课程详情,并支持预约课程写入操作
- 基于RAG实现ChatPDF,通过PDF向量化+ 向量数据库相似度检索,解决LLM长文本处理缺陷,提升检索相关性40%
- 使用Spring AI的流式响应提升智能客服和ChatPDF长文本处理的用户体验,用户等待时间减少60%
2、对话机器人场景
2.1、对话机器人场景设计
1、引入依赖,配置 application.yaml
2、定义配置类(创建 ChatClient Bean)
3、创建 Controller(处理HTTP请求)
3、智能客服场景
3.1、智能客服场景设计
- 用户发起请求:用户通过聊天界面或 API 发送自然语言请求到Spring Boot 后端服务(例如 /chat 端点)
- 后端组装 Prompt 发给LLM:需要预先定义好你的“工具”或“函数”。这些函数代表后端能执行的操作(如查询课程、预约课程)。在 Spring AI 中,这通常通过实现 @ToolParam注解来定义函数,每个函数需要指定,name: 函数唯一标识,description: 函数的自然语言描述,至关重要! LLM 靠这个理解函数的用途和何时调用。input schema: 函数所需的参数及其类型,组装 Prompt: 将用户的问题 + 当前对话上下文(如果需要) + 所有已注册函数的定义(名称、描述、参数 Schema) 一起发送给 LLM
- LLM 推理与结构化输出:LLM 接收到 Prompt(包含用户问题和函数定义)。LLM 分析用户意图:判断用户请求是否可以通过调用一个或多个已定义的函数来满足(基于函数描述)。如果可以,则选择最合适的函数(或多个函数)。解析用户问题中的相关信息,填充到选定函数的参数中。生成结构化响应: LLM 不再生成普通文本回复,而是生成一个特殊的、格式化的消息(通常是 JSON)。如果 LLM 判断用户请求不需要调用函数(比如只是普通聊天),它仍会生成标准的文本回复。
- 后端接收并解析 LLM 输出你的后端服务接收到 LLM 的响应。检查响应类型:如果是普通文本回复,直接将其返回给用户。如果是包含 function_call 的结构化响应,则进入函数调用流程。Spring AI 的 ChatResponse 对象通常会提供便捷的方法(如 getFunctionCall())来检查并提取这个结构化调用信息。
- 后端执行目标函数:根据 function_call.name,你的后端代码找到对应的、预先注册的函数实现(即被 @Tool 标记的方法)。函数执行会得到一个结果(如查询到的课程列表 List 或预约操作是否成功的状态 Boolean/String)。
- 后端将函数结果返回给 LLM (生成最终用户回复):后端不会直接把数据库结果(如 JSON 或 Java 对象)直接返回给用户,这通常不友好。而是将 函数执行结果(如查询到的课程数据或预约状态)再次发送给 LLM。这次发送的消息结构通常是:用户原始问题 + 第一次LLM的function_call响应 + 函数执行的结果。LLM 的第二次推理, 生成自然语言总结。
- 后端将最终回复返回给用户:你的后端服务将 LLM 第二次生成的、基于函数结果的自然语言回复发送给前端,展示给用户。如果启用了流式响应,这一步可以是流式输出的。
4、ChatPDF场景
4.1、ChatPDF场景设计
- PDF预处理(离线):拆分:Springai读取器读取PDF按段落/页拆分为小文本块Document,对每个文本块调用 Embedding模型生成向量。将 向量 + 原始文本存入向量数据库(如SimpleVectorStore,ChromaDB、Milvus)
- 用户提问向量化:将用户问题调用向量模型转换为向量,在向量数据库中搜索与问题向量最相似的Top-K个文本块(如K=5),计算余弦相似度,返回相似度最高的片段
- 调用LLM生成答案:将Prompt输入LLM(如GPT-4/Ollama),生成最终回复
四、校园生活服务平台(苍穹外卖)
1、项目概述
1.1、项目简介
本项目是基于Spring Boot的校园生活服务系统,分为管理端(供校内商家使用)和用户端(微信小程序)。管理端包括员工信息、商品及分类的管理,订单状态跟踪等功能;用户端在线浏览商品,添加购物车及下单等功能。
1.2、项目亮点
- 登录及身份验证使用JWT令牌技术,用自定义拦截器完成用户认证,通过ThreadLocal优化鉴权逻辑
- 使用Redis缓存高频数据如同分类商品,提高系统性能和响应速度
- 使用Nginx用作HTTP服务器,部署静态资源,反向代理和负载均衡
- 通过webSocket实现客户端与服务端的长连接,并实现来单提醒及客户催单等功能
- 使用SpringTask实现订单状态的定时处理,超时自动取消订单等功能
1.3、核心功能
管理端
1、admin:EmployeeController:用户登录(JWT、ThreadLocal)、员工信息维护
3、admin:DishController/CategoryController :商品/分类管理,Redis缓存菜品信息
4、admin:OrderController:订单状态跟踪
5、WebSocketServer:来单/催单提醒(webSocket实时推送)
6、OrderTask:定时任务处理超时订单(Spring Task)
用户端(微信小程序)
1、user:UserController微信登陆
2、user:DishController/CategoryController商品浏览(Redis缓存)
3、user:ShoppingCartController:购物车管理
4、user:OrderController/PayNotifyController:下单支付
2、JWT令牌+ThreadLocal
2.1、JWT登录流程
使用JWT令牌和自定义拦截器完成用户认证的流程如下:
- 用户登录时,客户端发送用户名和密码给服务器请求令牌,服务器验证通过后生成包含用户信息的JWT令牌并返回给客户端。
- 客户端收到JWT令牌后将其存储在本地(如localStorage或cookie),后续每次请求都在请求头中携带该令牌(如Authorization:Bearer )。
- 服务器端的自定义拦截器会拦截所有请求,首先从请求头中提取JWT令牌并进行解析验证(包括检查令牌是否存在、签名是否有效、是否过期等),若令牌不存在或验证失败则直接返回401未认证错误。
- 当JWT令牌验证通过后,拦截器将解析出的用户信息存储在ThreadLocal中,使本次请求的后续业务处理流程可以直接获取用户信息,避免重复解析JWT令牌。
- 请求处理完成后,拦截器会调用remove()方法清除ThreadLocal中的用户信息,确保不会发生内存泄漏。
2.2、ThreadLocal使用过多会造成的影响?怎么解决内存泄漏的问题?
每个线程都有⼀个ThreadLocalMap的内部属性,map的key是ThreaLocal,定义为弱引用,value是强引用类型。垃圾回收的时候会⾃动回收key,但对应的 Value 仍然是强引用,且线程未结束时,ThreadLocalMap 会一直持有该 Value,导致内存泄漏。
解决⽅法:每次使⽤完ThreadLocal就调⽤它的remove()⽅法,手动将对应的键值对删除,从⽽避免内存泄漏
2.3 JWT的组成:
它由三部分组成:header(头部)、payload(载荷)、signature(签名)
- Header(头部) 作用:描述令牌的元数据,如签名算法(如HS256、RS256)和令牌类型(固定为JWT)。
- Payload(负载) 作用:携带实际的数据(声明),分为三类:
预定义声明:标准字段,如 iss(签发者)、exp(过期时间)、sub(主题)等。
公开声明:自定义公共字段,需避免冲突(建议通过IANA 注册)。
私有声明:双方约定的自定义数据。 - Signature(签名)作用:验证令牌的完整性和真实性。生成方式:将编码后的 Header 和 Payload 拼接后通过 Header 中指定的算法(如 HS256)和密钥进行签名。
3、Redis
3.1项目中哪里用到redis了,缓存的粒度是什么?key,value如何定义?
缓存商品 用户端小程序展示的商品数据都是通过查询数据库获得,如果用户端访问量比较大,数据库访问压力随之增大。结果是系统响应慢、用户体验差。通过Redis来缓存商品数据,减少数据库查询操作。缓存逻辑是每个分类的商品下缓存一份数据,数据库中商品数据有变更时及时清理数据。key是分类的id,value是该分类下所有商品信息
3.2、数据库与redis如何实现的数据同步
在项目中,我们采用分布式锁策略实现数据库与Redis的数据同步。通过Redis分布式锁(如Redisson)对数据ID细粒度加锁,确保同一时间只有一个请求能修改数据;在锁内严格按"先更新数据库→再删除缓存"的顺序操作,保证后续请求必然读取最新数据;
因为有库存价格等信息所以不适合用延时双删)。
修改频率较低所以加锁性能损耗不高
4、Nginx
4.1 负载均衡:
Nginx 的负载均衡功能允许将请求分发给多个应用服务器,以均衡负载和提高系统的可扩展性和可靠性。下面是一些常用的 Nginx 负载均衡配置方法:
- 轮询:这是默认的负载均衡策略。Nginx 将请求依次分发给每个后端服务器,确保每个服务器都能获得相同的请求数量。
- IP 哈希(IP Hash):Nginx 使用客户端 IP地址的哈希值来决定将请求发送给哪个后端服务器。
- 加权轮询:可以为每个后端服务器设置权重,高权重的服务器将获得更多的请求。这种方式可以根据服务器的性能和处理能力来分配负载。
我们项目用的是轮询方式,共有2台后端服务器(一台本机,一台虚拟机)
在nginx.conf配置:
upstream webservers{
server 127.0.0.1:8080 weight=90 ;
#server 127.0.0.1:8088 weight=10 ;
}
4.2 反向代理与正向代理:
反向代理隐藏服务器,正向代理隐藏客户端。
- 正向代理是客户端发送请求后通过代理服务器访问目标服务器,代理服务器代表客户端发送请求并将响应返回给客户端。正向代理隐藏了客户端的真实身份和位置信息,为客户端提供代理访问互联网的功能。
- 反向代理是位于目标服务器和客户端之间的代理服务器,它代表服务器接收客户端的请求并将请求转发到真正的目标服务器上,并将得到的响应返回给客户端。反向代理隐藏了服务器的真实身份和位置信息,客户端只知道与反向代理进行通信,而不知道真正的服务器。
反向代理优点:
-
提高访问速度,因为nginx本身可以进行缓存,如果访问的同一接口,并且做了数据缓存,nginx就直接可把数据返回,不需要真正地访问服务端,从而提高访问速度。
-
进行负载均衡,把大量的请求按照我们指定的方式均衡的分配给集群中的每台服务器。
-
保证后端服务安全因为一般后台服务地址不会暴露,所以使用浏览器不能直接访问,可以把nginx作为请求访问的入口,请求到达nginx后转发到具体的服务中,从而保证后端服务的安全。
反向代理配置。在nginx.conf配置
server {
listen 80;
server_name localhost;
# 反向代理,处理管理端发送的请求
location /api/ {
proxy_pass http://localhost:8080/admin/;
#proxy_pass http://webservers/admin/;
}
# 反向代理,处理用户端发送的请求
location /user/ {
proxy_pass http://webservers/user/;
}
}
5、webSocket
5.1 什么是webSocket
WebSocket是基于TCP的一种新的网络协议。它实现了浏览器与服务器全双工通信——浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的连接,并进行双向数据传输。
5.2、WebSocket和HTTP协议的区别?为什么不用HTTP
| 特性 | WebSocket | HTTP |
|---|---|---|
| 连接性质 | 全双工(双向实时通信) | 半双工(单向请求-响应) |
| 持久性 | 长连接(建立后持续保持) | 短连接(默认每次请求后关闭) |
| 主动推送 | 服务端可主动推送数据 | 服务端只能被动响应客户端请求 |
这两个功能对实时性要求极高,传统的HTTP请求-响应模式无法满足毫秒级推送的需求,因此我们采用WebSocket协议实现服务端主动推送,确保商家能第一时间处理订单。
5.3 WebSocket在项目中的应用场景
我们使用WebSocket实现了两个核心功能: ✅ 来单实时提醒:当用户下单并支付成功后,系统立即通知商家端有新订单。 ✅ 客户催单处理:用户点击催单按钮后,商家端实时收到催单通知,并触发语音播报。
通过WebSocket实现管理端页面和服务端保持长连接状态。当客户支付完成或者点击催单后,调用WebSocket的相关API实现服务端向客户端推送消息,客户端浏览器解析服务端推送的消息,判断是来单提醒还是客户催单,进行相应的消息提醒和语言播报
6、SpringTask
6.1、springTask怎么实现的超时自动取消订单的功能?
Spring Task(Spring 任务调度)是 Spring 框架提供的一种任务调度框架,用于执行定时任务、异步任务、任务监听、任务调度等。
苍穹外卖采用Spring Task实现订单超时自动取消功能,核心逻辑是每分钟扫描一次数据库中订单,筛选出创建时间超过15分钟且状态为"待支付"的订单,批量修改为"已取消"状态
7、数据库相关
7.1 数据库表你是怎么设计的
| 序号 | 表名 |
|---|---|
| 1 | employee(员工表) |
| 2 | category(分类表) |
| 3 | dish(菜品表) |
| 4 | dish_flavor(菜品口味表) |
| 5 | setmeal(套餐表) |
| 6 | setmeal_dish(套餐菜品关系表) |
| 7 | user(用户表) |
| 8 | address_book(地址表) |
| 9 | shopping_cart(购物车表) |
| 10 | orders(订单表) |
| 11 | order_detail(订单明细表) |
7.2 为什么用逻辑外键,而不用数据库自带的外键?
- 减少数据库开销:物理外键约束会增加数据库在插入、更新和删除操作时的额外开销。数据库需要花费额外的时间来检查外键约束的完整性,这在高并发、大数据量的场景下可能会对性能产生明显的影响。而逻辑外键由应用程序来控制,开发人员可以根据具体业务场景,在必要时才进行关联数据的检查,避免了数据库层面不必要的检查操作,从而提高系统的整体性能
- 适应变化:在软件开发过程中,业务需求可能会不断变化。如果使用物理外键,当表结构发生变化时,例如修改外键关联的字段类型、删除关联表中的字段等操作,可能会受到外键约束的限制,导致数据库结构的修改变得复杂和困难。而逻辑外键不存在这种限制,开发人员可以更自由地对表结构进行调整,只需要在应用程序中相应地修改逻辑外键的处理逻辑即可,提高了数据库设计的灵活性和可维护性。
8、微信相关
8.1 微信登录是怎么实现的?
- 小程序端,调用wx.login()获取code,就是授权码。
- 小程序端,调用wx.request()发送请求并携带code,请求开发者服务器(自己编写的后端服务)。
- 开发者服务端,通过HttpClient向微信接口服务发送请求,并携带appId+appsecret+code三个参数。
- 开发者服务端,接收微信接口服务返回的数据,session_key+opendId等。opendId是微信用户的唯一标识。
- 开发者服务端,自定义登录态,生成令牌(token)和openid等数据返回给小程序端,方便后绪请求身份校验。
- 小程序端,收到自定义登录态,存储storage。
- 小程序端,后绪通过wx.request()发起业务请求时,携带token。
- 开发者服务端,收到请求后,通过携带的token,解析当前登录用户的id。
- 开发者服务端,身份校验通过后,继续相关的业务逻辑处理,最终返回业务数据。
8.2如何实现的微信支付功能?

完成微信支付有两个关键的步骤:
1️⃣ 就是需要在商户系统当中调用微信后台的一个下单接口,就是生成预支付交易单。
2️⃣ 就是支付成功之后微信后台会给推送消息。
9、你在项目中遇到了什么困难?
9.1处理公共字段的填充
如果都按照之前的操作方式来处理这些公共字段(创建时间,创建人id,修改时间,修改人id), 需要在每一个业务方法中进行操作, 编码相对冗余、繁琐。我们使用AOP切面编程,实现功能增强,来完成公共字段自动填充功能。
实现步骤:
1). 自定义注解 AutoFill,用于标识需要进行公共字段自动填充的方法
2). 自定义切面类 AutoFillAspect,统一拦截加入了 AutoFill 注解的方法,通过反射为公共字段赋值
3). 在 Mapper 的方法上加入 AutoFill 注解
更多推荐


所有评论(0)