# 会话记录:时区国际化功能 ## 元数据 - 需求编号:2026-01-21-002-04 - 创建时间:2026-01-25 - 创建人:SSOT 架构师 - 状态:进行中 - 父需求:2026-01-21-002-项目国际化需求 ## 执行阶段 ### 阶段 1:需求定义 - **状态**:已完成 - **生成文档**:[需求文档](../requirements/2026-01-21-002-04-时区国际化需求.md) - **关键决策**: - 所有时间字段存储为 UTC 时间 - 时区转换在 Service 层进行 - 使用 Redis 缓存时区配置 - 时区切换后立即刷新页面 - 支持用户 > 租户 > 系统的时区优先级 - 时区列表固定存储在数据库中 ### 阶段 2:方案设计 - **状态**:已完成 - **生成文档**:[设计文档](../design/2026-01-21-002-04-时区国际化设计.md) - **关键设计决策**: - 使用 Java TimeZone API(Java 21 内置)进行时区转换 - 使用 Redis 缓存时区配置,提高性能 - 时区转换在 Service 层进行,使用 AOP 切面拦截响应 - 时区优先级:用户 > 租户 > 系统 - 时区列表固定存储在数据库中,后续不允许用户调整 - 时区切换后清除 Redis 缓存并刷新页面 - 所有时间字段存储为 UTC 时间(LocalDateTime) - 缓存策略:时区列表 24 小时,用户时区 30 分钟 ### 阶段 3:方案决策 - **状态**:已完成 - **生成文档**:[决策记录](../decisions/adr/2026-01-25-002-04-ADR-时区国际化技术选型.md) - **关键决策**: - **决策 1:时区转换技术选择** - Java TimeZone API(Java 21 内置) - 理由:无额外依赖、功能完整、自动处理夏令时、性能优秀、成熟稳定、与框架兼容 - 放弃方案:Joda-Time 库(需要额外依赖、已进入维护模式)、自定义时区偏移实现(维护成本高、难以处理夏令时) - **决策 2:缓存策略选择** - Redis 缓存 - 理由:已集成、性能优秀、分布式支持、自动过期、数据结构丰富、支持持久化 - 缓存策略:时区列表缓存(TTL = 24 小时)、用户时区缓存(TTL = 30 分钟) - 放弃方案:Caffeine 本地缓存(无法在分布式环境下共享)、数据库缓存(性能较差、无法自动过期) - **决策 3:时区转换层选择** - Service 层转换 + AOP 切面拦截 - 理由:统一处理、AOP 切面自动转换、业务逻辑分离、易于维护、性能优化 - 实现方案:使用 @Around 切面拦截 Service 方法返回值,提供 @TimeZoneConvert 注解 - 放弃方案:Controller 层转换(职责过重、代码重复)、数据库层转换(违反分层架构、兼容性差) - **决策 4:时区优先级策略选择** - 多级优先级策略(用户 > 租户 > 系统) - 理由:灵活性高、用户体验好、租户隔离、系统默认、易于扩展 - 优先级规则:用户时区 > 租户时区 > 系统时区 > 硬编码默认 - 放弃方案:单级优先级(仅用户时区/仅系统时区)- 灵活性不足、无法满足多租户场景 ### 阶段 4:数据库结构 - **状态**:已完成 - **生成文档**:[SQL 脚本](../sql/2026-01-25-002-04-timezone-internationalization.sql) - **数据库变更**: - **创建新表**:sys_timezone(时区配置表) - 字段:id、timezone_id、timezone_name、timezone_offset、is_default、sort_order、status、create_by、create_time、update_by、update_time、remark - 索引:主键索引(id)、唯一索引(timezone_id)、普通索引(status)、普通索引(sort_order) - **修改现有表**:sys_user(用户表) - 新增字段:time_zone(时区 ID),默认值为 Asia/Shanghai - 新增索引:idx_time_zone(time_zone 字段) - **插入初始数据**: - 插入 20 个常用时区配置(包括中国、美国、欧洲、亚洲、澳洲等) - 插入时区状态数据字典(sys_timezone_status) - 插入时区默认标识数据字典(sys_timezone_default) ### 阶段 5:提示词生成 - **状态**:已完成 - **生成文档**:[提示词文档](../prompts/2026-01-25-002-04-prompt-时区国际化功能.md) - **方案变更**: - **原方案**:AOP 拦截 Controller 方法返回值 - **新方案**:AOP 拦截 Service 方法返回值 - **变更原因**: - 所有调用 Service 的地方都会触发转换(Controller、定时任务、内部调用等),覆盖面更广 - 转换逻辑统一在 AOP 切面中,无需手动调用 - 时区转换逻辑与业务逻辑分离,代码清晰 - 定时任务、内部调用等也能自动转换 - **影响范围**: - 设计文档:已更新时区转换实现部分 - 提示词文档:已更新 Aspect 层实现部分 - 架构决策记录:已更新决策 3 的内容 - **提示词内容摘要**: - **引用真源**:需求文档、设计文档、决策记录、SQL 脚本 - **需求描述**:时区转换功能、时区管理功能、时区优先级策略、缓存管理、自动时区转换 - **设计方案**:Java TimeZone API、Redis、MySQL 8.3.0、Spring Boot 3.5.7 + 若依框架、分层架构 - **输出格式要求**: - Entity 层:SysTimezone.java - 修改现有文件:SysUser.java(添加 time_zone 字段) - Utils 层:TimeZoneUtils.java - 注解:@TimeZoneConvert.java - Aspect 层:TimeZoneConvertAspect.java(拦截 Service 方法返回值) - Mapper 层:SysTimezoneMapper.java、SysTimezoneMapper.xml - Service 层:ISysTimezoneService.java、SysTimezoneServiceImpl.java - Controller 层:SysTimezoneController.java - 单元测试:TimeZoneUtilsTest.java、SysTimezoneServiceTest.java - **代码规范要求**:命名规范、注释规范、代码格式、导入规范 - **测试要求**:单元测试覆盖率 ≥ 80%、测试用例场景、测试框架、测试用例命名规范、测试数据 - **注意事项**:时区转换、缓存、权限控制、日志记录、异常处理、性能优化、安全、AOP 切面 ### 阶段 6:代码生成 - **状态**:已完成 - **生成文档**:[代码文档](../reference-code/2026-01-25-002-04-code-时区国际化功能.md)、[实施方案](../implementation/2026-01-25-002-04-implementation-时区国际化功能.md) - **代码生成情况**: - **代码生成器生成**: - SysTimezone.java(实体类) - SysTimezoneMapper.java(Mapper 接口) - SysTimezoneMapper.xml(Mapper XML) - SysTimezoneService.java(Service 接口基础方法) - SysTimezoneServiceImpl.java(Service 实现类基础方法) - SysTimezoneController.java(Controller 类基础接口) - **手动生成/扩展**: - ISysTimezoneService.java:扩展方法(getCurrentUserTimeZone、switchTimeZone、getDefaultTimeZone、getUserTimeZone) - SysTimezoneServiceImpl.java:实现扩展方法,包含 Redis 缓存逻辑 - TimeZoneUtils.java:时区转换工具类(convertUtcToTimeZone、convertTimeZoneToUtc、isValidTimeZone、getAllAvailableTimeZones、getDefaultTimeZone、getTimeZoneOffset) - @TimeZoneConvert.java:时区转换注解 - TimeZoneConvertAspect.java:时区转换切面(拦截 Service 方法返回值) - SysUser.java:添加 time_zone 字段 - TimeZoneUtilsTest.java:单元测试类 - **关键实现**: - 使用 AOP 切面拦截 Service 方法返回值进行时区转换 - Redis 缓存策略:时区列表缓存 24 小时,用户时区缓存 30 分钟 - 递归转换复杂对象中的 LocalDateTime 字段 - 异常处理:无效时区使用默认时区,null 值安全处理 - **修复的问题**: - I18nController.java 中的 TokenService 导入错误(com.datai.web.service → com.datai.framework.web.service) - I18nController.java 中的 RedisUtils 导入错误(替换为 RedisCache) - RedisUtils.deleteObject() 方法不存在(使用 RedisCache.keys() + RedisCache.deleteObject(Collection)) - **补充的接口**: - GET /system/timezone/current:获取当前用户时区 - POST /system/timezone/switch:切换用户时区(已添加 @Log 注解记录审计日志) - GET /system/timezone/default:获取系统默认时区(已添加 @PreAuthorize 注解控制访问权限) - **需求实现情况**: - ✅ 功能 1:时区设置 - 已实现 - ✅ 功能 2:时区转换 - 已实现 - ✅ 功能 3:时区显示 - 已实现(新增接口) - ✅ 功能 4:时区列表管理 - 已实现 - ✅ 功能 5:时区切换 - 已实现(新增接口,含审计日志) - ✅ 功能 6:时区缓存管理 - 已实现 - ⚠️ 功能 7:时区优先级管理 - 部分实现(缺少租户时区支持) - **未实现的需求**: - 租户时区支持(功能 7 的部分需求) - 数据缓存策略(在原有缓存键基础上添加时区后缀) ### 阶段 7:会话记录 - **状态**:已完成 - **生成文档**:当前文档(2026-01-25-002-04-session.md) - **更新内容**: - 更新阶段 6 的代码生成情况 - 记录补充的接口信息 - 记录需求实现情况 - 记录未实现的需求 ### 阶段 8:变更日志 - **状态**:待开始 - **生成文档**:待生成 ### 阶段 9:复盘与接口 - **状态**:待开始 - **生成文档**:待生成 ### 阶段 10:代码提交 - **状态**:待开始 - **生成文档**:待生成 ## 关键设计决策 ### 技术选型 1. **时区转换技术**:Java TimeZone API(Java 21 内置) - 理由:Java 21 内置,无需引入额外依赖;支持所有 IANA 时区 ID;自动处理夏令时转换;性能优秀,转换时间 < 10ms;成熟稳定,社区支持良好 2. **缓存技术**:Redis - 理由:项目已集成 Redis,无需额外配置;性能优秀,响应时间 < 1ms;支持分布式部署;支持自动过期机制;丰富的数据结构支持 3. **数据库技术**:MySQL 8.3.0 - 理由:项目现有数据库;支持 LocalDateTime 类型;性能优秀,支持高并发;事务支持完善 4. **框架技术**:Spring Boot 3.5.7 + 若依框架 - 理由:项目现有框架;Spring Boot 3.5.7 支持 Java 21;若依框架提供完善的权限、缓存、日志等功能;社区活跃,文档完善 ### 架构设计 1. **系统架构**:前端层(Vue 3)→ Controller 层 → Service 层 → Mapper 层 → 数据库层(MySQL 8.3.0)→ 缓存层(Redis) 2. **模块架构**: - datai-admin(启动模块):Controller 层 - datai-system(系统模块):Service 层、Mapper 层、实体类 - datai-common(公共模块):工具类、切面 - datai-plugins(插件模块):Redis 缓存工具类 ### 数据流设计 1. **用户登录流程**:用户输入用户名密码 → Controller 接收登录请求 → Service 验证用户信息 → 从数据库读取用户时区偏好 → 从 Redis 缓存读取时区配置 → 将用户时区信息存储到 LoginUser → 生成 Token 并返回 → 前端存储 Token 和时区信息 2. **数据查询流程**:前端发起数据查询请求 → Controller 接收请求 → Service 层从数据库读取 UTC 时间 → AOP 切面拦截响应 → 根据用户时区转换时间 → 返回转换后的时间 → 前端显示转换后的时间 3. **数据保存流程**:前端发起数据保存请求(携带用户时区时间)→ Controller 接收请求 → Service 层接收用户时区时间 → 将用户时区时间转换为 UTC 时间 → 保存 UTC 时间到数据库 → 返回保存结果 4. **时区切换流程**:用户选择新时区 → Controller 接收时区切换请求 → Service 验证时区 ID 有效性 → 更新用户时区偏好到数据库 → 清除 Redis 缓存 → 刷新 Token → 前端刷新页面,重新加载时间数据 ### 数据模型设计 1. **时区表(sys_timezone)**: - 字段:id、timezone_id、timezone_name、timezone_offset、is_default、sort_order、status、create_by、create_time、update_by、update_time、remark - 索引:主键索引(id)、唯一索引(timezone_id)、普通索引(status)、普通索引(sort_order) 2. **用户表修改(sys_user)**: - 新增字段:time_zone(时区 ID) - 默认值:Asia/Shanghai(中国标准时间) - 位置:在 lang_code 字段之后 ### 接口设计 1. **获取时区列表**:GET /system/timezone/list - 权限要求:@PreAuthorize("@ss.hasPermi('system:timezone:list')") - 响应数据:时区列表(包含 id、timezoneId、timezoneName、timezoneOffset、isDefault、sortOrder、status) 2. **获取当前用户时区**:GET /system/timezone/current - 权限要求:需要登录 - 响应数据:时区 ID(如:Asia/Shanghai) 3. **切换时区**:POST /system/timezone/switch - 权限要求:需要登录 - 请求参数:timeZone(时区 ID) - 响应数据:成功/失败消息 4. **获取默认时区**:GET /system/timezone/default - 权限要求:@PreAuthorize("@ss.hasPermi('system:timezone:query')") - 响应数据:默认时区 ID(如:Asia/Shanghai) ### 实现要点 1. **关键实现逻辑**: - 时区转换在 Service 层进行,使用 AOP 切面拦截响应 - 使用 TimeZoneUtils 工具类进行时区转换 - 处理 null 值,避免空指针异常 - 处理无效时区 ID,使用默认时区 2. **异常处理设计**: - 时区转换异常:捕获异常,记录日志,返回 UTC 时间,提示用户时区转换失败 - 时区 ID 无效异常:捕获异常,记录日志,返回错误响应,提示用户时区 ID 无效 - 缓存读取异常:捕获异常,记录日志,从数据库重新加载数据,提示用户缓存读取失败 3. **性能优化设计**: - 缓存优化:使用 Redis 缓存时区配置,时区列表缓存 24 小时,用户时区缓存 30 分钟 - 索引优化:为常用查询字段创建索引,提高查询性能 - 批量转换优化:批量转换时使用并行处理,合理设置线程池大小 4. **安全设计**: - 数据验证:验证时区 ID 有效性、格式、白名单 - 权限控制:使用 @PreAuthorize 注解控制接口权限,使用数据权限控制数据访问范围 - 审计日志:使用 @Log 注解记录操作日志,记录操作人、操作时间、操作内容 ## 相关文档 - [需求文档](../requirements/2026-01-21-002-04-时区国际化需求.md) - [设计文档](../design/2026-01-21-002-04-时区国际化设计.md)