-
Notifications
You must be signed in to change notification settings - Fork 0
Refactor/domain foundation | 实施计划中的 Phase 1 基座改造与版本化迁移 #19
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| ## Plan: Phase 1 基座改造与版本化迁移详细设计 | ||
|
|
||
| 本阶段在不改变 Spring Boot + Thymeleaf 主体架构的前提下,优先完成可持续演进底座:统一角色语义、稳定领域模型、引入可回滚迁移、预埋采集与导入导出扩展点、统一异常与审计口径。这样可避免 Phase 2-7 重复改表和改权限,保证后续功能可并行推进且回归成本可控。 | ||
|
|
||
| ### Steps | ||
| 1. Phase 1A 基线冻结与差距对齐(无依赖) | ||
| - 建立当前实现 vs 课程条款映射清单,覆盖 3.1、3.2、3.3(仅非推荐部分)、3.4(仅数据导入导出)。 | ||
| - 明确 Phase 1 只做底座,不提前实现分析算法、推荐系统、导入导出业务流程页面。 | ||
| - 输出三份基线工件:领域字段字典、角色矩阵草案、迁移影响清单。 | ||
|
|
||
| 2. Phase 1B 角色模型统一与命名规范(depends on 1) | ||
| - 统一角色域为 CUSTOMER、SALES、ADMIN(保留 Spring Security 的 ROLE_ 前缀映射规则)。 | ||
| - 规定角色枚举为唯一真源,控制器与模板只消费标准化角色语义,避免字符串散落。 | ||
| - 定义登录后路由占位策略:Customer 主站、Sales 工作台、Admin 管理台。 | ||
|
|
||
| 3. Phase 1C 领域模型补齐(depends on 1,可与 2 并行) | ||
| - 商品域:补齐类别和库存基础能力,确保 Phase 3 可直接进入目录管理与库存策略实现。 | ||
| - 用户域:预留画像必需基础维度(如地域字段)与统一更新时间字段。 | ||
| - 日志域:扩展为可分析结构(操作者身份、模块、操作类型、目标对象、结果、IP、时间、请求追踪标识等)。 | ||
| - 兼容策略:新增字段提供默认值与向后兼容读取。 | ||
|
|
||
| 4. Phase 1D 引入版本化迁移机制并完成首批迁移脚本(depends on 2 and 3) | ||
| - 从 JPA update 自动改表迁移到版本化脚本主导,建立可审计、可回滚、可重放的演进流程。 | ||
| - 首批迁移覆盖:角色标准化、商品分类与库存字段、日志扩展字段、必要索引与初始角色数据补齐。 | ||
| - 迁移设计要求:幂等、可重复执行、失败可回滚、支持本地/dev/docker 同步。 | ||
|
|
||
| 5. Phase 1E 匿名浏览与登录后能力解耦(depends on 2,可与 4 并行) | ||
| - 路由策略分层:公开可读(商品浏览)与登录后可写(购物车、下单、管理操作)明确分离。 | ||
| - 页面策略:未登录访问商品列表可见,触发加购/下单动作时引导登录并保留回跳上下文。 | ||
|
|
||
| 6. Phase 1F 统一异常处理与错误码口径(depends on 1,可与 2/3 并行) | ||
| - 建立统一错误语义:参数校验失败、资源不存在、权限不足、业务冲突、并发冲突、系统异常。 | ||
| - 控制器层统一处理异常并输出一致反馈模型。 | ||
|
|
||
| 7. Phase 1G 导入导出滚动伴生交付预埋(depends on 3 and 4) | ||
| - Phase 1 不实现导入导出业务功能,但完成字段字典、CSV 列命名规范、模板版本号规则、导入校验错误模型。 | ||
| - 权限基线:Admin 全量导入导出;Sales 仅职责内对象可导入导出;职责外对象导入与导出双禁。 | ||
| - 滚动安排:Phase 3 对接 7A,Phase 4 对接 7B,Phase 5 对接 7C,Phase 6 对接 7D。 | ||
|
|
||
| 8. Phase 1H 测试基线与验收门禁(depends on 2-7) | ||
| - 建立 Phase 1 验收脚本:角色与路由、迁移执行、旧数据兼容、页面回归、日志结构完整性。 | ||
| - 增加最小自动化覆盖:服务层关键路径单元测试 + 权限与页面访问集成测试。 | ||
|
|
||
| ### Branch | ||
| - Phase 1 对应分支:chore/domain-foundation | ||
|
|
||
| ### Scope Boundaries | ||
| - Included:角色语义统一、领域模型补齐、迁移机制、匿名浏览解耦、异常口径、导入导出契约预埋、Phase 1 测试门禁。 | ||
| - Excluded:推荐系统、分析算法实现、可视化页面、完整导入导出功能、反爬虫、移动端适配。 | ||
|
|
||
| ### Relevant files | ||
| - docs/大三下-课程项目要求/《网络应用开发》课程设计.pdf.md | ||
| - .github/prompts/plan-javaWebDesignLessonExpEcommerce02.prompt.md | ||
| - src/main/java/org/self4215/config/SecurityConfig.java | ||
| - src/main/java/org/self4215/config/CustomAuthenticationSuccessHandler.java | ||
| - src/main/java/org/self4215/config/CustomLogoutSuccessHandler.java | ||
| - src/main/java/org/self4215/controller/ProductController.java | ||
| - src/main/java/org/self4215/controller/LoginController.java | ||
| - src/main/java/org/self4215/service/UserService.java | ||
| - src/main/java/org/self4215/service/ProductService.java | ||
| - src/main/java/org/self4215/service/LogService.java | ||
| - src/main/java/org/self4215/entity/User.java | ||
| - src/main/java/org/self4215/entity/Product.java | ||
| - src/main/java/org/self4215/entity/OperationLog.java | ||
| - src/main/resources/application.properties | ||
| - src/main/resources/data.sql | ||
| - pom.xml |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -29,9 +29,27 @@ public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse resp | |||||||||||||||||||||||||||||||||||||
| if (authentication != null && authentication.getPrincipal() instanceof UserDetails) { | ||||||||||||||||||||||||||||||||||||||
| UserDetails userDetails = (UserDetails) authentication.getPrincipal(); | ||||||||||||||||||||||||||||||||||||||
| User user = userService.getCurrentUser(userDetails.getUsername()); | ||||||||||||||||||||||||||||||||||||||
| logService.logAction(user, "注销", null, "用户注销成功"); | ||||||||||||||||||||||||||||||||||||||
| logService.logAction( | ||||||||||||||||||||||||||||||||||||||
| user, | ||||||||||||||||||||||||||||||||||||||
| "注销", | ||||||||||||||||||||||||||||||||||||||
| null, | ||||||||||||||||||||||||||||||||||||||
| "用户注销成功", | ||||||||||||||||||||||||||||||||||||||
| "AUTH", | ||||||||||||||||||||||||||||||||||||||
| "LOGOUT", | ||||||||||||||||||||||||||||||||||||||
| "USER", | ||||||||||||||||||||||||||||||||||||||
| "SUCCESS", | ||||||||||||||||||||||||||||||||||||||
| extractClientIp(request), | ||||||||||||||||||||||||||||||||||||||
| request.getRequestedSessionId()); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| response.sendRedirect("/login?logout"); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| private String extractClientIp(HttpServletRequest request) { | ||||||||||||||||||||||||||||||||||||||
| String forwardedFor = request.getHeader("X-Forwarded-For"); | ||||||||||||||||||||||||||||||||||||||
| if (forwardedFor != null && !forwardedFor.isBlank()) { | ||||||||||||||||||||||||||||||||||||||
| return forwardedFor.split(",")[0].trim(); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| return request.getRemoteAddr(); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+41
to
+53
|
||||||||||||||||||||||||||||||||||||||
| extractClientIp(request), | |
| request.getRequestedSessionId()); | |
| } | |
| response.sendRedirect("/login?logout"); | |
| } | |
| private String extractClientIp(HttpServletRequest request) { | |
| String forwardedFor = request.getHeader("X-Forwarded-For"); | |
| if (forwardedFor != null && !forwardedFor.isBlank()) { | |
| return forwardedFor.split(",")[0].trim(); | |
| } | |
| return request.getRemoteAddr(); | |
| } | |
| request.getRemoteAddr(), | |
| request.getRequestedSessionId()); | |
| } | |
| response.sendRedirect("/login?logout"); | |
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| package org.self4215.controller; | ||
|
|
||
| import jakarta.servlet.http.HttpServletRequest; | ||
| import org.self4215.exception.AppErrorCode; | ||
| import org.springframework.stereotype.Controller; | ||
| import org.springframework.ui.Model; | ||
| import org.springframework.web.bind.annotation.GetMapping; | ||
|
|
||
| import java.time.Instant; | ||
|
|
||
| @Controller | ||
| public class ErrorPageController { | ||
|
|
||
| @GetMapping("/error/common") | ||
| public String commonError(HttpServletRequest request, Model model) { | ||
| Object errorCode = request.getAttribute("errorCode"); | ||
| Object errorMessage = request.getAttribute("errorMessage"); | ||
| Object httpStatus = request.getAttribute("httpStatus"); | ||
| Object path = request.getAttribute("path"); | ||
| Object timestamp = request.getAttribute("timestamp"); | ||
|
|
||
| model.addAttribute("errorCode", errorCode != null ? errorCode : AppErrorCode.SYSTEM_ERROR.name()); | ||
| model.addAttribute("errorMessage", errorMessage != null ? errorMessage : "系统出现异常,请稍后重试。"); | ||
| model.addAttribute("httpStatus", httpStatus != null ? httpStatus : 500); | ||
| model.addAttribute("path", path != null ? path : request.getRequestURI()); | ||
| model.addAttribute("timestamp", timestamp != null ? timestamp : Instant.now().toString()); | ||
|
|
||
| return "error/common"; | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
extractClientIpis duplicated across multiple classes and trusts theX-Forwarded-Forheader unconditionally, which makes audit IPs trivially spoofable unless the app is behind a trusted proxy with forwarded-header handling configured. Consider centralizing this logic (e.g., a shared util/service) and using Spring’s forwarded header support (ForwardedHeaderFilter/server.forward-headers-strategy) so you only trust proxy headers when appropriate.