From d8eae693f8f79275667c9ccf1a2d2ea49d61161d Mon Sep 17 00:00:00 2001 From: pu36s7yfv <2320898596@qq.com> Date: Sun, 15 Dec 2024 22:04:03 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=9A=E5=91=98=E4=B8=AD=E5=BF=83dcx=20(#1)?= =?UTF-8?q?=20Co-authored-by:=20pu36s7yfv=20<2320898596@qq.com>=20Co-commi?= =?UTF-8?q?tted-by:=20pu36s7yfv=20<2320898596@qq.com>?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- add | 0 .../src/main/java/com/tamguo/Application.java | 40 +- .../tamguo/config/dao/MybatisPlusConfig.java | 51 ++- .../com/tamguo/config/shiro/MemberRealm.java | 92 +++-- .../config/shiro/ShiroConfiguration.java | 63 +++ .../tamguo/config/web/ThymeleafConfig.java | 79 +++- .../java/com/tamguo/config/web/WebConfig.java | 72 +++- .../tamguo/interceptor/MemberInterceptor.java | 65 ++- .../java/com/tamguo/utils/FileMd5Utils.java | 42 +- .../java/com/tamguo/utils/ShiroUtils.java | 66 ++- tamguo/pom.xml | 382 ++++++++++-------- .../java/com/tamguo/config/WebConfig.java | 125 ++++-- .../tamguo/config/redis/SessionConfig.java | 21 +- .../com/tamguo/config/shiro/MemberRealm.java | 77 ++-- .../config/shiro/ShiroConfiguration.java | 49 ++- .../java/com/tamguo/dao/ChapterMapper.java | 22 +- .../main/java/com/tamguo/dao/PaperMapper.java | 63 ++- .../tamguo/interceptor/MemberInterceptor.java | 45 ++- .../main/java/com/tamguo/model/AdEntity.java | 32 +- .../java/com/tamguo/model/BookEntity.java | 78 +++- .../com/tamguo/service/impl/AdService.java | 35 +- .../com/tamguo/service/impl/AreaService.java | 71 +++- .../tamguo/service/impl/ChapterService.java | 103 +++-- .../tamguo/service/impl/MemberService.java | 242 ++++++----- .../com/tamguo/service/impl/MenuService.java | 127 +++++- .../com/tamguo/service/impl/PaperService.java | 272 ++++++------- .../tamguo/service/impl/QuestionService.java | 133 +++++- .../tamguo/service/impl/SchoolService.java | 75 +++- .../tamguo/service/impl/SubjectService.java | 82 +++- .../util/AbstractRunningLogHandler.java | 260 +++++++----- .../main/java/com/tamguo/util/DateUtils.java | 209 +++++----- .../src/main/java/com/tamguo/util/IdGen.java | 95 ++++- .../main/java/com/tamguo/util/ObjectUtil.java | 62 ++- .../main/java/com/tamguo/util/PageUtils.java | 119 +++--- .../java/com/tamguo/util/RequestHelper.java | 36 +- .../src/main/java/com/tamguo/util/Result.java | 109 ++++- .../main/java/com/tamguo/util/Uploader.java | 280 +++++-------- .../java/com/tamguo/web/BookController.java | 56 ++- .../java/com/tamguo/web/CourseController.java | 84 +++- .../com/tamguo/web/FileUploadController.java | 83 +++- .../java/com/tamguo/web/IndexController.java | 59 ++- .../java/com/tamguo/web/LoginController.java | 130 ++++-- .../java/com/tamguo/web/PaperController.java | 189 +++++++-- .../com/tamguo/web/PasswordController.java | 129 ++++-- .../com/tamguo/web/QuestionContrller.java | 125 ++++-- .../com/tamguo/web/RegisterController.java | 74 +++- .../com/tamguo/web/SubjectController.java | 104 +++-- .../com/tamguo/web/UEditorController.java | 139 +++++-- .../tamguo/web/member/AccountController.java | 99 ++++- .../web/member/MemberPaperController.java | 135 +++++-- .../tamguo/web/member/QuestionController.java | 112 +++-- .../tamguo/web/teacher/JoinusController.java | 35 +- 52 files changed, 3770 insertions(+), 1557 deletions(-) create mode 100644 add diff --git a/add b/add new file mode 100644 index 0000000..e69de29 diff --git a/tamguo-bms/src/main/java/com/tamguo/Application.java b/tamguo-bms/src/main/java/com/tamguo/Application.java index d1d90b8..01e8688 100644 --- a/tamguo-bms/src/main/java/com/tamguo/Application.java +++ b/tamguo-bms/src/main/java/com/tamguo/Application.java @@ -10,27 +10,59 @@ import com.alibaba.fastjson.serializer.SerializerFeature; import com.alibaba.fastjson.support.config.FastJsonConfig; import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter; +// 这是Spring Boot应用的主启动类,使用了 @SpringBootApplication 注解,该注解是一个组合注解,相当于同时使用了 @Configuration、@EnableAutoConfiguration 和 @ComponentScan 注解, +// 意味着它标记这个类是一个配置类,会自动配置Spring Boot应用的很多默认设置,并且会自动扫描和加载应用中的各个组件(如各种Bean、Controller、Service等)。 @SpringBootApplication +// 通过 @ComponentScan 注解指定要扫描的基础包路径,这里设置为 "com.tamguo",表示Spring会在该包及其子包下查找带有Spring相关注解(如 @Component、@Service、@Controller 等)的类,并将它们注册为Spring容器中的组件,方便后续进行依赖注入等操作。 @ComponentScan("com.tamguo") public class Application { + /** + * 应用的主入口方法,用于启动Spring Boot应用,通过创建 SpringApplicationBuilder 对象并传入当前的 Application 类作为配置类, + * 然后调用 run 方法并传入命令行参数(args)来启动整个Spring Boot应用,启动过程中会进行一系列的初始化操作,如加载配置文件、创建Spring容器、扫描组件、自动配置等,最终启动嵌入式的Web服务器(如果是Web应用的话)并监听相应的端口等待请求。 + * + * @param args 命令行参数,可用于传递一些外部配置信息给应用,例如指定运行的端口号、配置文件的路径等,在应用启动时可以根据这些参数进行相应的初始化配置调整,不过这里暂时未对这些参数进行详细的处理逻辑(可根据实际需求扩展)。 + */ public static void main(String[] args) { new SpringApplicationBuilder(Application.class).run(args); } - + /** * FastJson替代Jackson - * @return + * 该方法创建并配置了一个用于将对象转换为JSON格式数据的HttpMessageConverters(消息转换器)的Bean, + * 在Spring Boot应用中,默认使用Jackson作为JSON序列化和反序列化的工具,但在这里通过自定义配置,使用FastJson来替代Jackson进行JSON数据的处理, + * 可以按照项目的特定需求对JSON序列化的格式(如日期格式)以及一些特性(如禁用循环引用检测)进行配置,使得生成的JSON数据更符合项目要求,方便与前端等进行数据交互。 + * + * @return 返回配置好的HttpMessageConverters对象,Spring容器会管理该对象,并在处理HTTP请求和响应中涉及JSON数据转换时应用该消息转换器,将Java对象转换为JSON格式发送给客户端,或者将接收到的JSON数据转换为Java对象进行处理。 */ @Bean public HttpMessageConverters fastJsonHttpMessageConverters() { + // 创建一个FastJsonHttpMessageConverter实例,它是基于FastJson实现的HTTP消息转换器,用于将Java对象转换为FastJson格式的JSON字符串,或者将接收到的FastJson格式的JSON字符串转换为Java对象, + // 在后续配置中会设置它的相关属性,使其按照项目需求进行JSON数据的转换操作。 FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter(); + + // 创建一个FastJsonConfig实例,用于配置FastJson的相关参数,比如设置日期格式、序列化特性等,通过该配置对象可以定制化FastJson序列化和反序列化的行为,使其符合项目的具体要求。 FastJsonConfig fastJsonConfig = new FastJsonConfig(); + + // 设置FastJson序列化日期类型数据时的格式,这里设置为 "yyyy-MM-dd HH:mm:ss",使得在将包含日期属性的Java对象转换为JSON字符串时,日期字段会按照该格式进行序列化,方便前端进行统一的日期格式展示和处理, + // 例如数据库中的日期时间类型数据在转换为JSON发送给前端时,会以指定的这种常见的格式化字符串形式呈现,便于前端直接展示或者进行日期相关的操作(如格式化、比较等)。 fastJsonConfig.setDateFormat("yyyy-MM-dd HH:mm:ss"); + + // 设置FastJson的序列化特性,这里通过SerializerFeature.DisableCircularReferenceDetect来禁用循环引用检测, + // 当Java对象之间存在复杂的关联关系,可能形成循环引用时(比如对象A包含对象B,对象B又包含对象A这样的情况),默认情况下FastJson会进行循环引用检测并做相应处理, + // 但有时候可能希望关闭这种检测(根据具体业务场景和数据结构确定),通过设置该特性可以改变FastJson在序列化时对循环引用的处理方式,避免可能出现的序列化异常或者生成不符合预期的JSON结构等问题。 fastJsonConfig.setSerializerFeatures(SerializerFeature.DisableCircularReferenceDetect); + + // 将配置好的FastJsonConfig对象设置到FastJsonHttpMessageConverter中,使得消息转换器在进行JSON数据转换时应用这些配置参数,按照设定的日期格式和序列化特性来处理Java对象与JSON字符串之间的转换操作。 fastConverter.setFastJsonConfig(fastJsonConfig); + + // 将配置好的FastJsonHttpMessageConverter赋值给converter变量(这里其实可以直接使用fastConverter,赋值这一步略显多余,但不影响功能实现,可能是代码风格或者后续潜在扩展的考虑), + // 后续会使用该变量来创建并返回HttpMessageConverters对象。 FastJsonHttpMessageConverter converter = fastConverter; + + // 创建并返回一个HttpMessageConverters对象,将配置好的FastJsonHttpMessageConverter作为参数传入,这样Spring容器就会识别并使用这个自定义的消息转换器来处理HTTP请求和响应中的JSON数据转换, + // 替代默认的Jackson相关的消息转换器,实现使用FastJson进行JSON数据处理的目的。 return new HttpMessageConverters(converter); } - -} + +} \ No newline at end of file diff --git a/tamguo-bms/src/main/java/com/tamguo/config/dao/MybatisPlusConfig.java b/tamguo-bms/src/main/java/com/tamguo/config/dao/MybatisPlusConfig.java index 47d0386..c5c3eeb 100644 --- a/tamguo-bms/src/main/java/com/tamguo/config/dao/MybatisPlusConfig.java +++ b/tamguo-bms/src/main/java/com/tamguo/config/dao/MybatisPlusConfig.java @@ -19,23 +19,40 @@ import com.baomidou.mybatisplus.plugins.parser.tenant.TenantSqlParser; import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.expression.LongValue; +// 标识这是一个Spring的配置类,用于配置MyBatis Plus相关的功能和插件,通过定义多个Bean方法来创建并配置不同的MyBatis Plus组件, +// 例如性能分析拦截器、分页插件、元对象处理器以及SQL注入器等,这些组件将被Spring容器管理并应用到MyBatis Plus的相关操作中,影响数据库访问的行为和功能。 @Configuration +// 配置MyBatis的Mapper扫描路径,告诉MyBatis Plus去指定的包及其子包下扫描接口作为Mapper接口,这样MyBatis Plus就能自动为这些Mapper接口生成对应的实现类并注入到Spring容器中,方便进行数据库操作。 @MapperScan("com.tamguo.modules.*.dao*") public class MybatisPlusConfig { + /** + * 创建并配置PerformanceInterceptor(性能拦截器)的Bean,该拦截器用于在MyBatis Plus执行SQL语句时收集并输出相关性能统计信息, + * 例如SQL执行的耗时等情况,方便在开发和测试阶段对数据库操作的性能进行分析和优化,了解哪些SQL语句执行效率较低,以便针对性地进行改进。 + * + * @return 返回创建好的PerformanceInterceptor实例,Spring容器会管理该实例,并在合适的时机将其应用到MyBatis Plus的相关操作链路中进行性能监控。 + */ @Bean public PerformanceInterceptor performanceInterceptor() { return new PerformanceInterceptor(); } /** - * mybatis-plus分页插件
- * 文档:http://mp.baomidou.com
+ * 创建并配置PaginationInterceptor(分页插件)的Bean,用于在MyBatis Plus中实现分页功能,它能够自动拦截并处理带有分页参数的查询请求, + * 根据配置将原始查询转换为带有分页逻辑的数据库查询语句(例如添加LIMIT子句等,具体取决于数据库类型),使得开发人员可以方便地在代码中使用分页功能进行数据查询操作。 + * 同时,这里还配置了与多租户相关的SQL解析处理逻辑(虽然是示例性质的固定配置,实际可根据业务场景调整),用于处理多租户场景下的数据隔离等问题。 + *

+ * 文档参考:http://mp.baomidou.com + * + * @return 返回配置好的PaginationInterceptor实例,Spring容器会管理该实例,并在执行涉及分页的数据库查询操作时应用该插件进行分页处理以及相关的SQL解析操作。 */ @Bean public PaginationInterceptor paginationInterceptor() { PaginationInterceptor paginationInterceptor = new PaginationInterceptor(); - paginationInterceptor.setLocalPage(true);// 开启 PageHelper 的支持 + // 设置开启PageHelper的支持,这里的PageHelper可能是指与分页相关的某种兼容模式或者特定功能支持(具体依赖于MyBatis Plus内部实现以及与其他分页相关框架的整合情况), + // 通过开启此功能,可以更好地适配一些已有的分页相关的使用习惯或者与其他分页工具协同工作(具体根据实际业务场景确定)。 + paginationInterceptor.setLocalPage(true); + /* * 【测试多租户】 SQL 解析处理拦截器
* 这里固定写成住户 1 实际情况你可以从cookie读取,因此数据看不到 【 麻花藤 】 这条记录( 注意观察 SQL )
@@ -45,24 +62,28 @@ public class MybatisPlusConfig { tenantSqlParser.setTenantHandler(new TenantHandler() { @Override public Expression getTenantId() { + // 返回表示租户ID的表达式,这里固定返回一个值为1L的LongValue表达式,表示当前租户的ID为1,实际业务中通常需要从合适的地方(如请求头、Cookie等)动态获取租户ID。 return new LongValue(1L); } @Override public String getTenantIdColumn() { + // 返回用于标识租户ID的数据库表列名,这里指定为"course_id",意味着在多租户场景下,通过该列的值来区分不同租户的数据,即该列存储了租户的相关标识信息。 return "course_id"; } @Override public boolean doTableFilter(String tableName) { - // 这里可以判断是否过滤表 + // 这里可以判断是否过滤表,即根据表名决定是否应用多租户的过滤逻辑(例如某些公共表可能不需要进行租户隔离,可以返回true进行过滤,不应用租户相关的SQL解析处理), + // 当前直接返回true,表示默认对所有表应用过滤逻辑,实际业务中需要根据具体的表结构和业务需求来准确判断哪些表需要进行租户相关处理,哪些表不需要。 return true; } }); - + // 将配置好的TenantSqlParser添加到SQL解析器列表中,后续分页拦截器会遍历该列表中的解析器,对SQL语句依次进行解析处理,应用相应的逻辑(如多租户数据隔离等)。 sqlParserList.add(tenantSqlParser); paginationInterceptor.setSqlParserList(sqlParserList); + // 以下过滤方式与 @SqlParser(filter = true) 注解等效 // paginationInterceptor.setSqlParserFilter(new ISqlParserFilter() { // @Override @@ -75,19 +96,31 @@ public class MybatisPlusConfig { // return false; // } // }); + return paginationInterceptor; } + /** + * 创建并配置MetaObjectHandler(元对象处理器)的Bean,该处理器用于在MyBatis Plus执行插入和更新操作时,自动填充一些公共字段的值, + * 例如创建时间、更新时间、创建人、更新人等字段,通过实现该处理器的相关方法,可以定义统一的字段填充逻辑,避免在每个Mapper方法中手动设置这些公共字段的值,提高代码的复用性和开发效率。 + * + * @return 返回创建好的MetaObjectHandler实例(这里返回的是自定义的MyMetaObjectHandler,其应该继承自MetaObjectHandler并实现了相应的填充逻辑,具体实现类的逻辑根据业务需求而定), + * Spring容器会管理该实例,并在执行插入和更新操作时调用其相关方法进行字段的自动填充操作。 + */ @Bean - public MetaObjectHandler metaObjectHandler(){ + public MetaObjectHandler metaObjectHandler() { return new MyMetaObjectHandler(); } /** - * 注入sql注入器 + * 创建并配置ISqlInjector(SQL注入器)的Bean,用于向MyBatis Plus注入一些自定义的SQL方法,这里注入的是LogicSqlInjector, + * 它通常用于实现逻辑删除等功能(例如在数据库表中通过一个字段标识数据是否被删除,而不是真正从数据库中删除数据,方便数据的恢复以及保留数据的历史记录等), + * 通过注入这个SQL注入器,可以在MyBatis Plus的Mapper接口中方便地使用逻辑删除相关的方法,而不需要手动编写复杂的SQL语句来实现逻辑删除逻辑。 + * + * @return 返回创建好的LogicSqlInjector实例作为SQL注入器,Spring容器会管理该实例,并使其生效,使得在MyBatis Plus的相关操作中可以使用其提供的自定义SQL功能(如逻辑删除功能)。 */ @Bean - public ISqlInjector sqlInjector(){ + public ISqlInjector sqlInjector() { return new LogicSqlInjector(); } -} +} \ No newline at end of file diff --git a/tamguo-bms/src/main/java/com/tamguo/config/shiro/MemberRealm.java b/tamguo-bms/src/main/java/com/tamguo/config/shiro/MemberRealm.java index 6a55f8d..d5112a6 100644 --- a/tamguo-bms/src/main/java/com/tamguo/config/shiro/MemberRealm.java +++ b/tamguo-bms/src/main/java/com/tamguo/config/shiro/MemberRealm.java @@ -1,6 +1,7 @@ package com.tamguo.config.shiro; import java.util.Set; + import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; @@ -20,53 +21,88 @@ import com.tamguo.modules.member.service.IMemberService; /** * 认证 - * + * 该类继承自Shiro框架中的AuthorizingRealm,用于实现自定义的认证(登录验证)和授权(权限验证)逻辑, + * 在Shiro的安全框架体系中,Realm是用于连接应用程序的安全数据(如用户信息、权限信息等)与Shiro框架的桥梁, + * 在这里通过重写相应的方法,结合业务层的服务(IMemberService)来实现基于数据库中会员信息的认证和授权操作,例如验证用户登录时输入的用户名和密码是否正确、判断账号是否被锁定、更新登录相关信息以及获取用户权限信息等功能。 */ public class MemberRealm extends AuthorizingRealm { - + + // 自动注入IMemberService,用于调用会员相关的业务逻辑方法,比如根据用户名查找会员信息、获取登录失败次数、更新登录失败次数、更新最后登录时间等操作, + // 这些操作都是在认证(验证用户登录合法性)和授权(后续可能基于会员角色等信息获取权限)过程中需要依赖的业务逻辑实现。 @Autowired private IMemberService iMemberService; - - /** - * 授权(验证权限时调用) - */ + + /** + * 授权(验证权限时调用) + * 该方法是Shiro框架要求重写的用于获取授权信息的方法,当需要进行权限验证(例如访问某个受权限控制的资源时),Shiro会调用此方法来获取当前用户对应的权限信息, + * 在这里目前只是简单地创建了一个空的权限集合,并将其设置到SimpleAuthorizationInfo对象中返回,实际业务中需要根据具体的业务逻辑从数据库等数据源获取用户所拥有的权限信息并进行相应设置, + * 例如根据用户所属角色、用户直接关联的权限等方式来确定权限集合内容,然后通过info对象返回给Shiro框架用于后续的权限判断操作。 + * + * @param principals 包含了当前用户主体信息的集合,通过该集合可以获取到登录成功后的用户相关信息(例如用户名、用户对象等,具体取决于登录认证时设置的主体信息内容), + * 通常可以基于这些信息去查询数据库获取对应的权限信息,但当前方法只是简单示例,暂未充分利用该参数进行实际的权限查询操作。 + * @return 返回包含权限信息的AuthorizationInfo对象(这里返回的是SimpleAuthorizationInfo实例),Shiro框架会根据该对象中的权限信息来判断当前用户是否具有访问特定资源的权限。 + */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { - Set permsSet = null; + // 目前只是初始化一个空的权限集合,实际业务中应从数据库等数据源查询并填充相应的权限信息,例如根据用户角色、用户直接关联的权限等获取权限列表。 + Set permsSet = null; SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); + // 将权限集合设置到授权信息对象中,当前为空集合,表示没有赋予任何权限,实际情况需要根据业务逻辑查询并设置正确的权限信息。 info.setStringPermissions(permsSet); - + return info; } /** * 认证(登录时调用) + * 该方法是Shiro框架要求重写的用于进行用户认证(登录验证)的方法,当用户发起登录操作时,Shiro会调用此方法来验证用户输入的用户名和密码等凭证信息是否正确, + * 在这里通过获取登录凭证中的用户名和密码,调用会员服务查找对应的会员信息,然后进行一系列验证操作,如判断用户是否存在、账号是否被锁定、密码是否匹配等, + * 根据验证结果抛出相应的异常(由Shiro框架统一处理并返回给客户端合适的错误提示)或者返回认证成功的信息(包含用户对象和密码等信息的AuthenticationInfo对象)。 + * + * @param token 包含用户登录凭证信息的对象,例如用户名和密码等信息,Shiro框架会将前端传来的登录信息封装到该对象中传递给此方法进行验证, + * 在这里可以通过token.getPrincipal()获取用户名,通过token.getCredentials()获取密码(通常需要进行类型转换等处理)。 + * @return 返回包含认证成功信息的AuthenticationInfo对象(这里返回的是SimpleAuthenticationInfo实例),如果验证通过则将相关的用户信息、密码以及当前Realm的名称等信息设置到该对象中返回给Shiro框架, + * 表示登录认证成功;如果验证过程中出现异常情况(如用户不存在、密码错误、账号锁定等),则会抛出相应的AuthenticationException异常,由Shiro框架统一处理并返回错误提示给客户端。 + * @throws AuthenticationException 如果在认证过程中出现任何不符合验证规则的情况(如用户不存在、密码错误、账号锁定等),则抛出该异常,Shiro框架会根据异常类型进行相应的错误处理和提示返回。 */ @Override - protected AuthenticationInfo doGetAuthenticationInfo( - AuthenticationToken token) throws AuthenticationException { + protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { + // 从登录凭证对象中获取用户名信息,通常前端输入的用户名会被封装到token中作为主体信息,这里将其从token中取出并转换为字符串类型,用于后续查询数据库验证用户是否存在等操作。 String username = (String) token.getPrincipal(); - String password = new String((char[]) token.getCredentials()); - - MemberEntity member = iMemberService.findByUsername(username); - if(member == null) { - throw new UnknownAccountException("用户名或密码有误,请重新输入或找回密码"); - } - Integer loginFailureCount = iMemberService.getLoginFailureCount(member); - if(loginFailureCount > 10) { - throw new LockedAccountException("账号被锁定"); - } - - if(!new Sha256Hash(password).toHex().equals(member.getPassword())){ + // 从登录凭证对象中获取密码信息,token.getCredentials()返回的是字符数组类型,将其转换为字符串类型,用于后续与数据库中存储的用户密码进行比对验证操作。 + String password = new String((char[]) token.getCredentials()); + + // 通过会员服务,根据用户名去数据库中查找对应的会员实体信息,用于后续验证该用户是否存在以及密码是否匹配等操作,如果查找不到则表示用户不存在,需要抛出相应异常。 + MemberEntity member = iMemberService.findByUsername(username); + if (member == null) { + // 如果根据用户名未找到对应的会员信息,抛出UnknownAccountException异常,表示用户名不存在或者未找到对应的用户信息,Shiro框架会捕获该异常并返回相应的错误提示给客户端。 + throw new UnknownAccountException("用户名或密码有误,请重新输入或找回密码"); + } + + // 通过会员服务获取当前会员账号的登录失败次数,用于后续判断账号是否因为多次登录失败而被锁定,具体的锁定阈值(这里是10次)根据业务需求设定,可调整。 + Integer loginFailureCount = iMemberService.getLoginFailureCount(member); + if (loginFailureCount > 10) { + // 如果登录失败次数超过设定的阈值(10次),抛出LockedAccountException异常,表示账号被锁定,Shiro框架会捕获该异常并返回相应的错误提示给客户端。 + throw new LockedAccountException("账号被锁定"); + } + + // 将用户输入的密码进行哈希处理(使用Sha256Hash算法进行加密,这里将用户输入的密码转换为十六进制字符串形式,方便与数据库中存储的加密后的密码进行比对), + // 然后与数据库中存储的会员密码进行比对,如果不相等则表示密码错误,需要进行相应的错误处理操作(如记录登录失败次数、抛出异常等)。 + if (!new Sha256Hash(password).toHex().equals(member.getPassword())) { + // 如果密码不匹配,先将登录失败次数加1,表示又一次登录失败,然后调用会员服务更新数据库中该会员账号的登录失败次数记录。 loginFailureCount++; - iMemberService.updateLoginFailureCount(member , loginFailureCount); + iMemberService.updateLoginFailureCount(member, loginFailureCount); + // 抛出IncorrectCredentialsException异常,表示密码错误,Shiro框架会捕获该异常并返回相应的错误提示给客户端。 throw new IncorrectCredentialsException("用户名或密码有误,请重新输入或找回密码"); } - // 更新登录时间 + + // 如果密码验证通过,调用会员服务更新数据库中该会员账号的最后登录时间,记录用户此次登录的时间信息,方便后续进行一些统计分析或者基于登录时间的业务逻辑处理(如判断账号是否长时间未登录等情况)。 iMemberService.updateLastLoginTime(member.getId()); - - SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(member, password, getName()); - return info; + + // 创建一个SimpleAuthenticationInfo对象,用于返回认证成功的信息给Shiro框架,将找到的会员实体对象(member)作为主体信息、用户输入的密码(password)以及当前Realm的名称(通过getName()方法获取)设置到该对象中, + // 表示登录认证成功,Shiro框架后续会基于这些信息进行会话管理等相关操作,并且可以在后续的请求处理中通过Shiro的相关API获取当前登录用户的信息等内容。 + SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(member, password, getName()); + return info; } -} +} \ No newline at end of file diff --git a/tamguo-bms/src/main/java/com/tamguo/config/shiro/ShiroConfiguration.java b/tamguo-bms/src/main/java/com/tamguo/config/shiro/ShiroConfiguration.java index 96f9ab3..bf7aa6d 100644 --- a/tamguo-bms/src/main/java/com/tamguo/config/shiro/ShiroConfiguration.java +++ b/tamguo-bms/src/main/java/com/tamguo/config/shiro/ShiroConfiguration.java @@ -12,27 +12,59 @@ import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreato import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +// 标识这是一个Spring的配置类,用于配置Shiro框架相关的组件和功能,通过定义多个Bean方法来创建并配置Shiro中的核心组件, +// 如Realm(用于认证和授权的领域对象)、缓存管理器、安全管理器、过滤器工厂等,这些组件协同工作,为Web应用提供基于角色和权限的安全访问控制功能。 @Configuration public class ShiroConfiguration { + + // 定义一个静态的LinkedHashMap,用于存储URL路径与对应的Shiro过滤器链定义,其中键表示URL的匹配模式,值表示对应的过滤器链定义(例如需要进行何种认证、授权等操作), + // 后续会将该映射配置到ShiroFilterFactoryBean中,用于控制不同URL的访问权限和安全策略。 private static Map filterChainDefinitionMap = new LinkedHashMap(); + /** + * 创建并配置MemberRealm(自定义的Realm,用于实现具体的认证和授权逻辑,应该继承自Shiro的相关Realm类并实现了特定的业务逻辑)的Bean, + * 该Realm会被注入到安全管理器(SecurityManager)中,作为Shiro进行用户认证和授权操作的核心逻辑处理单元,根据业务需求从数据库等数据源获取用户信息、权限信息等进行相应验证。 + * + * @return 返回创建好的MemberRealm实例,Spring容器会管理该实例,并将其提供给其他Shiro组件(如安全管理器)使用,以实现认证和授权功能。 + */ @Bean(name = "shiroRealm") public MemberRealm getShiroRealm() { return new MemberRealm(); } + /** + * 创建并配置EhCacheManager(基于EhCache的缓存管理器)的Bean,用于管理Shiro框架中的缓存,例如缓存用户的认证信息、授权信息等, + * 可以提高系统性能,减少频繁查询数据库等操作,通过设置配置文件路径(classpath:ehcache-shiro.xml)来加载具体的缓存配置信息,确定缓存的策略、过期时间等相关设置。 + * + * @return 返回创建好的EhCacheManager实例,Spring容器会管理该实例,并将其注入到安全管理器等相关Shiro组件中,使其生效用于缓存管理。 + */ @Bean(name = "shiroEhcacheManager") public EhCacheManager getEhCacheManager() { EhCacheManager em = new EhCacheManager(); + // 设置EhCache的配置文件路径,该文件通常定义了缓存的名称、缓存的策略(如内存缓存、磁盘缓存等)、缓存的过期时间、最大缓存数量等相关配置信息, + // Shiro会根据该配置文件来初始化和管理缓存,这里使用classpath路径表示从类路径下查找该配置文件。 em.setCacheManagerConfigFile("classpath:ehcache-shiro.xml"); return em; } + /** + * 创建并配置LifecycleBeanPostProcessor(生命周期Bean后置处理器)的Bean,用于管理Shiro组件在Spring容器中的生命周期, + * 它可以自动调用Shiro组件实现了相关生命周期接口的方法(如初始化、销毁方法等),确保Shiro组件在正确的时机进行相应的初始化和资源释放操作,保证系统的稳定性和资源管理的合理性。 + * + * @return 返回创建好的LifecycleBeanPostProcessor实例,Spring容器会管理该实例,并使其作用于Shiro组件的生命周期管理过程中。 + */ @Bean(name = "lifecycleBeanPostProcessor") public LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() { return new LifecycleBeanPostProcessor(); } + /** + * 创建并配置DefaultAdvisorAutoProxyCreator(默认的顾问自动代理创建器)的Bean,用于在Spring AOP框架中创建基于顾问(Advisor)的自动代理, + * 在Shiro与Spring整合时,它可以帮助自动创建代理对象,使得Shiro的安全拦截等功能能够通过AOP机制应用到相应的方法或类上,实现权限控制等安全相关的切面逻辑, + * 通过设置proxyTargetClass为true,表示使用基于类的代理方式(而不是基于接口的代理方式,具体取决于业务场景和需求)。 + * + * @return 返回创建好的DefaultAdvisorAutoProxyCreator实例,Spring容器会管理该实例,并使其在Spring AOP和Shiro整合过程中发挥作用,创建合适的代理对象用于安全控制。 + */ @Bean public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() { DefaultAdvisorAutoProxyCreator daap = new DefaultAdvisorAutoProxyCreator(); @@ -40,14 +72,30 @@ public class ShiroConfiguration { return daap; } + /** + * 创建并配置DefaultWebSecurityManager(默认的Web安全管理器)的Bean,它是Shiro框架在Web应用中进行安全管理的核心组件, + * 负责协调和管理其他安全相关的组件,如Realm(用于认证和授权的具体逻辑实现)、缓存管理器(用于缓存相关信息以提高性能)等, + * 通过将自定义的MemberRealm和EhCacheManager注入到其中,使其具备了根据业务逻辑进行用户认证、授权以及缓存管理的能力,从而实现整个Web应用的安全访问控制功能。 + * + * @return 返回创建好的DefaultWebSecurityManager实例,Spring容器会管理该实例,并将其作为核心的安全管理组件应用到Shiro的整个安全体系中,例如配置到ShiroFilterFactoryBean中用于处理请求的安全验证等操作。 + */ @Bean(name = "securityManager") public DefaultWebSecurityManager getDefaultWebSecurityManager() { DefaultWebSecurityManager dwsm = new DefaultWebSecurityManager(); + // 设置自定义的Realm,将之前创建的MemberRealm实例注入到安全管理器中,使得安全管理器在进行认证和授权操作时能够调用MemberRealm中实现的具体业务逻辑。 dwsm.setRealm(getShiroRealm()); + // 设置缓存管理器,将之前创建的EhCacheManager实例注入到安全管理器中,使得安全管理器能够利用缓存来管理用户认证信息、授权信息等,提高系统性能和响应速度。 dwsm.setCacheManager(getEhCacheManager()); return dwsm; } + /** + * 创建并配置AuthorizationAttributeSourceAdvisor(授权属性源顾问)的Bean,它是Shiro与Spring AOP整合用于处理授权相关切面逻辑的组件, + * 通过设置安全管理器(将之前创建的DefaultWebSecurityManager实例注入其中),使得在进行方法调用等操作时,能够基于Shiro的授权机制对方法进行权限验证, + // 例如判断当前用户是否具有访问某个被@RequiresPermissions等授权注解标注的方法的权限,从而实现细粒度的权限控制功能。 + * + * @return 返回创建好的AuthorizationAttributeSourceAdvisor实例,Spring容器会管理该实例,并使其在Spring AOP和Shiro整合的授权控制过程中发挥作用,对方法进行权限验证等操作。 + */ @Bean public AuthorizationAttributeSourceAdvisor getAuthorizationAttributeSourceAdvisor() { AuthorizationAttributeSourceAdvisor aasa = new AuthorizationAttributeSourceAdvisor(); @@ -55,15 +103,30 @@ public class ShiroConfiguration { return new AuthorizationAttributeSourceAdvisor(); } + /** + * 创建并配置ShiroFilterFactoryBean(Shiro过滤器工厂Bean)的Bean,它是Shiro在Web应用中配置URL过滤器链的核心组件, + * 用于定义不同URL路径需要应用的Shiro过滤器(如认证过滤器、授权过滤器等),从而实现对Web资源的访问控制,通过设置安全管理器、登录页面URL、成功登录后的跳转URL以及具体的过滤器链定义映射等参数, + * 来定制整个Web应用的安全访问策略,例如哪些URL需要用户登录后才能访问,哪些URL可以匿名访问等情况。 + * + * @return 返回配置好的ShiroFilterFactoryBean实例,Spring容器会管理该实例,并根据其配置的过滤器链定义等信息,对Web请求进行相应的安全拦截和处理,实现Web应用的安全访问控制功能。 + */ @Bean(name = "shiroFilter") public ShiroFilterFactoryBean getShiroFilterFactoryBean() { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); + // 设置安全管理器,将之前创建的DefaultWebSecurityManager实例注入到ShiroFilterFactoryBean中,使得过滤器工厂能够利用安全管理器进行认证、授权等安全相关的操作, + // 根据安全管理器中配置的Realm、缓存管理器等组件来判断请求是否合法、用户是否具有访问权限等情况。 shiroFilterFactoryBean.setSecurityManager(getDefaultWebSecurityManager()); + // 设置登录页面的URL,当用户访问需要认证但未登录的资源时,Shiro会自动重定向到该URL,让用户进行登录操作,这里设置为"/login",表示应用中对应的登录页面路径。 shiroFilterFactoryBean.setLoginUrl("/login"); + // 设置用户成功登录后的跳转URL,当用户登录成功后,Shiro会自动重定向到该URL,这里设置为"/index",表示登录成功后默认跳转到应用中的首页路径(具体根据业务需求确定)。 shiroFilterFactoryBean.setSuccessUrl("/index"); + + // 将定义好的URL路径与对应的Shiro过滤器链定义的映射添加到ShiroFilterFactoryBean中,例如将"/member/**"路径设置为需要进行认证("authc"表示需要进行身份认证的过滤器), + // 将"/**"路径设置为可以匿名访问("anon"表示允许匿名访问的过滤器),通过这样的配置来精确控制不同URL资源的访问权限,实现灵活的安全访问策略。 filterChainDefinitionMap.put("/member/**", "authc"); filterChainDefinitionMap.put("/**", "anon"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); + return shiroFilterFactoryBean; } } \ No newline at end of file diff --git a/tamguo-bms/src/main/java/com/tamguo/config/web/ThymeleafConfig.java b/tamguo-bms/src/main/java/com/tamguo/config/web/ThymeleafConfig.java index 7c7be0b..bf8d203 100644 --- a/tamguo-bms/src/main/java/com/tamguo/config/web/ThymeleafConfig.java +++ b/tamguo-bms/src/main/java/com/tamguo/config/web/ThymeleafConfig.java @@ -11,32 +11,79 @@ import org.thymeleaf.spring5.view.ThymeleafViewResolver; import com.tamguo.common.utils.SystemConstant; +// 标识这是一个Spring的组件类,用于配置Thymeleaf视图相关的一些静态变量,通过实现EnvironmentAware接口可以获取Spring的应用程序环境信息, +// 进而从环境配置中获取特定的属性值,并将这些属性值以及一些常量值设置到ThymeleafViewResolver的静态变量中,使得在Thymeleaf模板中可以方便地使用这些变量进行页面渲染等操作。 @Component -public class ThymeleafConfig implements EnvironmentAware{ +public class ThymeleafConfig implements EnvironmentAware { + // 通过@Resource注解注入Spring的Environment对象,用于获取应用程序的配置属性信息,例如从配置文件(如application.properties或application.yml等)中读取各种自定义的配置项值。 @Resource - private Environment env; + private Environment env; + /** + * 配置Thymeleaf视图解析器的静态变量的方法,通过该方法向ThymeleafViewResolver对象中设置一系列静态变量, + * 这些变量的值一部分来自于应用程序的配置属性(通过Environment对象获取),另一部分来自于自定义的系统常量(通过SystemConstant类获取), + * 设置后的静态变量可以在Thymeleaf模板文件中直接使用,方便在页面渲染时进行动态数据展示或者根据不同的变量值进行不同的页面逻辑处理(如根据不同的域名显示不同的链接等情况)。 + * 此方法通过方法参数注入的方式获取ThymeleafViewResolver对象,Spring会自动查找并注入对应的实例供该方法使用。 + * + * @param viewResolver 要配置静态变量的ThymeleafViewResolver对象,用于设置各种静态变量,使其在视图解析和页面渲染过程中能够被Thymeleaf模板访问和使用。 + */ @Resource private void configureThymeleafStaticVars(ThymeleafViewResolver viewResolver) { - if(viewResolver != null) { - Map vars = new HashMap<>(); - vars.put("domainName", env.getProperty("domain.name")); - vars.put("adminDomain", env.getProperty("admin.domain.name")); - vars.put("memberDomain", env.getProperty("member.domain.name")); - vars.put("tamguoDomain", env.getProperty("tamguo.domain.name")); - vars.put("PAPER_TYPE_ZHENTI", SystemConstant.ZHENGTI_PAPER_ID); - vars.put("PAPER_TYPE_MONI", SystemConstant.MONI_PAPER_ID); - vars.put("PAPER_TYPE_YATI", SystemConstant.YATI_PAPER_ID); - vars.put("PAPER_TYPE_MINGXIAO", SystemConstant.MINGXIAO_PAPER_ID); - vars.put("BEIJING_AREA_ID", SystemConstant.BEIJING_AREA_ID); - viewResolver.setStaticVariables(vars); - } + if (viewResolver!= null) { + // 创建一个HashMap用于存储要设置的静态变量,键为变量名,值为对应的变量值,后续会将这些变量设置到ThymeleafViewResolver中,以便在Thymeleaf模板中使用。 + Map vars = new HashMap<>(); + + // 从应用程序的配置属性中获取名为"domain.name"的属性值,并将其作为"domainName"变量的值存储到vars映射中, + // 该变量可能用于在Thymeleaf模板中生成完整的域名相关的链接、资源引用等情况,例如拼接完整的图片地址、外部接口地址等,具体根据业务需求确定。 + vars.put("domainName", env.getProperty("domain.name")); + + // 从应用程序的配置属性中获取名为"admin.domain.name"的属性值,并将其作为"adminDomain"变量的值存储到vars映射中, + // 该变量可能用于在Thymeleaf模板中针对管理员相关的功能模块生成特定域名的链接、资源引用等情况,例如管理员登录页面的域名、管理员后台接口的域名等,具体根据业务中管理员模块的需求确定。 + vars.put("adminDomain", env.getProperty("admin.domain.name")); + + // 从应用程序的配置属性中获取名为"member.domain.name"的属性值,并将其作为"memberDomain"变量的值存储到vars映射中, + // 该变量可能用于在Thymeleaf模板中针对会员相关的功能模块生成特定域名的链接、资源引用等情况,例如会员中心页面的域名、会员相关接口的域名等,具体根据业务中会员模块的需求确定。 + vars.put("memberDomain", env.getProperty("member.domain.name")); + + // 从应用程序的配置属性中获取名为"tamguo.domain.name"的属性值,并将其作为"tamguoDomain"变量的值存储到vars映射中, + // 该变量可能用于在Thymeleaf模板中针对特定业务域名(以tamguo相关的业务场景)生成链接、资源引用等情况,具体根据业务中该域名对应的功能模块需求确定。 + vars.put("tamguoDomain", env.getProperty("tamguo.domain.name")); + + // 从自定义的SystemConstant类中获取名为ZHENGTI_PAPER_ID的常量值,并将其作为"PAPER_TYPE_ZHENTI"变量的值存储到vars映射中, + // 该变量可能用于在Thymeleaf模板中区分不同类型的试卷相关逻辑处理,例如根据试卷类型显示不同的样式、进行不同的操作按钮显示等情况,具体根据业务中试卷模块的需求确定。 + vars.put("PAPER_TYPE_ZHENTI", SystemConstant.ZHENGTI_PAPER_ID); + + // 从自定义的SystemConstant类中获取名为MONI_PAPER_ID的常量值,并将其作为"PAPER_TYPE_MONI"变量的值存储到vars映射中, + // 该变量可能用于在Thymeleaf模板中区分不同类型的试卷相关逻辑处理,例如根据试卷类型显示不同的样式、进行不同的操作按钮显示等情况,具体根据业务中试卷模块的需求确定。 + vars.put("PAPER_TYPE_MONI", SystemConstant.MONI_PAPER_ID); + + // 从自定义的SystemConstant类中获取名为YATI_PAPER_ID的常量值,并将其作为"PAPER_TYPE_YATI"变量的值存储到vars映射中, + // 该变量可能用于在Thymeleaf模板中区分不同类型的试卷相关逻辑处理,例如根据试卷类型显示不同的样式、进行不同的操作按钮显示等情况,具体根据业务中试卷模块的需求确定。 + vars.put("PAPER_TYPE_YATI", SystemConstant.YATI_PAPER_ID); + + // 从自定义的SystemConstant类中获取名为MINGXIAO_PAPER_ID的常量值,并将其作为"PAPER_TYPE_MINGXIAO"变量的值存储到vars映射中, + // 该变量可能用于在Thymeleaf模板中区分不同类型的试卷相关逻辑处理,例如根据试卷类型显示不同的样式、进行不同的操作按钮显示等情况,具体根据业务中试卷模块的需求确定。 + vars.put("PAPER_TYPE_MINGXIAO", SystemConstant.MINGXIAO_PAPER_ID); + + // 从自定义的SystemConstant类中获取名为BEIJING_AREA_ID的常量值,并将其作为"BEIJING_AREA_ID"变量的值存储到vars映射中, + // 该变量可能用于在Thymeleaf模板中涉及北京地区相关的逻辑处理,例如根据地区ID进行特定地区的业务展示、数据筛选等情况,具体根据业务中地区相关模块的需求确定。 + vars.put("BEIJING_AREA_ID", SystemConstant.BEIJING_AREA_ID); + + // 将包含所有静态变量的映射设置到ThymeleafViewResolver对象中,使得这些变量可以在Thymeleaf模板中被访问和使用,实现配置的生效,方便页面渲染时的动态数据展示和逻辑处理。 + viewResolver.setStaticVariables(vars); + } } + /** + * 实现EnvironmentAware接口的方法,用于设置当前类中的Environment对象,Spring容器会在初始化该类时自动调用此方法, + * 将应用程序的环境对象传递进来,以便在类中可以通过该对象获取各种配置属性信息,在这里将传入的environment对象赋值给类中已声明的env对象,方便后续使用。 + * + * @param environment Spring的应用程序环境对象,包含了应用程序的配置属性、系统环境变量等信息,通过该对象可以获取到各种自定义的配置项值以及系统相关的环境变量值等内容。 + */ @Override public void setEnvironment(Environment environment) { env = environment; } -} +} \ No newline at end of file diff --git a/tamguo-bms/src/main/java/com/tamguo/config/web/WebConfig.java b/tamguo-bms/src/main/java/com/tamguo/config/web/WebConfig.java index 2b2faad..859b0a5 100644 --- a/tamguo-bms/src/main/java/com/tamguo/config/web/WebConfig.java +++ b/tamguo-bms/src/main/java/com/tamguo/config/web/WebConfig.java @@ -12,32 +12,70 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import com.tamguo.interceptor.MemberInterceptor; +// 标识这是一个Spring的配置类,用于配置Spring Web应用相关的功能,通过实现WebMvcConfigurer接口并重写其中的方法, +// 可以对资源处理器、拦截器以及Cookie序列化等方面进行定制化配置,以满足项目特定的业务需求,例如设置文件资源的访问路径、添加会员相关请求的拦截逻辑、配置Cookie的相关属性等。 @Configuration public class WebConfig implements WebMvcConfigurer { - - @Value("${file.storage.path}") - private String fileStoragePath; - @Value("${cookie.domian.name}") - private String cookieDomianName; - @Autowired - private MemberInterceptor memberInterceptor; - + + // 通过@Value注解从配置文件(如application.properties或application.yml等)中注入文件存储路径属性值, + // 该路径用于指定服务器上存储文件的实际位置,后续在配置文件资源处理器时会用到,以便正确地将特定URL路径映射到该文件存储位置,实现文件的访问。 + @Value("${file.storage.path}") + private String fileStoragePath; + + // 通过@Value注解从配置文件中注入Cookie的域名属性值,该域名用于设置Cookie的作用域,决定了Cookie在哪些域名下可以被发送和接收, + // 在配置Cookie序列化相关逻辑时会使用该值来准确设置Cookie的域名属性,确保会话相关的Cookie在正确的域名范围内有效。 + @Value("${cookie.domian.name}") + private String cookieDomianName; + + // 自动注入MemberInterceptor,这是一个自定义的拦截器,用于在特定的请求路径下执行一些前置处理逻辑,比如可能用于验证会员的登录状态、权限等情况, + // 在配置拦截器时会将该拦截器添加到相应的请求路径上,使其生效并发挥作用。 + @Autowired + private MemberInterceptor memberInterceptor; + + /** + * 重写WebMvcConfigurer接口中的addResourceHandlers方法,用于配置资源处理器,将特定的URL路径与服务器上的实际文件存储位置进行映射, + * 使得当客户端访问指定的URL路径时,能够正确地从服务器对应的文件存储位置获取并返回相应的文件资源,例如图片、文档等静态文件资源。 + * + * @param registry 资源处理器注册对象,通过该对象可以添加不同的资源处理器配置,将URL路径模式与实际的文件资源位置进行关联。 + */ @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { - registry.addResourceHandler("/files/**").addResourceLocations("file:"+fileStoragePath); + // 将以"/files/"开头的所有URL路径(例如 "/files/image.jpg"、"/files/documents/report.pdf"等), + // 映射到服务器上以"file:"开头的实际文件存储路径(通过fileStoragePath变量获取),这样客户端访问 "/files/" 相关路径时,就能获取到存储在对应文件存储路径下的文件资源了。 + registry.addResourceHandler("/files/**").addResourceLocations("file:" + fileStoragePath); } - - @Override - public void addInterceptors(InterceptorRegistry registry) { - registry.addInterceptor(memberInterceptor).addPathPatterns("/member/**"); - } - + + /** + * 重写WebMvcConfigurer接口中的addInterceptors方法,用于添加自定义的拦截器到指定的请求路径上, + * 使得在这些请求被处理之前,拦截器能够执行相应的前置逻辑,比如进行会员登录状态验证、权限检查等操作,根据验证结果决定是否允许请求继续执行或者进行相应的跳转、提示等处理。 + * + * @param registry 拦截器注册对象,通过该对象可以添加不同的拦截器,并指定拦截器应用的请求路径模式,决定拦截器在哪些请求上生效。 + */ + @Override + public void addInterceptors(InterceptorRegistry registry) { + // 将自动注入的MemberInterceptor添加到以"/member/"开头的所有请求路径上(例如 "/member/profile"、"/member/orders"等), + // 意味着当客户端发起这些会员相关的请求时,MemberInterceptor中的逻辑会先被执行,进行相应的前置处理,例如检查当前用户是否已登录为会员,如果未登录则可能重定向到登录页面等操作。 + registry.addInterceptor(memberInterceptor).addPathPatterns("/member/**"); + } + + /** + * 创建并配置CookieSerializer(Cookie序列化器)的Bean,这里创建的是DefaultCookieSerializer的实例, + * 用于对Spring Session相关的Cookie进行序列化和反序列化操作,同时设置了Cookie的一些关键属性,如Cookie名称、域名以及路径等, + * 这些属性配置决定了Cookie在客户端和服务器之间的传输、存储以及作用范围等情况,确保会话相关的Cookie能按照项目需求正确工作。 + * + * @return 返回配置好的CookieSerializer实例,Spring容器会管理该实例,并将其应用到Spring Session相关的Cookie处理过程中,使其配置生效。 + */ @Bean - public CookieSerializer defaultCookieSerializer(){ + public CookieSerializer defaultCookieSerializer() { DefaultCookieSerializer defaultCookieSerializer = new DefaultCookieSerializer(); + // 设置Cookie的名称为"sessionId",这意味着在客户端和服务器之间传输的会话相关的Cookie名称将是"sessionId", + // 服务器通过该名称来识别和处理对应的会话信息,前端在发送请求时也会带上名为"sessionId"的Cookie来标识当前会话。 defaultCookieSerializer.setCookieName("sessionId"); + // 设置Cookie的域名,使用之前注入的cookieDomianName属性值,确定Cookie的作用域,使得Cookie只会在指定的域名下有效, + // 例如,如果域名设置为"example.com",那么只有访问该域名下的页面时,相关的Cookie才会被发送和接收,保证会话信息在正确的域名范围内传递。 defaultCookieSerializer.setDomainName(cookieDomianName); + // 设置Cookie的路径为"/",表示该Cookie在整个域名下的所有路径都有效,即无论访问域名下的哪个具体页面路径,都会带上这个Cookie,方便会话信息在整个应用的各个页面间共享和使用。 defaultCookieSerializer.setCookiePath("/"); return defaultCookieSerializer; } -} +} \ No newline at end of file diff --git a/tamguo-bms/src/main/java/com/tamguo/interceptor/MemberInterceptor.java b/tamguo-bms/src/main/java/com/tamguo/interceptor/MemberInterceptor.java index c26a535..1d52adc 100644 --- a/tamguo-bms/src/main/java/com/tamguo/interceptor/MemberInterceptor.java +++ b/tamguo-bms/src/main/java/com/tamguo/interceptor/MemberInterceptor.java @@ -8,48 +8,75 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; +// 标识这是一个Spring的组件类,用于拦截Web请求,它继承自HandlerInterceptorAdapter,通过重写其中的preHandle方法来实现自定义的请求拦截逻辑, +// 主要用于检查用户是否已登录(通过查看会话中是否存在特定的用户标识信息),根据不同的请求类型(如普通的GET请求、AJAX请求等)以及登录状态进行相应的处理, +// 例如未登录时对于AJAX请求返回特定的错误响应头和状态码,对于普通GET请求则重定向到登录页面并携带当前请求的URL作为重定向后的参数,对于其他非GET请求则直接重定向到登录页面。 @Component -public class MemberInterceptor extends HandlerInterceptorAdapter{ +public class MemberInterceptor extends HandlerInterceptorAdapter { - /** "重定向URL"参数名称 */ + /** + * "重定向URL"参数名称 + * 定义一个常量字符串,表示在重定向到登录页面时,用于传递当前请求的原始URL的参数名称,以便在登录成功后可以根据该参数值将用户重定向回原本想要访问的页面。 + */ private static final String REDIRECT_URL_PARAMETER_NAME = "redirectUrl"; - /** 默认登录URL */ + /** + * 默认登录URL + * 定义一个常量字符串,表示默认的登录页面的URL路径,当用户未登录且需要重定向到登录页面时,如果没有特别配置登录URL,则会使用这个默认的路径进行重定向操作,方便统一管理登录入口。 + */ private static final String DEFAULT_LOGIN_URL = "/login.html"; - /** 登录URL */ + /** + * 登录URL + * 用于存储实际的登录页面的URL路径,初始值为默认登录URL(DEFAULT_LOGIN_URL),可以通过配置文件注入等方式来修改这个值,使其指向项目中真正的登录页面路径,以适应不同的项目部署或配置需求。 + */ private String loginUrl = DEFAULT_LOGIN_URL; - + + // 通过@Value注解从配置文件(如application.properties或application.yml等)中注入名为"tamguo.domain.name"的属性值, + // 该域名通常用于构建完整的登录页面URL等情况,确保重定向到登录页面时使用的是正确的域名,使得在不同的部署环境下(域名可能不同)能准确访问到登录页面。 @Value("${tamguo.domain.name}") private String tamguoDomainName; - + /** * 请求前处理 - * - * @param request - * HttpServletRequest - * @param response - * HttpServletResponse - * @param handler - * 处理器 - * @return 是否继续执行 + * 重写HandlerInterceptorAdapter类中的preHandle方法,该方法会在请求被处理之前执行,用于实现自定义的请求拦截逻辑,在这里主要用于检查用户的登录状态, + * 根据登录状态以及请求的类型(如是否为AJAX请求、是否为GET请求等)来决定是允许请求继续执行还是进行相应的重定向、返回错误响应等操作,返回值决定了后续请求处理流程是否继续进行。 + * + * @param request 当前的HttpServletRequest对象,包含了请求的各种信息,如请求头、请求参数、会话信息等,可以通过它获取到当前请求的详细情况,用于判断登录状态以及构建重定向URL等操作。 + * @param response 当前的HttpServletResponse对象,用于向客户端发送响应,例如设置响应头、重定向页面、返回错误状态码等操作,根据不同的逻辑情况向客户端做出相应的响应。 + * @param handler 处理器对象,一般是被拦截的请求对应的处理器(如Controller中的方法等),在这里当前方法暂未对该参数做过多使用,但在一些复杂的拦截逻辑中可能会根据处理器的相关信息来做更精细的处理决定。 + * @return 返回一个布尔值,若返回true,则表示允许请求继续执行后续的处理流程(如调用对应的Controller方法等);若返回false,则表示拦截该请求,中断后续的请求处理流程,直接根据当前方法中的逻辑向客户端返回相应的响应。 + * @throws Exception 如果在执行拦截逻辑过程中出现异常(如获取会话属性异常、重定向操作异常等),则抛出该异常,可能会导致请求处理出现错误情况,一般需要根据实际业务进行合适的异常处理和日志记录等操作。 */ @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { - Object currMember = request.getSession().getAttribute("currMember"); - if (currMember != null) { + // 从当前请求的会话(Session)中获取名为"currMember"的属性值,该属性通常在用户登录成功后被设置,用于标识当前登录的用户信息, + // 如果能获取到该属性值(即不为null),表示用户已经登录,此时允许请求继续执行后续的处理流程,直接返回true。 + Object currMember = request.getSession().getAttribute("currMember"); + if (currMember!= null) { return true; } else { + // 获取请求头中名为"X-Requested-With"的字段值,该字段常用于判断请求是否为AJAX请求,不同的前端框架在发起AJAX请求时通常会设置这个请求头字段,方便后端进行区分处理。 String requestType = request.getHeader("X-Requested-With"); - if (requestType != null && requestType.equalsIgnoreCase("XMLHttpRequest")) { + if (requestType!= null && requestType.equalsIgnoreCase("XMLHttpRequest")) { + // 如果请求头中的"X-Requested-With"字段值表明这是一个AJAX请求,且用户未登录,那么向响应头中添加一个名为"loginStatus"的字段,值为"accessDenied", + // 用于告知前端该请求因为未登录被拒绝访问,然后通过response发送一个HTTP状态码为FORBIDDEN(403,表示禁止访问)的错误响应给客户端,最后返回false,拦截该请求,中断后续的请求处理流程。 response.addHeader("loginStatus", "accessDenied"); response.sendError(HttpServletResponse.SC_FORBIDDEN); return false; } else { + // 如果不是AJAX请求,则根据请求的方法(GET、POST等)进行不同的重定向处理。 if (request.getMethod().equalsIgnoreCase("GET")) { - String redirectUrl = request.getQueryString() != null ? request.getRequestURI() + "?" + request.getQueryString() : request.getRequestURI(); + // 如果是GET请求,构建要重定向的URL参数部分,先判断请求是否带有查询字符串(即请求URL中是否包含 "?" 后面的参数部分), + // 如果有查询字符串,则将请求的URI(不包含域名和端口部分的请求路径)加上原有的查询字符串作为重定向的URL参数部分;如果没有查询字符串,则只使用请求的URI作为重定向的URL参数部分, + // 这样做是为了在用户登录成功后能根据这个参数值将用户重定向回原本想要访问的页面。 + String redirectUrl = request.getQueryString()!= null? request.getRequestURI() + "?" + request.getQueryString() : request.getRequestURI(); + // 进行重定向操作,将用户重定向到登录页面,构建完整的登录页面URL,通过将配置的域名(tamguoDomainName)、登录页面的URL路径(loginUrl)以及重定向URL参数(使用URLEncoder进行UTF-8编码,确保特殊字符能正确传递)拼接起来, + // 形成最终的重定向地址发送给客户端,让用户先进行登录操作,然后返回false,拦截该请求,中断后续的请求处理流程。 response.sendRedirect(tamguoDomainName + loginUrl + "?" + REDIRECT_URL_PARAMETER_NAME + "=" + URLEncoder.encode(redirectUrl, "UTF-8")); } else { + // 如果是除GET之外的其他请求方法(如POST等),直接将用户重定向到登录页面,构建完整的登录页面URL,通过将配置的域名(tamguoDomainName)和登录页面的URL路径(loginUrl)拼接起来, + // 形成最终的重定向地址发送给客户端,让用户先进行登录操作,然后返回false,拦截该请求,中断后续的请求处理流程。 response.sendRedirect(tamguoDomainName + loginUrl); } return false; @@ -57,4 +84,4 @@ public class MemberInterceptor extends HandlerInterceptorAdapter{ } } -} +} \ No newline at end of file diff --git a/tamguo-bms/src/main/java/com/tamguo/utils/FileMd5Utils.java b/tamguo-bms/src/main/java/com/tamguo/utils/FileMd5Utils.java index 539a8ba..e33b6c8 100644 --- a/tamguo-bms/src/main/java/com/tamguo/utils/FileMd5Utils.java +++ b/tamguo-bms/src/main/java/com/tamguo/utils/FileMd5Utils.java @@ -5,33 +5,61 @@ import java.io.IOException; import java.security.MessageDigest; import org.apache.commons.codec.binary.Hex; +// 该类是一个工具类,主要提供了计算文件MD5值的功能,通过读取文件内容并按照一定的算法逐步更新MD5摘要信息,最终生成文件的MD5值, +// 并且能够处理大文件(通过分块读取文件内容的方式避免一次性将整个大文件读入内存),同时对可能出现的异常进行了相应处理,并确保文件输入流资源在使用后能正确关闭。 public class FileMd5Utils { /** - * 获取一个文件的md5值(可处理大文件) - * @return md5 value + * 获取一个文件的md5值(可处理大文件) + * 该方法用于计算给定文件输入流对应的文件的MD5值,采用分块读取文件内容的方式,每次读取一定大小的数据块(8192字节), + * 将读取到的数据块更新到MD5摘要对象中,直到读取完整个文件内容,最后通过编码转换将生成的MD5摘要字节数组转换为十六进制字符串形式的MD5值返回, + * 如果在计算过程中出现异常,则会打印异常堆栈信息并返回null表示获取MD5值失败。 + * + * @param fileInputStream 要计算MD5值的文件对应的输入流对象,通过该输入流可以读取文件的内容,该方法会从该输入流中逐块读取数据进行MD5值的计算, + * 在调用该方法时,需要确保传入的文件输入流已正确打开且指向有效的文件,并且调用者有责任在合适的时候创建和关闭该输入流(本方法内部会在最后进行关闭操作)。 + * @return 返回文件的MD5值,以十六进制字符串形式表示,如果计算过程中出现异常则返回null,调用者可根据返回值判断是否成功获取到文件的MD5值。 */ public static String getMD5(FileInputStream fileInputStream) { try { + // 获取MD5算法的MessageDigest实例,用于计算文件内容的MD5摘要信息,MessageDigest类提供了按照特定算法(这里是MD5算法)计算数据摘要的功能, + // 通过调用getInstance方法并传入算法名称("MD5")来创建对应的实例,后续可以使用该实例不断更新要计算摘要的数据内容,最终生成完整的摘要结果(即MD5值对应的字节数组)。 MessageDigest MD5 = MessageDigest.getInstance("MD5"); + + // 创建一个字节数组缓冲区,用于每次从文件输入流中读取文件内容的数据块,这里设置缓冲区大小为8192字节,通过分块读取文件内容的方式, + // 可以避免一次性将整个大文件读入内存,从而能够处理大文件的MD5计算,提高内存使用效率以及避免内存溢出等问题,特别是对于大型文件的处理更为合理和可行。 byte[] buffer = new byte[8192]; + + // 用于记录每次从文件输入流中实际读取到的字节数,在循环读取文件内容过程中,每次读取操作都会更新该变量的值,通过判断其是否为 -1 来确定是否已经读取完整个文件内容, + // 如果读取到文件末尾则返回 -1,表示文件内容已全部读完,循环结束。 int length; - while ((length = fileInputStream.read(buffer)) != -1) { + + // 通过循环,不断从文件输入流中读取文件内容到缓冲区,每次读取的数据长度存储在length变量中,然后使用MD5摘要对象的update方法将读取到的数据块更新到摘要计算中, + // 这样不断更新摘要信息,直到读取完整个文件内容(即length为 -1 时结束循环),实现对整个文件内容的MD5摘要计算。 + while ((length = fileInputStream.read(buffer))!= -1) { MD5.update(buffer, 0, length); } + + // 通过Hex类(来自Apache Commons Codec库)的encodeHex方法将最终生成的MD5摘要字节数组转换为十六进制字符串形式, + // 这样得到的就是常见的以十六进制字符串表示的文件MD5值,方便后续存储、展示以及与其他MD5值进行比对等操作,最后将该十六进制字符串作为文件的MD5值返回。 return new String(Hex.encodeHex(MD5.digest())); } catch (Exception e) { + // 如果在获取MD5算法实例、读取文件内容、更新MD5摘要或者转换MD5摘要字节数组为十六进制字符串等过程中出现异常, + // 则会打印异常的堆栈信息(通过e.printStackTrace()方法),方便后续排查问题,同时返回null,表示获取文件MD5值失败,调用者可根据返回值进行相应的错误处理。 e.printStackTrace(); return null; } finally { try { - if (fileInputStream != null){ + // 在finally块中确保文件输入流资源能够正确关闭,避免因异常等情况导致资源未释放的问题,先判断文件输入流是否不为空, + // 如果不为空则调用其close方法关闭输入流,释放相关的文件资源,确保文件读取操作完成后正确关闭资源,避免资源泄漏等问题。 + if (fileInputStream!= null) { fileInputStream.close(); - } + } } catch (IOException e) { + // 如果在关闭文件输入流过程中出现异常(例如文件被其他程序占用无法正常关闭等情况),同样会打印异常的堆栈信息,方便后续查看问题原因, + // 虽然此处无法对关闭流失败进行更多的补救措施,但通过打印异常信息可以帮助了解资源释放出现的问题。 e.printStackTrace(); } } } - -} + +} \ No newline at end of file diff --git a/tamguo-bms/src/main/java/com/tamguo/utils/ShiroUtils.java b/tamguo-bms/src/main/java/com/tamguo/utils/ShiroUtils.java index a31888d..bb30c1c 100644 --- a/tamguo-bms/src/main/java/com/tamguo/utils/ShiroUtils.java +++ b/tamguo-bms/src/main/java/com/tamguo/utils/ShiroUtils.java @@ -6,38 +6,96 @@ import org.apache.shiro.subject.Subject; import com.tamguo.modules.member.model.MemberEntity; +// 该类是一个工具类,主要提供了一系列基于Shiro框架的便捷方法,用于获取Shiro相关的核心对象(如Subject、Session等)、获取当前登录用户的信息(如用户实体、用户ID等)、操作会话属性(设置和获取)以及判断用户是否登录、执行用户登出等功能, +// 在整个项目中方便其他地方复用这些与Shiro框架交互的常用操作逻辑,避免重复编写相似的代码来获取Shiro相关对象和信息。 public class ShiroUtils { + /** + * 获取Shiro的会话对象(Session)。 + * 通过调用Shiro框架提供的SecurityUtils工具类的getSubject方法获取当前的Subject对象,再从Subject对象中获取对应的会话对象, + * 该会话对象可用于存储和获取与当前用户会话相关的信息,例如在用户登录后的一段时间内保存用户的临时状态、缓存一些数据等,方便在不同请求处理过程中共享这些信息。 + * + * @return 返回当前用户对应的Shiro会话对象(Session),后续可基于该对象进行会话相关的属性操作(如设置、获取属性等)。 + */ public static Session getSession() { return SecurityUtils.getSubject().getSession(); } + /** + * 获取Shiro的Subject对象。 + * 在Shiro框架中,Subject代表了当前执行的用户主体,它是与当前用户交互的核心对象,可用于进行认证(登录验证)、授权(权限验证)以及获取用户相关信息、操作会话等诸多操作, + * 通过Shiro的SecurityUtils工具类的getSubject方法来获取,表示当前与系统交互的用户主体对象,其他很多Shiro相关的操作都依赖于该对象来发起。 + * + * @return 返回当前的Subject对象,用于后续基于该对象进行诸如登录、获取用户信息、判断权限等一系列与用户主体相关的操作。 + */ public static Subject getSubject() { return SecurityUtils.getSubject(); } + /** + * 获取当前登录用户对应的MemberEntity(会员实体对象)。 + * 通过Shiro的SecurityUtils工具类获取当前的Subject对象,再从Subject对象中获取其主体信息(Principal),这里假设主体信息就是存储的会员实体对象(MemberEntity),所以进行强制类型转换获取, + * 该会员实体对象包含了当前登录用户的详细信息(如用户名、用户ID、用户其他业务相关属性等),方便在业务逻辑中获取和使用当前登录用户的具体信息进行相应处理,例如根据用户信息展示个性化页面、进行用户相关的数据操作等。 + * + * @return 返回当前登录用户对应的MemberEntity对象,如果当前用户未登录,则可能会抛出类型转换异常(因为未登录时获取的主体信息可能为null),在实际使用中需要注意进行合适的异常处理(通常在调用处判断用户是否登录)。 + */ public static MemberEntity getMember() { - return (MemberEntity)SecurityUtils.getSubject().getPrincipal(); + return (MemberEntity) SecurityUtils.getSubject().getPrincipal(); } + /** + * 获取当前登录用户的ID。 + * 先调用getMember方法获取当前登录用户对应的MemberEntity对象,再从该对象中获取其ID属性值,用于在业务逻辑中方便地获取当前登录用户的唯一标识, + * 例如根据用户ID查询该用户相关的其他数据、进行权限判断与用户ID关联等操作,在很多业务场景下需要明确知道当前操作的用户的唯一标识信息,通过该方法可以便捷获取。 + * + * @return 返回当前登录用户的ID,以字符串形式表示,如果当前用户未登录,调用getMember方法时可能出现异常情况(因为未登录获取不到MemberEntity对象),实际使用中要注意处理这种边界情况。 + */ public static String getMemberId() { return getMember().getId(); } - + + /** + * 设置Shiro会话对象(Session)中的属性值。 + * 先调用getSession方法获取当前用户对应的会话对象,然后使用该会话对象的setAttribute方法将要存储的属性键(key)和属性值(value)设置到会话中, + * 这样在同一个用户会话的不同请求处理过程中,可以通过相同的键来获取该属性值,实现会话级别的数据共享,方便存储一些临时的、与当前用户相关的数据,例如缓存用户在某个页面的操作状态等。 + * + * @param key 要设置的会话属性的键,用于唯一标识该属性,后续可通过该键来获取对应的属性值,类型为Object,可以是任意符合Java对象规范的类型,根据业务需求确定具体的键和对应的值类型。 + * @param value 要设置的会话属性的值,与传入的键相对应,存储到会话中,方便后续在同一个用户会话内通过键来获取使用,类型同样为Object,可以根据实际业务场景存储各种类型的数据。 + */ public static void setSessionAttribute(Object key, Object value) { getSession().setAttribute(key, value); } + /** + * 获取Shiro会话对象(Session)中指定键对应的属性值。 + * 先调用getSession方法获取当前用户对应的会话对象,然后使用该会话对象的getAttribute方法根据传入的属性键(key)来获取对应的属性值, + * 如果会话中存在该键对应的属性,则返回相应的值,否则返回null,通过该方法可以在同一个用户会话的不同请求处理中获取之前设置的临时数据,便于业务逻辑中使用会话中存储的相关信息。 + * + * @param key 要获取的会话属性的键,用于在会话中查找对应的属性值,类型为Object,需与之前设置属性时使用的键类型一致,确保能准确获取到对应的属性值。 + * @return 返回会话中指定键对应的属性值,如果不存在该键对应的属性,则返回null,返回值类型为Object,在实际使用时通常需要根据业务预期的属性值类型进行相应的强制类型转换操作。 + */ public static Object getSessionAttribute(Object key) { return getSession().getAttribute(key); } + /** + * 判断当前用户是否已登录。 + * 通过Shiro的SecurityUtils工具类获取当前的Subject对象,再判断其主体信息(Principal)是否为null,如果不为null则表示当前用户已登录,否则表示未登录, + * 可用于在业务逻辑中根据用户的登录状态进行不同的处理,例如对于未登录的用户重定向到登录页面、对于已登录用户展示特定的功能页面等情况,方便进行登录状态的判断操作。 + * + * @return 返回一个布尔值,如果当前用户已登录(即Subject的Principal不为null)则返回true,否则返回false,表示当前用户未登录。 + */ public static boolean isLogin() { - return SecurityUtils.getSubject().getPrincipal() != null; + return SecurityUtils.getSubject().getPrincipal()!= null; } + /** + * 执行用户登出操作。 + * 通过Shiro的SecurityUtils工具类获取当前的Subject对象,然后调用其logout方法进行用户登出处理,该操作会清除当前用户的会话信息、相关的认证和授权状态等, + * 使得用户回到未登录的状态,常用于用户主动点击登出按钮或者系统根据某些业务规则(如长时间未操作等)强制用户登出的场景,确保用户登出功能的实现。 + */ public static void logout() { SecurityUtils.getSubject().logout(); } -} +} \ No newline at end of file diff --git a/tamguo/pom.xml b/tamguo/pom.xml index a9e3a6e..1beb56b 100644 --- a/tamguo/pom.xml +++ b/tamguo/pom.xml @@ -1,172 +1,224 @@ + - 4.0.0 - com.tamguo - tamguo - V1.0.1 - tamguo + 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"> + + 4.0.0 + - - org.springframework.boot - spring-boot-starter-parent - 1.5.3.RELEASE - - + com.tamguo + + tamguo + + V1.0.1 + + tamguo + - - UTF-8 - 1.8 - 2.1.9 - + + + org.springframework.boot + spring-boot-starter-parent + 1.5.3.RELEASE + + + + - - - org.springframework.boot - spring-boot-starter-web - - - org.springframework.cloud - spring-cloud-starter-config - - - org.springframework.boot - spring-boot-starter-thymeleaf - - - org.springframework.boot - spring-boot-starter-jdbc - - - - com.baomidou - mybatis-plus-boot-starter - ${mybatis-plus-boot-starter.version} - - - tomcat-jdbc - org.apache.tomcat - - - - - - net.sourceforge.nekohtml - nekohtml - - - org.springframework.boot - spring-boot-starter-test - test - - - org.springframework.boot - spring-boot-starter-redis - 1.3.8.RELEASE - - - org.springframework.session - spring-session-data-redis - - - com.alibaba - fastjson - 1.2.32 - - - org.apache.shiro - shiro-spring - 1.2.5 - - - org.apache.shiro - shiro-ehcache - 1.2.5 - - - com.github.theborakompanioni - thymeleaf-extras-shiro - 1.2.1 - - - cn.songxinqiang - com.baidu.ueditor - 1.1.2-edit-1.0 - - - commons-codec - commons-codec - - - commons-fileupload - commons-fileupload - 1.3.1 - - - commons-io - commons-io - - - com.github.penggle - kaptcha - 2.3.2 - - - javax.servlet-api - javax.servlet - - - - - com.alibaba - druid - 1.0.18 - - - mysql - mysql-connector-java - - - org.apache.commons - commons-lang3 - 3.6 - - - com.aliyun - aliyun-java-sdk-dysmsapi - 1.0.0 - - - com.aliyun - aliyun-java-sdk-core - 3.2.8 - - - org.apache.commons - commons-email - 1.5 - - + + + UTF-8 + + 1.8 + + 2.1.9 + + - - - - org.springframework.cloud - spring-cloud-dependencies - Camden.SR6 - pom - import - - - + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.cloud + spring-cloud-starter-config + + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + + org.springframework.boot + spring-boot-starter-jdbc + + + + + com.baomidou + mybatis-plus-boot-starter + ${mybatis-plus-boot-starter.version} + + + + tomcat-jdbc + org.apache.tomcat + + + + + + + net.sourceforge.nekohtml + nekohtml + + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.springframework.boot + spring-boot-starter-redis + 1.3.8.RELEASE + + + + org.springframework.session + spring-session-data-redis + + + + com.alibaba + fastjson + 1.2.32 + + + + org.apache.shiro + shiro-spring + 1.2.5 + + + + org.apache.shiro + shiro-ehcache + 1.2.5 + + + + com.github.theborakompanioni + thymeleaf-extras-shiro + 1.2.1 + + + + cn.songxinqiang + com.baidu.ueditor + 1.1.2-edit-1.0 + + + + commons-codec + commons-codec + + + + commons-fileupload + commons-fileupload + 1.3.1 + + + + commons-io + commons-io + + + + com.github.penggle + kaptcha + 2.3.2 + + + javax.servlet-api + javax.servlet + + + + + + + com.alibaba + druid + 1.0.18 + + + + mysql + mysql-connector-java + + + + org.apache.commons + commons-lang3 + 3.6 + + + + com.aliyun + aliyun-java-sdk-dysmsapi + 1.0.0 + + + + com.aliyun + aliyun-java-sdk-core + 3.2.8 + + + + org.apache.commons + commons-email + 1.5 + + + - - tamguo - - - org.springframework.boot - spring-boot-maven-plugin - - - + + + + + org.springframework.cloud + spring-cloud-dependencies + Camden.SR6 + pom + import + + + + + + + + tamguo + + + + org.springframework.boot + spring-boot-maven-plugin + + + + \ No newline at end of file diff --git a/tamguo/src/main/java/com/tamguo/config/WebConfig.java b/tamguo/src/main/java/com/tamguo/config/WebConfig.java index 23d0c61..217161e 100644 --- a/tamguo/src/main/java/com/tamguo/config/WebConfig.java +++ b/tamguo/src/main/java/com/tamguo/config/WebConfig.java @@ -19,51 +19,93 @@ import com.google.code.kaptcha.util.Config; import com.tamguo.interceptor.MemberInterceptor; import com.tamguo.interceptor.MenuInterceptor; - +// 这个类使用了Spring的 @Configuration 注解,表示它是一个配置类,用于配置Spring Web相关的一些功能和组件, +// Spring在启动时会自动扫描并加载该类中的配置信息,将其中定义的Bean注册到Spring容器中。 @Configuration public class WebConfig extends WebMvcConfigurerAdapter { - - @Value("${file.storage.path}") - private String fileStoragePath; - @Autowired - private MenuInterceptor menuInterceptor; - @Autowired - private MemberInterceptor memberInterceptor; - /** - * 拦截器 - */ - @Override - public void addInterceptors(InterceptorRegistry registry) { - registry.addInterceptor(menuInterceptor).addPathPatterns("/**"); - registry.addInterceptor(memberInterceptor).addPathPatterns("/member/**"); - } - - @Override - public void addResourceHandlers(ResourceHandlerRegistry registry) { - registry.addResourceHandler("/files/**").addResourceLocations("file:"+fileStoragePath); - super.addResourceHandlers(registry); - } - - @Bean(name="producer") - public DefaultKaptcha getKaptchaBean(){ - DefaultKaptcha defaultKaptcha=new DefaultKaptcha(); - Properties properties=new Properties(); - // properties.setProperty("kaptcha.border.color", "105,179,90"); + // 通过 @Value 注解从配置文件(例如 application.properties 或 application.yml)中读取名为 "file.storage.path" 的属性值, + // 并注入到这个变量中,该路径可能用于指定文件存储的位置,后续在资源处理器配置中会用到这个路径来映射资源访问路径。 + @Value("${file.storage.path}") + private String fileStoragePath; + + // 通过Spring的依赖注入机制自动注入 MenuInterceptor 实例,MenuInterceptor 应该是自定义的拦截器, + // 用于在请求处理过程中对菜单相关的逻辑进行拦截和处理,比如权限校验、菜单数据准备等操作(具体功能取决于拦截器的实现代码)。 + @Autowired + private MenuInterceptor menuInterceptor; + + // 同样通过依赖注入机制注入 MemberInterceptor 实例,MemberInterceptor 也是自定义拦截器, + // 从命名可以推测它可能用于对会员相关的请求路径进行拦截,执行如会员权限验证、会员数据处理等相关操作。 + @Autowired + private MemberInterceptor memberInterceptor; + + /** + * 拦截器 + * 重写了父类 WebMvcConfigurerAdapter 的 addInterceptors 方法,用于注册自定义的拦截器到Spring的拦截器注册表中, + * 这样在处理Web请求时,相应的拦截器就会按照配置的路径模式对请求进行拦截并执行拦截器内部的逻辑。 + */ + @Override + public void addInterceptors(InterceptorRegistry registry) { + // 将 menuInterceptor 注册到拦截器注册表中,并配置其拦截的路径模式为 "/**",表示对所有的请求路径都会进行拦截, + // 具体在 MenuInterceptor 内部会根据业务逻辑判断是否需要进行处理以及如何处理这些请求。 + registry.addInterceptor(menuInterceptor).addPathPatterns("/**"); + // 将 memberInterceptor 注册到拦截器注册表中,配置其拦截的路径模式为 "/member/**",意味着对以 "/member/" 开头的请求路径进行拦截, + // 这通常用于对会员模块相关的请求进行特定的前置处理,比如验证会员登录状态、权限等情况。 + registry.addInterceptor(memberInterceptor).addPathPatterns("/member/**"); + } + + /** + * 资源处理器配置 + * 重写了父类的 addResourceHandlers 方法,用于配置Spring Web中资源文件的处理方式, + * 这里主要是定义了对 "/files/**" 这样的请求路径对应的资源文件的查找位置,将其映射到本地文件系统中指定的 fileStoragePath 路径下, + * 这样当客户端发起对 "/files/" 开头路径的资源请求时,Spring就能从对应的本地文件存储位置去查找并返回相应的文件资源。 + */ + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + registry.addResourceHandler("/files/**").addResourceLocations("file:" + fileStoragePath); + super.addResourceHandlers(registry); + } + + /** + * Kaptcha验证码配置 + * 定义了一个名为 "producer" 的Bean,用于创建和配置 DefaultKaptcha 实例,DefaultKaptcha 是用于生成验证码的组件, + * 在这里通过设置 Properties 对象中的各种属性来定制验证码的生成规则,例如验证码图片的宽度、高度、字体、字符长度等相关参数, + * 最终返回配置好的 DefaultKaptcha 实例,以便在项目的其他地方(比如登录页面等需要验证码的地方)可以使用它来生成验证码图片及对应的验证文本。 + */ + @Bean(name = "producer") + public DefaultKaptcha getKaptchaBean() { + DefaultKaptcha defaultKaptcha = new DefaultKaptcha(); + Properties properties = new Properties(); +// properties.setProperty("kaptcha.border.color", "105,179,90"); + // 设置验证码图片是否显示边框,这里配置为 "no",即不显示边框。 properties.setProperty("kaptcha.border", "no"); - properties.setProperty("kaptcha.image.width", "125"); - properties.setProperty("kaptcha.image.height", "45"); - properties.setProperty("kaptcha.session.key", "code"); - properties.setProperty("kaptcha.textproducer.char.length", "4"); - properties.setProperty("kaptcha.textproducer.font.names", "宋体,楷体,微软雅黑"); - Config config=new Config(properties); - defaultKaptcha.setConfig(config); - return defaultKaptcha; - } - - @Bean + // 设置验证码图片的宽度为 125 像素。 + properties.setProperty("kaptcha.image.width", "125"); + // 设置验证码图片的高度为 45 像素。 + properties.setProperty("kaptcha.image.height", "45"); + // 设置在Session中存储验证码文本的键名,这里设置为 "code",方便后续在验证时从Session中获取对应的验证码文本进行比对验证。 + properties.setProperty("kaptcha.session.key", "code"); + // 设置验证码文本的字符长度为 4 个字符,即生成的验证码文本将包含 4 个字符。 + properties.setProperty("kaptcha.textproducer.char.length", "4"); + // 设置验证码文本生成时使用的字体,可以指定多个字体,用逗号隔开,这里指定了宋体、楷体、微软雅黑等常见字体, + // 在生成验证码时会随机从这些字体中选择一种来绘制验证码文本。 + properties.setProperty("kaptcha.textproducer.font.names", "宋体,楷体,微软雅黑"); + Config config = new Config(properties); + defaultKaptcha.setConfig(config); + return defaultKaptcha; + } + + /** + * 嵌入式Servlet容器定制 + * 定义了一个名为 "containerCustomizer" 的Bean,返回一个实现了 EmbeddedServletContainerCustomizer 接口的匿名内部类实例, + * 用于对嵌入式Servlet容器(比如在Spring Boot项目中默认使用的Tomcat容器等)进行定制化配置, + * 这里主要配置了错误页面的映射,当发生 HttpStatus.NOT_FOUND(404 状态码,表示资源未找到)错误时,将重定向到 "/404.html" 页面, + * 当发生 HttpStatus.INTERNAL_SERVER_ERROR(500 状态码,表示服务器内部错误)错误时,将重定向到 "/500.html" 页面, + * 通过这样的配置可以为用户提供更友好的错误提示页面,提升用户体验。 + */ + @Bean public EmbeddedServletContainerCustomizer containerCustomizer() { - return new EmbeddedServletContainerCustomizer(){ + return new EmbeddedServletContainerCustomizer() { @Override public void customize(ConfigurableEmbeddedServletContainer container) { container.addErrorPages(new ErrorPage(HttpStatus.NOT_FOUND, "/404.html")); @@ -71,5 +113,4 @@ public class WebConfig extends WebMvcConfigurerAdapter { } }; } - -} +} \ No newline at end of file diff --git a/tamguo/src/main/java/com/tamguo/config/redis/SessionConfig.java b/tamguo/src/main/java/com/tamguo/config/redis/SessionConfig.java index 6bde953..47bead1 100644 --- a/tamguo/src/main/java/com/tamguo/config/redis/SessionConfig.java +++ b/tamguo/src/main/java/com/tamguo/config/redis/SessionConfig.java @@ -5,23 +5,38 @@ import org.springframework.context.annotation.Bean; import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession; -//这个类用配置redis服务器的连接 -@EnableRedisHttpSession(maxInactiveIntervalInSeconds= 1800) +// 这个类用于配置Redis服务器的连接,它是整个项目中与Redis会话相关配置的一部分 +@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 1800) +// @EnableRedisHttpSession注解用于启用Spring Session基于Redis的HTTP会话管理功能, +// maxInactiveIntervalInSeconds属性设置了会话的最大非活动时间间隔,这里设置为1800秒(即30分钟), +// 意味着如果一个会话在30分钟内没有任何活动,将会被认为过期并失效。 public class SessionConfig { + // 使用@Value注解从配置文件(例如application.properties或application.yml)中读取名为"redis.hostname"的属性值, + // 并将其注入到这个变量中,用于后续设置Redis连接的主机名。 @Value("${redis.hostname}") String HostName; + + // 同样通过@Value注解从配置文件中读取"redis.port"属性值,注入到这个变量,用于设置Redis连接的端口号。 @Value("${redis.port}") int Port; + + // 从配置文件读取"redis.password"属性值,注入此变量,用于设置连接Redis服务器时的密码(如果有设置密码的话)。 @Value("${redis.password}") String password; + // 使用@Bean注解将这个方法返回的对象注册为Spring容器中的一个Bean, + // 这个Bean就是JedisConnectionFactory类型,用于创建与Redis服务器的连接。 @Bean public JedisConnectionFactory connectionFactory() { JedisConnectionFactory connection = new JedisConnectionFactory(); + // 设置JedisConnectionFactory的端口号,使用前面通过@Value注入获取到的Port变量的值, + // 以此来确定连接Redis服务器的具体端口。 connection.setPort(Port); + // 设置JedisConnectionFactory的主机名,使用注入的HostName变量值,明确要连接的Redis服务器主机地址。 connection.setHostName(HostName); + // 设置连接Redis服务器的密码,使用password变量值,确保连接的安全性(如果Redis服务器设置了密码验证的话)。 connection.setPassword(password); return connection; } -} +} \ No newline at end of file diff --git a/tamguo/src/main/java/com/tamguo/config/shiro/MemberRealm.java b/tamguo/src/main/java/com/tamguo/config/shiro/MemberRealm.java index e697481..42a7b65 100644 --- a/tamguo/src/main/java/com/tamguo/config/shiro/MemberRealm.java +++ b/tamguo/src/main/java/com/tamguo/config/shiro/MemberRealm.java @@ -21,53 +21,80 @@ import com.tamguo.service.IMemberService; /** * 认证 - * + * 这个类 `MemberRealm` 继承自 `AuthorizingRealm`,是 Apache Shiro 框架中用于进行认证(登录验证)和授权(权限验证)相关操作的核心类, + * 它负责与业务逻辑层交互,获取用户信息并进行相应的验证判断等操作。 */ public class MemberRealm extends AuthorizingRealm { - + + // 通过Spring的依赖注入机制,自动注入 `IMemberService` 接口的实现类实例, + // 用于后续在认证和授权过程中与数据库等数据源交互,获取用户相关信息(如用户实体、登录失败次数等)。 @Autowired private IMemberService iMemberService; - - /** - * 授权(验证权限时调用) - */ + + /** + * 授权(验证权限时调用) + * 这个方法重写了父类 `AuthorizingRealm` 的 `doGetAuthorizationInfo` 方法,用于获取用户的授权信息,即权限相关的数据。 + * 在 Shiro 框架验证用户权限时会调用此方法,根据用户的身份信息(通常通过 `PrincipalCollection` 传递)来确定该用户拥有哪些权限。 + * 当前实现比较简单,只是初始化了一个 `SimpleAuthorizationInfo` 对象,并且设置了权限集合(这里暂时设置为空集合,实际应用中需根据业务逻辑填充具体权限)。 + */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { - Set permsSet = null; + // 初始化一个用于存储权限字符串的集合,当前示例中暂未填充具体的权限数据,实际业务中需要从数据库等数据源获取用户对应的权限并添加到这个集合中。 + Set permsSet = null; SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); + // 将权限集合设置到 `SimpleAuthorizationInfo` 对象中,用于后续权限验证时判断用户是否具有特定权限。 info.setStringPermissions(permsSet); - + return info; } /** * 认证(登录时调用) + * 重写了父类 `AuthorizingRealm` 的 `doGetAuthenticationInfo` 方法,用于进行用户登录认证操作。 + * 当用户尝试登录系统时,Shiro 框架会调用这个方法来验证用户提供的登录信息(用户名和密码)是否正确, + * 该方法会与业务逻辑层交互,获取用户实体信息,并根据一系列规则判断登录是否合法,如用户名是否存在、密码是否正确、账号是否被锁定等。 */ @Override protected AuthenticationInfo doGetAuthenticationInfo( AuthenticationToken token) throws AuthenticationException { + // 从 `AuthenticationToken` 中获取用户输入的用户名,在 Shiro 中,`AuthenticationToken` 通常是在登录时传递用户名和密码等登录凭证的对象, + // 这里将获取到的用户名强制转换为 `String` 类型,因为假设登录时用户名是以字符串形式传递的(实际应用中需根据具体的 `AuthenticationToken` 实现来确定类型转换是否正确)。 String username = (String) token.getPrincipal(); - String password = new String((char[]) token.getCredentials()); - - MemberEntity member = iMemberService.findByUsername(username); - if(member == null) { - throw new UnknownAccountException("用户名或密码有误,请重新输入或找回密码"); - } - Integer loginFailureCount = iMemberService.getLoginFailureCount(member); - if(loginFailureCount > 10) { - throw new LockedAccountException("账号被锁定"); - } - - if(!new Sha256Hash(password).toHex().equals(member.getPassword())){ + // 从 `AuthenticationToken` 中获取用户输入的密码,同样需要进行类型转换,将其转换为 `String` 类型,这里将 `char[]` 类型的密码转换为 `String`, + // 注意这种方式在实际应用中可能存在安全风险(比如密码在内存中会以明文形式存在一段时间),更安全的做法是使用合适的加密算法立即对密码进行处理,避免明文暴露。 + String password = new String((char[]) token.getCredentials()); + + // 通过注入的 `IMemberService` 调用 `findByUsername` 方法,根据用户名从数据库或其他数据源查找对应的 `MemberEntity`(用户实体对象), + // 如果返回 `null`,说明用户名不存在,此时抛出 `UnknownAccountException`,表示用户名或密码有误(在实际应用中可以更细化错误提示,区分是用户名不存在还是密码错误等情况)。 + MemberEntity member = iMemberService.findByUsername(username); + if (member == null) { + throw new UnknownAccountException("用户名或密码有误,请重新输入或找回密码"); + } + + // 通过 `IMemberService` 调用 `getLoginFailureCount` 方法获取当前用户的登录失败次数, + // 如果登录失败次数大于 10 次,说明账号可能存在异常风险,抛出 `LockedAccountException`,表示账号被锁定,禁止登录。 + Integer loginFailureCount = iMemberService.getLoginFailureCount(member); + if (loginFailureCount > 10) { + throw new LockedAccountException("账号被锁定"); + } + + // 使用 `Sha256Hash` 对用户输入的密码进行哈希处理(将密码转换为不可逆的哈希值),并与数据库中存储的用户密码(假设数据库中存储的也是经过相同哈希算法处理后的哈希值)进行比较, + // 如果不相等,说明密码错误,此时增加登录失败次数,并通过 `IMemberService` 的 `updateLoginFailureCount` 方法更新数据库中的登录失败次数记录, + // 然后抛出 `IncorrectCredentialsException`,提示用户名或密码有误,请重新输入或找回密码。 + if (!new Sha256Hash(password).toHex().equals(member.getPassword())) { loginFailureCount++; - iMemberService.updateLoginFailureCount(member , loginFailureCount); + iMemberService.updateLoginFailureCount(member, loginFailureCount); throw new IncorrectCredentialsException("用户名或密码有误,请重新输入或找回密码"); } + + // 如果密码正确,调用 `IMemberService` 的 `updateLastLoginTime` 方法更新用户的最后登录时间,记录此次登录操作, + // 然后创建一个 `SimpleAuthenticationInfo` 对象,将用户实体对象(`member`)、密码(`password`)以及当前 `Realm` 的名称(通过 `getName()` 获取)传递进去, + // 这个对象会被 Shiro 框架用于后续的认证相关流程,比如在会话中保存认证信息等操作。 // 更新登录时间 iMemberService.updateLastLoginTime(member.getUid().toString()); - - SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(member, password, getName()); - return info; + + SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(member, password, getName()); + return info; } -} +} \ No newline at end of file diff --git a/tamguo/src/main/java/com/tamguo/config/shiro/ShiroConfiguration.java b/tamguo/src/main/java/com/tamguo/config/shiro/ShiroConfiguration.java index 430d260..6b44a16 100644 --- a/tamguo/src/main/java/com/tamguo/config/shiro/ShiroConfiguration.java +++ b/tamguo/src/main/java/com/tamguo/config/shiro/ShiroConfiguration.java @@ -14,15 +14,26 @@ import org.springframework.context.annotation.Configuration; import at.pollux.thymeleaf.shiro.dialect.ShiroDialect; +// 这个类使用了Spring的 @Configuration 注解,表明它是一个配置类,用于配置Shiro框架相关的各种组件和功能, +// Spring在启动时会自动扫描并加载这个类中的配置信息,将其中定义的Bean注册到Spring容器中。 @Configuration public class ShiroConfiguration { + // 创建一个LinkedHashMap用于定义Shiro的过滤链规则,键是URL路径的匹配模式,值是对应的权限过滤器名称, + // 后续会将这个映射关系配置到ShiroFilterFactoryBean中,用于控制不同URL的访问权限。 private static Map filterChainDefinitionMap = new LinkedHashMap(); + // 定义一个名为 "shiroRealm" 的Bean,这个Bean返回的是自定义的MemberRealm实例, + // MemberRealm是继承自Shiro的AuthorizingRealm,用于实现具体的认证(登录验证)和授权(权限验证)逻辑, + // 将其注册为Spring容器中的Bean,方便其他Shiro相关组件引用,比如在SecurityManager中设置使用这个Realm进行验证操作。 @Bean(name = "shiroRealm") public MemberRealm getShiroRealm() { return new MemberRealm(); } + // 定义一个名为 "shiroEhcacheManager" 的Bean,用于创建和配置EhCacheManager实例, + // EhCacheManager在Shiro框架中用于缓存相关的数据,比如用户的授权信息等,提高系统性能,减少重复查询授权等操作的开销。 + // 通过设置cacheManagerConfigFile属性指定了EhCache的配置文件路径(这里是类路径下的ehcache-shiro.xml文件), + // 该配置文件会定义缓存的具体策略、缓存区域等相关设置。 @Bean(name = "shiroEhcacheManager") public EhCacheManager getEhCacheManager() { EhCacheManager em = new EhCacheManager(); @@ -30,11 +41,17 @@ public class ShiroConfiguration { return em; } + // 定义一个名为 "lifecycleBeanPostProcessor" 的Bean,创建LifecycleBeanPostProcessor实例, + // LifecycleBeanPostProcessor是Shiro提供的一个用于管理Shiro相关Bean生命周期的后置处理器, + // 它能够确保Shiro的一些组件(如Realm等)在Spring容器中正确地初始化、销毁等,按照其生命周期规范执行相应操作。 @Bean(name = "lifecycleBeanPostProcessor") public LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() { return new LifecycleBeanPostProcessor(); } + // 定义一个名为 "defaultAdvisorAutoProxyCreator" 的Bean,创建DefaultAdvisorAutoProxyCreator实例, + // 它是Spring AOP中的一个自动代理创建器,用于在Spring容器中自动创建基于Advisor(切面顾问)的代理对象, + // 在Shiro与Spring集成时,帮助实现基于AOP的权限控制拦截等功能,通过设置proxyTargetClass为true,表示使用基于类的代理方式(而不是基于接口的代理)。 @Bean public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() { DefaultAdvisorAutoProxyCreator daap = new DefaultAdvisorAutoProxyCreator(); @@ -42,6 +59,10 @@ public class ShiroConfiguration { return daap; } + // 定义一个名为 "securityManager" 的Bean,创建DefaultWebSecurityManager实例, + // DefaultWebSecurityManager是Shiro在Web应用中用于管理安全相关操作的核心组件, + // 在这里设置了它所使用的Realm(通过调用getShiroRealm()方法获取前面定义的MemberRealm实例)以及缓存管理器(通过getEhCacheManager()获取EhCacheManager实例), + // 这样SecurityManager在进行认证和授权操作时就会使用指定的Realm进行验证,并利用缓存管理器来缓存相关信息,提高效率。 @Bean(name = "securityManager") public DefaultWebSecurityManager getDefaultWebSecurityManager() { DefaultWebSecurityManager dwsm = new DefaultWebSecurityManager(); @@ -50,22 +71,36 @@ public class ShiroConfiguration { return dwsm; } + // 定义一个名为 "authorizationAttributeSourceAdvisor" 的Bean,创建AuthorizationAttributeSourceAdvisor实例, + // AuthorizationAttributeSourceAdvisor是Shiro与Spring AOP集成的一个关键组件,用于在方法级别进行权限控制, + // 它通过设置securityManager(调用getDefaultWebSecurityManager()获取配置好的SecurityManager)来关联整个权限管理体系, + // 使得在Spring应用中可以基于注解等方式方便地进行权限控制,比如在方法上添加Shiro的权限注解来限制哪些用户可以访问该方法。 @Bean public AuthorizationAttributeSourceAdvisor getAuthorizationAttributeSourceAdvisor() { AuthorizationAttributeSourceAdvisor aasa = new AuthorizationAttributeSourceAdvisor(); aasa.setSecurityManager(getDefaultWebSecurityManager()); return new AuthorizationAttributeSourceAdvisor(); } - - /** - * ShiroDialect,为了在thymeleaf里使用shiro的标签的bean - * @return - */ - @Bean - public ShiroDialect shiroDialect(){ + + /** + * ShiroDialect,为了在thymeleaf里使用shiro的标签的bean + * 这个方法定义了一个名为 "shiroDialect" 的Bean,返回ShiroDialect实例, + * ShiroDialect是用于在Thymeleaf模板引擎中使用Shiro标签的一个方言类, + * 当项目中使用Thymeleaf作为页面模板并且需要在页面中进行Shiro相关的权限判断、显示等操作时(比如根据用户权限显示或隐藏页面元素), + * 通过将这个Bean注册到Spring容器中,Thymeleaf就能识别并解析Shiro相关的标签,实现相应的功能。 + */ + @Bean + public ShiroDialect shiroDialect() { return new ShiroDialect(); } + // 定义一个名为 "shiroFilter" 的Bean,创建ShiroFilterFactoryBean实例, + // ShiroFilterFactoryBean是Shiro在Web应用中用于配置URL级别的访问控制过滤器链的核心组件, + // 它通过设置securityManager(关联前面配置好的DefaultWebSecurityManager)来建立整个权限过滤的基础框架, + // 同时设置了登录页面的URL(通过setLoginUrl方法设置为 "/login",当用户未登录访问受保护资源时会重定向到这个登录页面)、 + // 登录成功后的默认跳转页面URL(通过setSuccessUrl方法设置为 "/index"), + // 并且将前面定义的filterChainDefinitionMap(包含了URL路径和对应权限过滤器的映射关系)设置进去, + // 这样Shiro就能根据这些配置对不同的URL请求进行权限验证和相应的处理,比如哪些URL需要认证才能访问,哪些URL可以匿名访问等。 @Bean(name = "shiroFilter") public ShiroFilterFactoryBean getShiroFilterFactoryBean() { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); diff --git a/tamguo/src/main/java/com/tamguo/dao/ChapterMapper.java b/tamguo/src/main/java/com/tamguo/dao/ChapterMapper.java index 1274471..ea83164 100644 --- a/tamguo/src/main/java/com/tamguo/dao/ChapterMapper.java +++ b/tamguo/src/main/java/com/tamguo/dao/ChapterMapper.java @@ -6,12 +6,22 @@ import org.apache.ibatis.annotations.Param; import com.tamguo.config.dao.SuperMapper; import com.tamguo.model.ChapterEntity; -public interface ChapterMapper extends SuperMapper{ +// 定义了一个名为ChapterMapper的接口,该接口继承自SuperMapper, +// 通常继承自自定义的通用Mapper接口(这里的SuperMapper大概率是用于简化MyBatis常见CRUD操作的基础Mapper接口), +// 这样可以复用一些通用的数据库操作方法(比如增删改查等基本功能),同时也可以定义自己特定的查询方法,用于与数据库中的章节(Chapter)相关表进行交互。 +public interface ChapterMapper extends SuperMapper { - List findByBookId(@Param(value="bookId") String bookId); - - List findByParentId(@Param(value="parentId") String parentId); + // 定义了一个查询方法findByBookId,用于根据书籍的唯一标识符(bookId)从数据库中查找对应的章节信息列表。 + // 使用了@Param注解来为方法参数指定一个名称,这样在对应的SQL语句中就可以通过这个指定的名称来引用参数, + // 在这里参数名为"bookId",表示传入的是书籍的ID,方法返回值是一个ChapterEntity类型的列表,即查询到的对应章节实体的集合。 + List findByBookId(@Param(value = "bookId") String bookId); - ChapterEntity findNextPoint(@Param(value="uid")String uid , @Param(value="orders")Integer orders); + // 定义另一个查询方法findByParentId,目的是根据章节的父级章节的唯一标识符(parentId)来查找对应的子章节信息列表。 + // 同样使用了@Param注解为参数命名为"parentId",方便在SQL语句中引用,返回值也是ChapterEntity类型的列表,包含了满足条件的子章节实体集合。 + List findByParentId(@Param(value = "parentId") String parentId); -} + // 定义了一个查找下一个知识点(这里推测ChapterEntity可能代表知识点相关实体,具体取决于业务逻辑)的方法findNextPoint。 + // 该方法接收两个参数,一个是知识点的唯一标识符(uid),另一个是顺序号(orders),通过这两个参数从数据库中查找符合条件的下一个知识点实体。 + // 使用@Param注解分别为两个参数命名为"uid"和"orders",方便在对应的SQL语句中准确引用参数,返回值是一个ChapterEntity类型的对象,代表找到的下一个知识点实体。 + ChapterEntity findNextPoint(@Param(value = "uid") String uid, @Param(value = "orders") Integer orders); +} \ No newline at end of file diff --git a/tamguo/src/main/java/com/tamguo/dao/PaperMapper.java b/tamguo/src/main/java/com/tamguo/dao/PaperMapper.java index 2191eaf..7aee791 100644 --- a/tamguo/src/main/java/com/tamguo/dao/PaperMapper.java +++ b/tamguo/src/main/java/com/tamguo/dao/PaperMapper.java @@ -7,27 +7,54 @@ import com.baomidou.mybatisplus.plugins.pagination.Pagination; import com.tamguo.config.dao.SuperMapper; import com.tamguo.model.PaperEntity; -public interface PaperMapper extends SuperMapper{ - - List findByTypeAndAreaId(@Param(value="type")String type, @Param(value="areaId")String areaId , Pagination page); - - List findByAreaId(@Param(value="areaId") String areaId , Pagination page); - - List findBySchoolId(@Param(value="schoolId")String schoolId , Pagination page); - - List findList(@Param(value="subjectId")String subjectId, @Param(value="courseId")String courseId, @Param(value="paperType")String paperType, - @Param(value="year")String year, @Param(value="area")String area , Pagination page); - - List findPaperByAreaId(@Param(value="areaId")String areaId , @Param(value="type")String type , Pagination page); - +// 定义了名为PaperMapper的接口,它继承自SuperMapper, +// 与之前类似,SuperMapper大概率是一个自定义的通用Mapper接口,用于继承通用的数据库操作方法(如基本的增删改查等), +// 在此基础上,PaperMapper接口可以定义针对试卷(PaperEntity,从命名推测其代表试卷相关实体)相关的特定数据库查询方法。 +public interface PaperMapper extends SuperMapper { + + // 定义一个查询方法findByTypeAndAreaId,用于根据试卷类型(type)和地区ID(areaId)进行分页查询试卷信息列表。 + // 使用了@Param注解为方法的前两个参数分别命名为"type"和"areaId",方便在对应的SQL语句中准确引用参数, + // 第三个参数是Pagination类型的page,这是MyBatis Plus提供的用于分页的对象,通过它可以指定分页相关的参数(如页码、每页数量等), + // 方法返回值是一个PaperEntity类型的列表,即符合条件的试卷实体集合。 + List findByTypeAndAreaId(@Param(value = "type") String type, @Param(value = "areaId") String areaId, Pagination page); + + // 定义查询方法findByAreaId,根据地区ID(areaId)进行分页查询试卷信息列表。 + // 使用@Param注解为参数"areaId"命名,便于SQL语句引用,同样传入Pagination类型的page对象用于分页, + // 返回值为PaperEntity类型的列表,包含了该地区下对应的试卷实体集合。 + List findByAreaId(@Param(value = "areaId") String areaId, Pagination page); + + // 定义查询方法findBySchoolId,依据学校ID(schoolId)进行分页查询试卷信息列表。 + // 通过@Param注解将参数命名为"schoolId",结合分页对象page来实现分页查询功能,返回符合条件的试卷实体列表。 + List findBySchoolId(@Param(value = "schoolId") String schoolId, Pagination page); + + // 定义了一个较为复杂的查询方法findList,用于根据多个条件进行分页查询试卷信息列表,这些条件包括学科ID(subjectId)、课程ID(courseId)、 + // 试卷类型(paperType)、年份(year)以及地区(area),通过@Param注解分别为这些参数命名,方便在SQL语句中准确引用, + // 传入Pagination类型的page对象用于分页操作,返回值是满足所有条件的PaperEntity类型的试卷实体列表。 + List findList(@Param(value = "subjectId") String subjectId, @Param(value = "courseId") String courseId, + @Param(value = "paperType") String paperType, + @Param(value = "year") String year, @Param(value = "area") String area, Pagination page); + + // 定义查询方法findPaperByAreaId,根据地区ID(areaId)和试卷类型(type)进行分页查询试卷信息列表。 + // 利用@Param注解对两个参数"areaId"和"type"进行命名,结合分页对象page实现分页查询,返回符合条件的试卷实体集合。 + List findPaperByAreaId(@Param(value = "areaId") String areaId, @Param(value = "type") String type, Pagination page); + + // 定义了一个查询方法getPaperTotal,用于获取试卷的总数量。 + // 该方法没有传入分页相关的参数,返回值是Long类型,表示符合某种未明确限定(可能是所有记录,也可能是满足特定默认条件的记录,取决于具体业务逻辑和SQL实现)的试卷总条数。 Long getPaperTotal(); - List findByCreaterId(@Param(value="createrId")String createrId); - - List queryPageByNameAndCreatorId(@Param(value="name")String name, @Param(value="memberId")String memberId , Pagination page); + // 定义查询方法findByCreaterId,根据创建者的ID(createrId)来查找对应的试卷信息列表。 + // 使用@Param注解将参数命名为"createrId",方便在SQL语句中引用,返回值为PaperEntity类型的列表,即该创建者创建的试卷实体集合。 + List findByCreaterId(@Param(value = "createrId") String createrId); - List featuredPaper(@Param(value="type")String type, @Param(value="subjectId")String subjectId , Pagination page); + // 定义查询方法queryPageByNameAndCreatorId,用于根据试卷名称(name)和创建者ID(memberId)进行分页查询试卷信息列表。 + // 通过@Param注解分别为两个参数命名,传入Pagination类型的page对象用于分页,返回值是满足条件的试卷实体集合。 + List queryPageByNameAndCreatorId(@Param(value = "name") String name, @Param(value = "memberId") String memberId, Pagination page); - List findHotPaper(@Param(value="subjectId")String subjectId,@Param(value="courseId") String courseId, Pagination page); + // 定义查询方法featuredPaper,根据试卷类型(type)和学科ID(subjectId)进行分页查询特色试卷信息列表。 + // 使用@Param注解为参数命名,结合分页对象page实现分页查询功能,返回值是符合特色试卷相关条件的PaperEntity类型的列表。 + List featuredPaper(@Param(value = "type") String type, @Param(value = "subjectId") String subjectId, Pagination page); + // 定义查询方法findHotPaper,根据学科ID(subjectId)和课程ID(courseId)进行分页查询热门试卷信息列表。 + // 利用@Param注解对两个参数命名,结合分页对象page进行分页查询操作,返回值是满足热门试卷相关条件的PaperEntity类型的列表。 + List findHotPaper(@Param(value = "subjectId") String subjectId, @Param(value = "courseId") String courseId, Pagination page); } diff --git a/tamguo/src/main/java/com/tamguo/interceptor/MemberInterceptor.java b/tamguo/src/main/java/com/tamguo/interceptor/MemberInterceptor.java index 0012392..0c660c6 100644 --- a/tamguo/src/main/java/com/tamguo/interceptor/MemberInterceptor.java +++ b/tamguo/src/main/java/com/tamguo/interceptor/MemberInterceptor.java @@ -6,8 +6,10 @@ import javax.servlet.http.HttpServletResponse; import org.springframework.stereotype.Component; import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; +// 使用@Component注解将这个类标记为Spring组件,这样Spring在扫描时能够发现并将其纳入到Spring容器管理中, +// 便于在其他地方进行依赖注入或者配置使用。 @Component -public class MemberInterceptor extends HandlerInterceptorAdapter{ +public class MemberInterceptor extends HandlerInterceptorAdapter { /** "重定向URL"参数名称 */ private static final String REDIRECT_URL_PARAMETER_NAME = "redirectUrl"; @@ -17,39 +19,50 @@ public class MemberInterceptor extends HandlerInterceptorAdapter{ /** 登录URL */ private String loginUrl = DEFAULT_LOGIN_URL; - + /** * 请求前处理 - * - * @param request - * HttpServletRequest - * @param response - * HttpServletResponse - * @param handler - * 处理器 - * @return 是否继续执行 + * 重写了父类HandlerInterceptorAdapter的preHandle方法,该方法会在请求到达具体的处理器(比如Controller中的方法)之前被调用, + * 用于进行一些前置的拦截处理逻辑,比如权限校验、登录验证等操作,根据验证结果决定是否允许请求继续往下执行。 + * + * @param request HttpServletRequest,代表客户端发来的HTTP请求对象,从中可以获取请求的各种信息,如请求头、请求参数、会话信息等。 + * @param response HttpServletResponse,用于向客户端返回响应信息,可设置响应状态码、响应头、输出响应内容等。 + * @param handler 处理器,通常是指向具体要执行的Controller方法的一个对象引用,不过在这里一般不需要直接操作它,主要是根据请求相关情况做拦截判断。 + * @return 是否继续执行,如果返回true,表示允许请求继续执行,会接着调用后续的处理器(Controller方法等);如果返回false,则直接中断请求处理流程,不再执行后续操作。 */ @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { - Object currMember = request.getSession().getAttribute("currMember"); - if (currMember != null) { + // 从当前的HTTP请求对象中获取会话(Session)里名为"currMember"的属性值,通常这里的"currMember"可能是存储了当前已登录会员的相关信息的对象, + // 如果获取到的值不为null,说明当前用户已经登录,那么允许请求继续执行,直接返回true。 + Object currMember = request.getSession().getAttribute("currMember"); + if (currMember!= null) { return true; } else { + // 获取请求头中名为"X-Requested-With"的值,这个请求头常用于判断请求是否是通过Ajax(XMLHttpRequest)发起的, + // 在一些Web应用中,对于Ajax请求和普通页面请求的处理方式可能会有所不同,比如返回的错误提示格式等方面。 String requestType = request.getHeader("X-Requested-With"); - if (requestType != null && requestType.equalsIgnoreCase("XMLHttpRequest")) { + if (requestType!= null && requestType.equalsIgnoreCase("XMLHttpRequest")) { + // 如果请求是Ajax请求("X-Requested-With"请求头的值为"XMLHttpRequest"),则向响应头中添加一个名为"loginStatus"的自定义头信息, + // 设置其值为"accessDenied",表示登录状态为拒绝访问(也就是未登录情况下的Ajax请求被拦截了), + // 然后设置响应的状态码为HttpServletResponse.SC_FORBIDDEN(403,表示禁止访问),最后返回false,中断请求处理流程,不让请求继续执行。 response.addHeader("loginStatus", "accessDenied"); response.sendError(HttpServletResponse.SC_FORBIDDEN); return false; } else { + // 如果不是Ajax请求,再判断请求的方法是否是GET方法,对于不同的HTTP请求方法,重定向的逻辑可能稍有不同。 if (request.getMethod().equalsIgnoreCase("GET")) { - String redirectUrl = request.getQueryString() != null ? request.getRequestURI() + "?" + request.getQueryString() : request.getRequestURI(); + // 如果是GET请求,构建一个重定向的URL,将当前请求的完整路径(包括请求的URI和查询字符串,如果有的话)进行编码后作为参数添加到登录URL后面, + // 具体做法是先判断当前请求是否有查询字符串(通过request.getQueryString()判断),如果有,则将请求的URI和查询字符串拼接起来, + // 如果没有查询字符串,就只使用请求的URI,然后使用URLEncoder将这个重定向的URL按照UTF-8编码格式进行编码,避免出现中文等特殊字符乱码问题, + // 最后通过response.sendRedirect方法将客户端重定向到登录页面,并带上这个重定向URL参数,以便在登录成功后可以根据这个参数跳回原来请求的页面。 + String redirectUrl = request.getQueryString()!= null? request.getRequestURI() + "?" + request.getQueryString() : request.getRequestURI(); response.sendRedirect(request.getContextPath() + loginUrl + "?" + REDIRECT_URL_PARAMETER_NAME + "=" + URLEncoder.encode(redirectUrl, "UTF-8")); } else { + // 如果不是GET请求(比如POST等其他请求方法),直接重定向到登录页面,不携带额外的重定向URL参数,因为对于非GET请求,通常不需要记录原请求的详细信息来进行后续跳转。 response.sendRedirect(request.getContextPath() + loginUrl); } return false; } } } - -} +} \ No newline at end of file diff --git a/tamguo/src/main/java/com/tamguo/model/AdEntity.java b/tamguo/src/main/java/com/tamguo/model/AdEntity.java index a09b5f8..6d01d3c 100644 --- a/tamguo/src/main/java/com/tamguo/model/AdEntity.java +++ b/tamguo/src/main/java/com/tamguo/model/AdEntity.java @@ -9,51 +9,69 @@ import com.tamguo.config.dao.SuperEntity; /** * The persistent class for the tiku_ad database table. - * + * 这个类(AdEntity)用于映射数据库中名为“tiku_ad”的表,从命名推测它可能是与广告(Ad)相关的数据实体类, + * 继承自SuperEntity并实现了Serializable接口,继承SuperEntity可能是为了复用一些通用的实体相关方法(如基础的数据库操作相关方法等,具体取决于SuperEntity的定义), + * 实现Serializable接口是为了让该类的对象能够在网络传输、持久化存储等场景下进行序列化和反序列化操作,保证对象状态的保存和恢复。 */ -@TableName(value="tiku_ad") +@TableName(value = "tiku_ad") public class AdEntity extends SuperEntity implements Serializable { private static final long serialVersionUID = 1L; + // 用于存储业务相关的键值,具体含义可能与广告业务中的某种标识或者关联信息有关,其值由外部设置并获取,通过对应的getter和setter方法操作。 private String businessKey; + // 用于存储广告的名称,方便在业务中对不同广告进行区分和识别,同样通过getter和setter方法进行访问和修改。 private String name; - + + // 用于存储广告的详细信息,可能是包含了广告内容、展示规则等各种相关数据的字符串表示形式(具体格式取决于业务设计), + // 通过getter和setter方法来操作该属性值。 private String adInfo; + // getName方法用于获取广告名称属性的值,供外部代码调用,遵循JavaBean规范的getter方法定义。 public String getName() { return name; } + // setName方法用于设置广告名称属性的值,外部代码可以传入一个字符串参数来更新广告名称,遵循JavaBean规范的setter方法定义。 public void setName(String name) { this.name = name; } + // getSerialversionuid方法返回类的序列化版本UID,这个UID在序列化和反序列化过程中用于验证类的版本一致性, + // 这里直接返回了定义好的静态常量serialVersionUID的值,一般不需要手动修改这个方法的实现,它是按照Java序列化机制要求定义的。 public static long getSerialversionuid() { return serialVersionUID; } + // getAdInfo方法用于获取广告详细信息(adInfo)属性的值,外部代码可以通过调用这个方法获取存储的广告相关信息字符串。 public String getAdInfo() { return adInfo; } + // setAdInfo方法用于设置广告详细信息属性的值,外部代码可以传入一个字符串参数来更新广告详细信息的内容。 public void setAdInfo(String adInfo) { this.adInfo = adInfo; } - - public JSONArray getAds(){ - if(StringUtils.isEmpty(getAdInfo())){ + + /** + * getAds方法用于将存储在adInfo属性中的字符串解析为JSONArray对象,方便后续以JSON数据结构的形式对广告相关信息进行操作。 + * 如果adInfo属性的值为空(通过StringUtils.isEmpty方法判断),则返回null,表示没有可解析的有效广告信息; + * 如果不为空,则使用阿里巴巴的FastJSON库的JSONArray.parseArray方法将其解析为JSONArray对象,以便进行如遍历、获取特定元素等操作。 + */ + public JSONArray getAds() { + if (StringUtils.isEmpty(getAdInfo())) { return null; } return JSONArray.parseArray(getAdInfo()); } + // getBusinessKey方法用于获取业务键值(businessKey)属性的值,供外部代码获取相关的业务标识或关联信息。 public String getBusinessKey() { return businessKey; } + // setBusinessKey方法用于设置业务键值属性的值,外部代码可以传入一个字符串参数来更新该属性所代表的业务相关标识信息。 public void setBusinessKey(String businessKey) { this.businessKey = businessKey; } - } \ No newline at end of file diff --git a/tamguo/src/main/java/com/tamguo/model/BookEntity.java b/tamguo/src/main/java/com/tamguo/model/BookEntity.java index cf28015..f5b2eee 100644 --- a/tamguo/src/main/java/com/tamguo/model/BookEntity.java +++ b/tamguo/src/main/java/com/tamguo/model/BookEntity.java @@ -6,91 +6,143 @@ import com.baomidou.mybatisplus.activerecord.Model; import com.baomidou.mybatisplus.annotations.TableId; import com.baomidou.mybatisplus.annotations.TableName; -@TableName(value="tiku_book") -public class BookEntity extends Model{ - +// 这个类(BookEntity)定义了与数据库中名为“tiku_book”表对应的实体对象,从命名来看,它可能是用于表示书籍相关信息的数据实体类。 +// 该类继承自MyBatis Plus的Model类,继承Model类可以方便地使用MyBatis Plus提供的一些便捷的数据库操作方法,比如CRUD操作等, +// 同时还实现了Serializable接口,使得该类的对象能够在诸如网络传输、持久化存储等场景下进行序列化和反序列化操作,保证对象状态可以被正确保存和恢复。 +@TableName(value = "tiku_book") +public class BookEntity extends Model { + private static final long serialVersionUID = 1L; - + + // 使用@TableId注解标记这个字段,表明它是数据库表(“tiku_book”表)中的主键字段,在这里字段名为“uid”, + // 具体的主键生成策略等相关设置(如果有的话)通常会根据MyBatis Plus的默认配置或者项目中额外的配置来确定, + // 通过对应的getter和setter方法来操作这个属性的值,外部代码可以获取或设置书籍的唯一标识符(uid)。 @TableId private String uid; + + // 用于存储学科的唯一标识符,通过这个字段可以关联到对应的学科信息,其值由外部设置并获取,使用对应的getter和setter方法进行操作, + // 方便在业务逻辑中确定书籍所属的学科分类等情况。 private String subjectId; + + // 用于存储课程的唯一标识符,类似学科标识符的作用,可用于关联到具体的课程信息,外部代码可以通过getter和setter方法对其值进行操作, + // 帮助在业务中明确书籍与具体课程之间的关联关系。 private String courseId; + + // 用于存储书籍的名称,通过getter和setter方法(getName和setName),外部代码能够获取或更新书籍的名称信息,便于在业务中展示、查找等操作时使用。 private String name; + + // 用于存储书籍的出版社信息,外部代码可利用相应的getter和setter方法获取或修改该属性值,在涉及书籍来源、版权等业务场景下会用到这个信息。 private String publishingHouse; + + // 用于存储书籍中包含的题目数量相关信息,可能是一个数字字符串形式(具体取决于业务存储格式),通过对应的getter和setter方法进行操作, + // 在统计、查询等业务操作中可以依据这个属性了解书籍的题目规模情况。 private String questionNum; + + // 用于存储书籍中所涵盖的知识点数量相关信息,同样通过getter和setter方法来操作其值,方便在业务逻辑中对书籍涵盖的知识点情况进行把握和处理。 private String pointNum; + + // 用于存储书籍的排序序号相关信息,可能用于在展示、排序等场景下确定书籍的顺序,通过对应的getter和setter方法进行操作, + // 外部代码可以获取或修改这个序号值来调整书籍的排列顺序等情况。 private Integer orders; - public String getUid() { - return uid; + // getName方法是遵循JavaBean规范的getter方法,用于获取书籍名称属性(name)的值,外部代码可以调用这个方法获取当前BookEntity对象所代表书籍的名称。 + public String getName() { + return name; } - public void setUid(String uid) { - this.uid = uid; + // setName方法是遵循JavaBean规范的setter方法,用于设置书籍名称属性的值,外部代码可以传入一个字符串参数来更新当前BookEntity对象所代表书籍的名称。 + public void setName(String name) { + this.name = name; } + // getSubjectId方法用于获取学科标识符(subjectId)属性的值,供外部代码获取书籍所属的学科相关信息,便于进行关联查询、分类统计等操作。 public String getSubjectId() { return subjectId; } + // setSubjectId方法用于设置学科标识符属性的值,外部代码可以传入一个字符串参数来更新书籍所属的学科信息,在业务逻辑中调整书籍的学科分类归属等情况时会用到。 public void setSubjectId(String subjectId) { this.subjectId = subjectId; } + // getCourseId方法用于获取课程标识符(courseId)属性的值,方便外部代码知晓书籍与具体课程的关联情况,在课程相关的业务操作中起到关联作用。 public String getCourseId() { return courseId; } + // setCourseId方法用于设置课程标识符属性的值,外部代码可以传入一个字符串参数来更新书籍对应的课程信息,用于在业务中重新确定书籍与课程的关联关系。 public void setCourseId(String courseId) { this.courseId = courseId; } - public String getName() { - return name; + // getUid方法用于获取书籍的唯一标识符(uid)属性的值,这个值作为主键在数据库操作以及对象标识等方面有着重要作用, + // 外部代码可以通过调用这个方法获取当前BookEntity对象对应的书籍在数据库中的唯一标识。 + public String getUid() { + return uid; } - public void setName(String name) { - this.name = name; + // setUid方法用于设置书籍的唯一标识符属性的值,外部代码可以传入一个字符串参数来更新这个唯一标识信息,不过在实际应用中要谨慎操作, + // 因为主键通常具有唯一性且关联着数据库中的重要数据记录,随意修改可能导致数据不一致等问题。 + public void setUid(String uid) { + this.uid = uid; } + // getPublishingHouse方法用于获取书籍出版社(publishingHouse)属性的值,外部代码可以通过调用这个方法获取当前BookEntity对象所代表书籍的出版社信息。 public String getPublishingHouse() { return publishingHouse; } + // setPublishingHouse方法用于设置书籍出版社属性的值,外部代码可以传入一个字符串参数来更新书籍的出版社信息,比如在书籍信息更新、录入新书籍等场景下使用。 public void setPublishingHouse(String publishingHouse) { this.publishingHouse = publishingHouse; } + // getQuestionNum方法用于获取书籍题目数量(questionNum)属性的值,外部代码可以获取这个属性值来了解书籍包含的题目规模情况, + // 在统计分析、展示等业务操作中会用到这个信息。 public String getQuestionNum() { return questionNum; } + // setQuestionNum方法用于设置书籍题目数量属性的值,外部代码可以传入一个字符串参数来更新书籍所包含的题目数量信息, + // 例如在题目数量发生变化(新增、删除题目等情况)时对该属性进行相应的更新操作。 public void setQuestionNum(String questionNum) { this.questionNum = questionNum; } + // getPointNum方法用于获取书籍知识点数量(pointNum)属性的值,外部代码可以通过调用这个方法获取当前BookEntity对象所代表书籍涵盖的知识点数量情况, + // 在知识点相关的业务处理、统计等场景下会用到这个属性值。 public String getPointNum() { return pointNum; } + // setPointNum方法用于设置书籍知识点数量属性的值,外部代码可以传入一个字符串参数来更新书籍涵盖的知识点数量信息, + // 比如在对书籍内容进行知识点梳理、更新后对该属性进行相应的调整操作。 public void setPointNum(String pointNum) { this.pointNum = pointNum; } + // getOrders方法用于获取书籍排序序号(orders)属性的值,外部代码可以获取这个序号值来了解书籍在相关展示、排序场景下的顺序位置情况, + // 并且可以通过对应的setOrders方法来修改这个序号,从而调整书籍的排列顺序等。 public Integer getOrders() { return orders; } + // setOrders方法用于设置书籍排序序号属性的值,外部代码可以传入一个整数参数来更新书籍的排序序号,用于在业务中改变书籍的排列顺序, + // 例如在列表展示中调整书籍的先后位置等情况。 public void setOrders(Integer orders) { this.orders = orders; } + // getSerialversionuid方法返回类的序列化版本UID,这个UID在序列化和反序列化过程中用于验证类的版本一致性, + // 这里直接返回了定义好的静态常量serialVersionUID的值,一般不需要手动修改这个方法的实现,它是按照Java序列化机制要求定义的。 public static long getSerialversionuid() { return serialVersionUID; } + // 重写了父类Model的pkVal方法,用于指定当前实体类的主键值,在这里返回了getUid方法获取到的书籍唯一标识符(uid), + // 这样MyBatis Plus在进行一些基于主键的数据库操作(如根据主键查询、更新等)时就能准确知道当前实体对应的主键是什么,从而正确执行相关操作。 @Override protected Serializable pkVal() { return getUid(); } -} +} \ No newline at end of file diff --git a/tamguo/src/main/java/com/tamguo/service/impl/AdService.java b/tamguo/src/main/java/com/tamguo/service/impl/AdService.java index 5a06426..9ca9a93 100644 --- a/tamguo/src/main/java/com/tamguo/service/impl/AdService.java +++ b/tamguo/src/main/java/com/tamguo/service/impl/AdService.java @@ -13,24 +13,49 @@ import com.tamguo.model.AdEntity; import com.tamguo.service.IAdService; import com.tamguo.util.TamguoConstant; +// 使用 @Service 注解将这个类标记为Spring中的服务层组件,表明它是用于处理业务逻辑的类,Spring会自动扫描并将其纳入到Spring容器管理中, +// 方便在其他地方进行依赖注入等操作,同时也遵循了Spring的分层架构规范,使得代码结构更清晰。 @Service -public class AdService extends ServiceImpl implements IAdService{ - +public class AdService extends ServiceImpl implements IAdService { + + // 通过Spring的依赖注入机制,自动注入 AdMapper 接口的实现类实例, + // AdMapper 应该是用于定义与广告(AdEntity)相关的数据库操作方法的接口(比如查询、插入、更新等操作), + // 在这个服务类中会调用它的方法来与数据库进行交互,获取广告相关的数据。 @Autowired AdMapper adMapper; + + // 同样通过依赖注入机制注入 CacheService 实例,CacheService 大概率是用于处理缓存相关操作的服务类, + // 例如从缓存中获取数据、将数据存入缓存等功能,在这里主要用于缓存广告数据,减少频繁访问数据库的开销,提高系统性能。 @Autowired CacheService cacheService; + /** + * findAll 方法 + * 这个方法重写了接口 IAdService 中的 findAll 方法(从方法签名可以判断,这里实现了接口定义的抽象方法), + * 用于获取所有的广告信息列表,并且加入了缓存相关的逻辑处理,以优化数据获取的性能。 + */ @SuppressWarnings("unchecked") @Override public List findAll() { + // 首先尝试从缓存中获取名为 TamguoConstant.ALL_AD 的缓存对象,这里将获取到的对象强制转换为 List 类型, + // 期望从缓存中获取到之前存储的广告信息列表,如果缓存命中,就可以直接返回这个缓存中的广告列表数据,减少数据库查询操作。 List adList = (List) cacheService.getObject(TamguoConstant.ALL_AD); + // 此处将 adList 赋值为 null,这看起来有点奇怪,可能是代码有误或者有特定的后续逻辑(比如要重新获取最新数据等情况), + // 正常情况下如果是想清空缓存数据重新获取,应该先删除缓存中的对应数据等操作,而不是简单赋值为 null,不过先按照现有逻辑继续分析。 adList = null; - if(adList == null || adList.isEmpty()){ + // 判断从缓存中获取到的广告列表是否为 null 或者为空列表(即没有缓存数据或者缓存数据不存在有效的广告信息), + // 如果满足这个条件,就需要从数据库中重新获取广告信息列表。 + if (adList == null || adList.isEmpty()) { + // 通过注入的 AdMapper 调用其 selectList 方法,传入 Condition.EMPTY 参数,这表示使用默认的查询条件(通常是查询所有记录), + // 从数据库中获取所有的广告实体信息,将获取到的结果赋值给 adList 变量,用于后续操作。 adList = adMapper.selectList(Condition.EMPTY); - cacheService.setObject(TamguoConstant.ALL_AD, adList , 2 * 60 * 60); + // 调用注入的 CacheService 的 setObject 方法,将刚刚从数据库中获取到的广告列表数据(adList)存入缓存中, + // 缓存的键名为 TamguoConstant.ALL_AD,同时设置了缓存的有效时间为 2 * 60 * 60 秒(即 2 小时), + // 这样在接下来的 2 小时内,如果再次调用 findAll 方法,就可以直接从缓存中获取广告数据,而不用再次查询数据库了。 + cacheService.setObject(TamguoConstant.ALL_AD, adList, 2 * 60 * 60); } + // 最后返回获取到的广告信息列表,这个列表可能是从缓存中直接获取的,也可能是从数据库中查询后存入缓存再返回的, + // 取决于缓存中是否存在有效数据以及之前的逻辑执行情况。 return adList; } - } diff --git a/tamguo/src/main/java/com/tamguo/service/impl/AreaService.java b/tamguo/src/main/java/com/tamguo/service/impl/AreaService.java index c6158da..26a0b8e 100644 --- a/tamguo/src/main/java/com/tamguo/service/impl/AreaService.java +++ b/tamguo/src/main/java/com/tamguo/service/impl/AreaService.java @@ -15,48 +15,91 @@ import com.tamguo.service.IAreaService; import com.tamguo.util.Result; import com.tamguo.util.TamguoConstant; +// 使用 @Service 注解将这个类标记为Spring中的服务层组件,意味着它主要负责处理业务逻辑相关的操作, +// Spring在扫描时会自动识别并将其纳入到Spring容器中进行管理,方便在其他地方进行依赖注入等操作,符合Spring的分层架构设计。 @Service -public class AreaService extends ServiceImpl implements IAreaService{ - +public class AreaService extends ServiceImpl implements IAreaService { + + // 通过Spring的依赖注入机制,自动注入 AreaMapper 接口的实现类实例, + // AreaMapper 应该是用于定义与地区(AreaEntity)相关的数据库操作方法的接口,比如查询、插入、更新等操作, + // 在本服务类中会调用它的方法来和数据库进行交互,获取地区相关的数据信息。 @Autowired private AreaMapper areaMapper; + + // 同样通过依赖注入注入 CacheService 实例,CacheService 大概率是用于处理缓存相关操作的服务类, + // 例如从缓存中获取数据、将数据存入缓存以及判断缓存是否存在等功能,在这个类中主要用于缓存地区相关的数据,以优化数据获取性能,减少数据库访问次数。 @Autowired private CacheService cacheService; - + + /** + * findAll 方法 + * 重写了 IAreaService 接口中的 findAll 方法,用于获取所有的地区信息列表, + * 直接调用了通过依赖注入获取的 AreaMapper 的 selectList 方法,并传入 Condition.EMPTY 参数, + * 这意味着使用默认的查询条件(通常是查询所有符合条件的记录,在这里就是所有地区记录)从数据库中获取地区实体信息列表并返回。 + */ @SuppressWarnings("unchecked") @Override public List findAll() { return areaMapper.selectList(Condition.EMPTY); } - @Transactional(readOnly=true) + /** + * findRootArea 方法 + * 重写了 IAreaService 接口中的 findRootArea 方法,该方法被标记为只读事务(通过 @Transactional(readOnly = true) 注解指定), + * 意味着在这个方法执行的数据库操作中,只会进行查询操作,不会进行修改数据的操作(如插入、更新、删除等), + * 调用了 AreaMapper 的 findRootArea 方法,用于从数据库中获取最顶层(根)的地区信息列表,具体的查询逻辑由 AreaMapper 中对应的方法实现来确定, + // 返回查询到的根地区信息列表。 + */ + @Transactional(readOnly = true) @Override public List findRootArea() { return areaMapper.findRootArea(); } - @Transactional(readOnly=true) + /** + * findAreaTree 方法 + * 重写了 IAreaService 接口中的 findAreaTree 方法,同样被标记为只读事务,用于获取地区信息的树形结构数据, + * 方法内部加入了缓存相关的逻辑,先判断缓存中是否已经存在名为 TamguoConstant.AREA_ALL_TREE 的缓存数据, + * 如果缓存中存在,就直接将缓存中的数据作为成功结果返回(通过 Result.successResult 方法进行包装,将缓存数据封装到统一的结果对象中返回), + * 避免重复查询数据库构建地区树结构数据,提高获取数据的效率。 + */ + @Transactional(readOnly = true) @Override public Result findAreaTree() { - if(cacheService.isExist(TamguoConstant.AREA_ALL_TREE)) { + if (cacheService.isExist(TamguoConstant.AREA_ALL_TREE)) { return Result.successResult(cacheService.getObject(TamguoConstant.AREA_ALL_TREE)); } + + // 如果缓存中不存在相应数据,先从数据库中获取根地区信息列表,调用 AreaMapper 的 findRootArea 方法来获取, + // 这些根地区将作为地区树结构的顶层节点,后续会基于这些节点逐步构建完整的地区树。 List areaList = areaMapper.findRootArea(); - for(AreaEntity area : areaList) { + + // 遍历根地区列表,对于每个根地区,查找其下一级子地区信息,通过调用 AreaMapper 的 findByParent 方法,传入当前根地区的唯一标识符(area.getUid())作为参数, + // 获取该根地区对应的子地区列表,然后判断子地区列表是否为空,如果不为空,将子地区列表设置为当前根地区的子节点(通过 area.setChildren(childend) 方法设置), + // 这样就构建了地区树结构的第一层父子关系。 + for (AreaEntity area : areaList) { List childend = areaMapper.findByParent(area.getUid()); - if(!CollectionUtils.isEmpty(childend)) { + if (!CollectionUtils.isEmpty(childend)) { area.setChildren(childend); } - - for(AreaEntity a : childend) { + + // 对于每个子地区,再进一步查找它的下一级子地区(也就是孙子地区等更深层次的子节点),同样通过调用 AreaMapper 的 findByParent 方法, + // 传入子地区的唯一标识符(a.getUid())获取其对应的子地区列表,若列表不为空,将这些子地区设置为当前子地区的子节点(通过 a.setChildren(ceList) 方法设置), + // 以此类推,通过循环嵌套的方式逐步构建出完整的地区树结构,包含多层级的父子关系。 + for (AreaEntity a : childend) { List ceList = areaMapper.findByParent(a.getUid()); - if(!CollectionUtils.isEmpty(ceList)) { + if (!CollectionUtils.isEmpty(ceList)) { a.setChildren(ceList); } } } - cacheService.setObject(TamguoConstant.AREA_ALL_TREE, areaList , 60 * 60 * 2); + + // 将构建好的地区树结构数据存入缓存中,缓存的键名为 TamguoConstant.AREA_ALL_TREE,同时设置缓存的有效时间为 60 * 60 * 2 秒(即 2 小时), + // 这样在接下来的 2 小时内,如果再次调用 findAreaTree 方法,就可以直接从缓存中获取地区树数据,而不用重新构建,提高了数据获取的性能和效率。 + cacheService.setObject(TamguoConstant.AREA_ALL_TREE, areaList, 60 * 60 * 2); + + // 将构建好的地区树结构数据作为成功结果返回(通过 Result.successResult 方法进行包装),方便在调用处统一处理返回结果, + // 并且符合项目中对返回结果进行统一格式封装的规范要求。 return Result.successResult(areaList); } - -} +} \ No newline at end of file diff --git a/tamguo/src/main/java/com/tamguo/service/impl/ChapterService.java b/tamguo/src/main/java/com/tamguo/service/impl/ChapterService.java index 6bd85c1..32bbf1d 100644 --- a/tamguo/src/main/java/com/tamguo/service/impl/ChapterService.java +++ b/tamguo/src/main/java/com/tamguo/service/impl/ChapterService.java @@ -16,84 +16,140 @@ import com.tamguo.model.ChapterEntity; import com.tamguo.service.IChapterService; import com.tamguo.util.TamguoConstant; +// 使用 @Service 注解将这个类标记为Spring中的服务层组件,表明它主要负责处理与章节(Chapter)相关的业务逻辑, +// Spring会自动扫描并将其纳入到Spring容器管理中,方便在其他地方进行依赖注入等操作,遵循了Spring的分层架构设计规范。 @Service -public class ChapterService extends ServiceImpl implements IChapterService{ - +public class ChapterService extends ServiceImpl implements IChapterService { + + // 通过Spring的依赖注入机制,自动注入 ChapterMapper 接口的实现类实例, + // ChapterMapper 是用于定义与章节实体(ChapterEntity)相关的数据库操作方法的接口,例如查询、插入、更新等操作, + // 在本服务类中会调用它的方法来与数据库进行交互,获取章节相关的数据信息。 @Autowired private ChapterMapper chapterMapper; + /** + * findCourseChapter 方法 + * 实现了 IChapterService 接口中的 findCourseChapter 方法,用于获取某课程书籍(通过 bookId 参数指定)下的章节信息,并构建章节的树形结构数据。 + */ @Override public List findCourseChapter(String bookId) { + // 首先调用 ChapterMapper 的 findByBookId 方法,根据传入的书籍 ID(bookId)从数据库中获取该书籍对应的所有章节信息列表, + // 这些章节信息将作为后续构建章节树形结构的基础数据。 List chapterList = chapterMapper.findByBookId(bookId); - - // 获取根chapter UID + + // 初始化用于存储根章节 UID 的变量,初始值设为空字符串,后续会在章节列表中查找真正的根章节 UID 并赋值。 String rootUid = StringUtils.EMPTY; - for(int i=0 ; i entitys = new ArrayList<>(); - for(int i=0 ; i childs = new ArrayList<>(); - for(int k=0 ; k childs = entitys.get(i).getChildChapterList(); - for(int k=0 ; k tmpChilds = new ArrayList<>(); - for(int n=0 ; n getChapterTree(String courseId) { - if(StringUtils.isEmpty(courseId) || "null".equals(courseId)){ + // 首先判断传入的课程 ID 是否为空字符串或者等于 "null"(这里判断 "null" 的方式不太严谨,更合适的做法可能是判断是否为 null 值,不过先按照现有逻辑分析), + // 如果满足这个条件,说明没有有效的课程 ID 传入,那么调用 rootChapterNode 方法返回一个默认的根章节节点列表,作为章节树的基础结构(可能用于没有具体课程关联时的默认展示等情况)。 + if (StringUtils.isEmpty(courseId) || "null".equals(courseId)) { return rootChapterNode(); } + + // 根据传入的课程 ID,调用 ChapterMapper 的 findByBookId 方法(这里可能命名不太准确,根据上下文推测应该是根据课程相关的标识去查找对应的章节信息,也许后续需要确认方法名是否合适), + // 从数据库中获取该课程对应的章节信息列表,然后判断获取到的列表是否为空(即没有找到对应的章节信息), + // 如果为空,同样调用 rootChapterNode 方法返回默认的根章节节点列表作为章节树结构。 List list = chapterMapper.findByBookId(courseId); - if(CollectionUtils.isEmpty(list)) { + if (CollectionUtils.isEmpty(list)) { return rootChapterNode(); } + + // 如果获取到了有效的章节信息列表,就直接返回这个列表,该列表后续可用于构建具体的章节树形结构或者直接在业务中进行展示、处理等操作,具体取决于业务逻辑要求。 return list; } - private List rootChapterNode(){ + /** + * rootChapterNode 方法 + * 这是一个私有方法,用于创建并返回一个默认的根章节节点列表,构建了一个包含单个 ChapterEntity 实例的列表, + * 该实例模拟了一个根章节的基本信息,如设置了课程 ID、顺序号、知识点数量、题目数量、章节 UID、章节名称以及父章节 ID 等属性值, + * 返回的这个默认根章节节点列表可以在没有具体课程章节数据或者作为章节树的顶级节点等情况下使用,确保章节树结构有一个初始的根节点展示。 + */ + private List rootChapterNode() { ChapterEntity chapter = new ChapterEntity(); chapter.setCourseId(TamguoConstant.CHAPTER_DEFAULT_ROOT_UID); chapter.setOrders(0); @@ -104,5 +160,4 @@ public class ChapterService extends ServiceImpl im chapter.setParentId(TamguoConstant.CHAPTER_DEFAULT_ROOT_UID); return Arrays.asList(chapter); } - -} +} \ No newline at end of file diff --git a/tamguo/src/main/java/com/tamguo/service/impl/MemberService.java b/tamguo/src/main/java/com/tamguo/service/impl/MemberService.java index c3c1250..ecc153d 100644 --- a/tamguo/src/main/java/com/tamguo/service/impl/MemberService.java +++ b/tamguo/src/main/java/com/tamguo/service/impl/MemberService.java @@ -18,80 +18,148 @@ import com.tamguo.util.Result; import com.tamguo.util.ShiroUtils; import com.tamguo.util.TamguoConstant; +// 使用 @Service 注解将这个类标记为Spring中的服务层组件,表明它主要负责处理与会员(Member)相关的业务逻辑, +// Spring会自动扫描并将其纳入到Spring容器管理中,方便在其他地方进行依赖注入等操作,遵循Spring的分层架构设计。 @Service -public class MemberService extends ServiceImpl implements IMemberService{ - +public class MemberService extends ServiceImpl implements IMemberService { + + // 通过Spring的依赖注入机制,自动注入 MemberMapper 接口的实现类实例, + // MemberMapper 是用于定义与会员实体(MemberEntity)相关的数据库操作方法的接口,比如查询、插入、更新等操作, + // 在本服务类中会调用它的方法来与数据库进行交互,获取会员相关的数据信息。 @Autowired private MemberMapper memberMapper; + + // 同样通过依赖注入注入 CacheService 实例,CacheService 大概率是用于处理缓存相关操作的服务类, + // 例如从缓存中获取数据、将数据存入缓存以及判断缓存是否存在等功能,在会员相关业务中用于缓存如登录失败次数、验证码等信息,以优化业务操作性能。 @Autowired private CacheService cacheService; + /** + * login 方法 + * 实现了 IMemberService 接口中的 login 方法,用于处理会员登录业务逻辑,主要进行用户名密码验证以及登录失败次数相关的处理。 + */ @Override public Result login(String username, String password) { + // 首先通过 MemberMapper 的 findByUsername 方法,根据传入的用户名(username)从数据库中查找对应的会员实体信息, + // 如果没有找到(即返回 null),说明用户名不存在,直接返回相应的结果提示(用户名或密码有误),封装在 Result 对象中返回给调用者。 MemberEntity member = memberMapper.findByUsername(username); - if(member == null){ + if (member == null) { return Result.result(201, member, "用户名或密码有误,请重新输入或找回密码"); } - Integer loginFailureCount = this.getLoginFailureCount(member); - if(!new Sha256Hash(password).toHex().equals(member.getPassword())){ + + // 获取当前会员的登录失败次数,调用本类中的 getLoginFailureCount 方法来获取,后续用于判断是否达到限制次数等情况。 + Integer loginFailureCount = this.getLoginFailureCount(member); + + // 使用 Sha256Hash 对传入的密码进行哈希处理,并与数据库中存储的会员密码(member.getPassword())进行比对, + // 如果不相等,说明密码错误,此时将登录失败次数加 1,并调用 updateLoginFailureCount 方法更新缓存中的登录失败次数记录, + // 然后返回相应的错误提示结果(用户名或密码有误),封装在 Result 对象中。 + if (!new Sha256Hash(password).toHex().equals(member.getPassword())) { loginFailureCount++; - this.updateLoginFailureCount(member , loginFailureCount); + this.updateLoginFailureCount(member, loginFailureCount); return Result.result(202, member, "用户名或密码有误,请重新输入或找回密码"); } - this.updateLoginFailureCount(member , 0); + + // 如果密码正确,将登录失败次数重置为 0(表示此次登录成功,清除之前的失败记录),调用 updateLoginFailureCount 方法更新缓存中的登录失败次数, + // 最后返回登录成功的结果提示,封装在 Result 对象中返回给调用者,表示会员登录成功。 + this.updateLoginFailureCount(member, 0); return Result.result(200, member, "登录成功"); } - - public void updateLoginFailureCount(MemberEntity member , Integer loginFailureCount){ - cacheService.setObject(TamguoConstant.LOGIN_FAILURE_COUNT + member.getUid(), loginFailureCount , 2 * 60 * 60); + + /** + * updateLoginFailureCount 方法 + * 用于更新会员的登录失败次数信息到缓存中,缓存的键名由 TamguoConstant.LOGIN_FAILURE_COUNT 和会员的唯一标识符(member.getUid())拼接而成, + * 值为传入的登录失败次数(loginFailureCount),同时设置了缓存的有效时间为 2 * 60 * 60 秒(即 2 小时), + // 这样在这 2 小时内,后续获取登录失败次数时会从缓存中读取该值,避免频繁访问数据库来获取和更新这个数据,提高业务操作效率。 + */ + public void updateLoginFailureCount(MemberEntity member, Integer loginFailureCount) { + cacheService.setObject(TamguoConstant.LOGIN_FAILURE_COUNT + member.getUid(), loginFailureCount, 2 * 60 * 60); } - - public Integer getLoginFailureCount(MemberEntity member){ - if(member == null){ + + /** + * getLoginFailureCount 方法 + * 用于获取会员的登录失败次数,首先判断传入的会员实体是否为 null,如果是 null,则直接返回 0,表示没有登录失败次数记录(可能是新用户等情况)。 + * 接着判断缓存中是否存在以 TamguoConstant.LOGIN_FAILURE_COUNT 和会员的唯一标识符(member.getUid())拼接而成的键对应的缓存数据, + // 如果不存在,同样返回 0,表示没有记录到该会员的登录失败次数;如果存在,则从缓存中获取对应的登录失败次数(类型转换为 Integer)并返回。 + */ + public Integer getLoginFailureCount(MemberEntity member) { + if (member == null) { return 0; } - if(!cacheService.isExist(TamguoConstant.LOGIN_FAILURE_COUNT + member.getUid())){ + if (!cacheService.isExist(TamguoConstant.LOGIN_FAILURE_COUNT + member.getUid())) { return 0; } - return (Integer)cacheService.getObject(TamguoConstant.LOGIN_FAILURE_COUNT + member.getUid()); + return (Integer) cacheService.getObject(TamguoConstant.LOGIN_FAILURE_COUNT + member.getUid()); } + /** + * checkUsername 方法 + * 实现了 IMemberService 接口中的 checkUsername 方法,用于检查用户名是否已被使用, + // 通过 MemberMapper 的 findByUsername 方法根据传入的用户名(username)从数据库中查找对应的会员实体, + // 如果找到(即返回的会员实体不为 null),说明用户名已经存在,返回相应的提示结果(该用户名已经存在),封装在 Result 对象中; + // 如果没有找到,则返回表示用户名可用的提示结果,同样封装在 Result 对象中返回给调用者。 + */ @Override public Result checkUsername(String username) { MemberEntity member = memberMapper.findByUsername(username); - if(member != null){ + if (member!= null) { return Result.result(201, null, "该用户名已经存在"); } return Result.result(200, null, "该用户名可用"); } + /** + * checkMobile 方法 + * 实现了 IMemberService 接口中的 checkMobile 方法,用于检查手机号是否已被使用, + // 通过 MemberMapper 的 findByMobile 方法根据传入的手机号(mobile)从数据库中查找对应的会员实体, + // 如果找到(即返回的会员实体不为 null),说明手机号已经存在,返回相应的提示结果(该手机号已经存在),封装在 Result 对象中; + // 如果没有找到,则返回表示手机号可用的提示结果,同样封装在 Result 对象中返回给调用者。 + */ @Override public Result checkMobile(String mobile) { MemberEntity member = memberMapper.findByMobile(mobile); - if(member != null){ + if (member!= null) { return Result.result(201, null, "该手机号已经存在"); } return Result.result(200, null, "该手机号可用"); } - @Transactional(readOnly=false) + /** + * register 方法 + * 实现了 IMemberService 接口中的 register 方法,用于处理会员注册业务逻辑,包含了对用户名、手机号是否已存在的检查,验证码验证以及会员信息插入数据库等操作, + * 被标记为可读写事务(通过 @Transactional(readOnly = false) 注解指定),因为涉及到对数据库的插入操作,需要保证事务的完整性。 + */ + @Transactional(readOnly = false) @Override public Result register(MemberEntity member) { + // 首先通过 MemberMapper 的 findByUsername 方法,根据传入的要注册的用户名(member.getUsername())从数据库中查找是否已存在该用户名的会员, + // 如果找到(即返回的会员实体不为 null),说明用户名已被使用,返回相应的提示结果(该用户已经存在),封装在 Result 对象中。 MemberEntity m = memberMapper.findByUsername(member.getUsername()); - if(m != null){ + if (m!= null) { return Result.result(201, null, "该用户已经存在"); } + + // 接着通过 MemberMapper 的 findByMobile 方法,根据传入的要注册的手机号(member.getMobile())从数据库中查找是否已存在该手机号的会员, + // 如果找到(即返回的会员实体不为 null),说明手机号已被使用,返回相应的提示结果(该手机号已经存在),封装在 Result 对象中。 m = memberMapper.findByMobile(member.getMobile()); - if(m != null){ + if (m!= null) { return Result.result(202, null, "该手机号已经存在"); } - if(!cacheService.isExist(TamguoConstant.ALIYUN_MOBILE_SMS_PREFIX + member.getMobile())){ + + // 检查缓存中是否存在以 TamguoConstant.ALIYUN_MOBILE_SMS_PREFIX 和要注册的手机号(member.getMobile())拼接而成的键对应的验证码缓存数据, + // 如果不存在,说明验证码错误(可能未发送或者已过期等情况),返回相应的提示结果(验证码错误),封装在 Result 对象中。 + if (!cacheService.isExist(TamguoConstant.ALIYUN_MOBILE_SMS_PREFIX + member.getMobile())) { return Result.result(203, null, "验证码错误"); } + + // 从缓存中获取对应的验证码,将获取到的验证码与传入的要注册的会员实体中的验证码(member.getVerifyCode())进行比对, + // 如果不相等,说明验证码错误,返回相应的提示结果(验证码错误),封装在 Result 对象中。 String code = (String) cacheService.getObject(TamguoConstant.ALIYUN_MOBILE_SMS_PREFIX + member.getMobile()); - if(!code.equals(member.getVerifyCode())){ + if (!code.equals(member.getVerifyCode())) { return Result.result(204, null, "验证码错误"); } + + // 创建一个新的 MemberEntity 实例,用于存储要注册的会员信息,设置了一些默认值(如头像使用默认头像 TamguoConstant.DEFAULT_MEMBER_AVATAR), + // 并将传入的会员实体中的相关信息(如手机号、密码、用户名等)设置到新实例中,其中密码使用 Sha256Hash 进行哈希处理后存储,提高安全性。 MemberEntity entity = new MemberEntity(); entity.setAvatar(TamguoConstant.DEFAULT_MEMBER_AVATAR); entity.setMobile(member.getMobile()); @@ -101,30 +169,51 @@ public class MemberService extends ServiceImpl imple entity.setSubjectId(member.getSubjectId()); entity.setCourseId(member.getCourseId()); entity.setEmail(member.getEmail()); + + // 通过 MemberMapper 的 insert 方法将新的会员实体信息插入到数据库中,完成会员注册操作, + // 最后返回注册成功的提示结果,封装在 Result 对象中返回给调用者,表示会员注册成功。 memberMapper.insert(entity); return Result.result(200, entity, "注册成功"); } + /** + * checkAccount 方法 + * 实现了 IMemberService 接口中的 checkAccount 方法,用于检查账号(可以是用户名、邮箱或手机号,根据传入的 account 参数判断)是否存在, + // 首先判断传入的账号是否为空字符串,如果为空,说明账号不存在,返回相应的提示结果(帐号不存在!),封装在 Result 对象中。 + // 接着通过 MemberMapper 的 findByUsernameOrEmailOrMobile 方法根据传入的账号从数据库中查找对应的会员实体, + // 如果没有找到(即返回的会员实体为 null),同样返回表示账号不存在的提示结果,封装在 Result 对象中; + // 如果找到,则返回表示该账号存在的提示结果,封装在 Result 对象中返回给调用者。 + */ @Override public Result checkAccount(String account) { - if(StringUtils.isEmpty(account)){ + if (StringUtils.isEmpty(account)) { return Result.result(201, null, "帐号不存在!"); } MemberEntity member = memberMapper.findByUsernameOrEmailOrMobile(account); - if(member == null){ + if (member == null) { return Result.result(201, null, "帐号不存在!"); } return Result.result(200, null, "该帐号存在"); } + /** + * confirmAccount 方法 + * 实现了 IMemberService 接口中的 confirmAccount 方法,用于确认账号是否存在并验证验证码是否正确, + // 首先判断传入的账号是否为空字符串,如果为空,说明账号不存在,返回相应的提示结果(帐号不存在!),封装在 Result 对象中。 + // 接着通过 MemberMapper 的 findByUsernameOrEmailOrMobile 方法根据传入的账号从数据库中查找对应的会员实体, + // 如果没有找到(即返回的会员实体为 null),同样返回表示账号不存在的提示结果,封装在 Result 对象中。 + // 然后通过 ShiroUtils 的 getKaptcha 方法获取验证码(根据 Constants.KAPTCHA_SESSION_KEY 这个键从相应的会话等地方获取), + // 将获取到的验证码与传入的验证验证码(veritycode)进行比对,如果不相等,说明验证码错误,返回相应的提示结果(验证码错误),封装在 Result 对象中。 + // 如果验证码正确,则返回表示该账号存在的提示结果,封装在 Result 对象中返回给调用者。 + */ @Override public Result confirmAccount(String account, String veritycode) { - if(StringUtils.isEmpty(account)){ + if (StringUtils.isEmpty(account)) { return Result.result(201, null, "帐号不存在!"); } MemberEntity member = memberMapper.findByUsernameOrEmailOrMobile(account); - if(member == null){ - return Result.result(201, null, "帐号不存在!"); + if (member == null) { + return Result.result(201, null, "帐号不存在!");; } String kaptcha = ShiroUtils.getKaptcha(Constants.KAPTCHA_SESSION_KEY); if (!veritycode.equalsIgnoreCase(kaptcha)) { @@ -133,96 +222,37 @@ public class MemberService extends ServiceImpl imple return Result.result(200, member, "该帐号存在"); } + /** + * securityCheck 方法 + * 实现了 IMemberService 接口中的 securityCheck 方法,用于进行安全验证,根据传入的 isEmail 参数判断是验证邮箱验证码还是手机号验证码, + // 首先通过 MemberMapper 的 findByUsername 方法根据传入的用户名(username)从数据库中查找对应的会员实体, + // 如果 isEmail 参数为 "1",表示验证邮箱验证码,此时检查缓存中是否存在以 TamguoConstant.ALIYUN_MAIL_FIND_PASSWORD_PREFIX 和会员的邮箱(member.getEmail())拼接而成的键对应的验证码缓存数据, + // 如果不存在,说明验证码错误,返回相应的提示结果(验证码错误),封装在 Result 对象中。 + // 接着从缓存中获取对应的验证码,将获取到的验证码与传入的验证验证码(vcode)进行比对,如果不相等,说明验证码错误,返回相应的提示结果(验证码错误),封装在 Result 对象中。 + // 如果 isEmail 参数不为 "1",表示验证手机号验证码,进行类似的操作,检查手机号对应的验证码缓存数据是否存在以及比对验证码是否正确, + // 如果验证通过,生成一个唯一的随机字符串(使用 UUID.randomUUID().toString())作为键,将用户名存入缓存中(缓存键名为 TamguoConstant.SECURITY_CHECK_PREFIX 加上生成的随机字符串), + // 设置缓存的有效时间为 2 * 60 * 60 秒(即 2 小时),最后返回安全验证通过的提示结果,封装在 Result 对象中返回给调用者,同时将生成的随机字符串作为结果数据返回。 + */ @Override - public Result securityCheck(String username , String isEmail , String vcode) { + public Result securityCheck(String username, String isEmail, String vcode) { MemberEntity member = memberMapper.findByUsername(username); - if("1".equals(isEmail)){ - if(!cacheService.isExist(TamguoConstant.ALIYUN_MAIL_FIND_PASSWORD_PREFIX + member.getEmail())){ + if ("1".equals(isEmail)) { + if (!cacheService.isExist(TamguoConstant.ALIYUN_MAIL_FIND_PASSWORD_PREFIX + member.getEmail())) { return Result.result(201, member, "验证码错误"); } String code = (String) cacheService.getObject(TamguoConstant.ALIYUN_MAIL_FIND_PASSWORD_PREFIX + member.getEmail()); - if(!code.equals(vcode)){ + if (!code.equals(vcode)) { return Result.result(202, member, "验证码错误"); } - }else{ - if(!cacheService.isExist(TamguoConstant.ALIYUN_MOBILE_SMS_PREFIX + member.getMobile())){ + } else { + if (!cacheService.isExist(TamguoConstant.ALIYUN_MOBILE_SMS_PREFIX + member.getMobile())) { return Result.result(203, member, "验证码错误"); } String code = (String) cacheService.getObject(TamguoConstant.ALIYUN_MOBILE_SMS_PREFIX + member.getMobile()); - if(!code.equals(vcode)){ + if (!code.equals(vcode)) { return Result.result(204, member, "验证码错误"); } } String key = UUID.randomUUID().toString(); - cacheService.setObject(TamguoConstant.SECURITY_CHECK_PREFIX + key, username , 2 * 60 * 60); - return Result.result(200, key, "安全验证通过"); - } - - @Override - public Result resetPassword(String resetPasswordKey , String username , String password, String verifypwd) { - if(cacheService.isExist(TamguoConstant.SECURITY_CHECK_PREFIX + resetPasswordKey)){ - MemberEntity member = memberMapper.findByUsername(username); - if(password.equals(verifypwd)){ - member.setPassword(new Sha256Hash(password).toHex()); - memberMapper.updateById(member); - } - } - return Result.result(200, null, "更新成功"); - } - - @Transactional(readOnly=false) - @Override - public void updateMember(MemberEntity member) { - MemberEntity entity = memberMapper.selectById(ShiroUtils.getUserId()); - entity.setAvatar(member.getAvatar()); - entity.setEmail(member.getEmail()); - entity.setMobile(member.getMobile()); - entity.setCourseId(member.getCourseId()); - entity.setSubjectId(member.getSubjectId()); - entity.setNickName(member.getNickName()); - - memberMapper.updateById(entity); - } - - @Transactional(readOnly=true) - @Override - public MemberEntity findByUid(String uid) { - return memberMapper.selectById(uid); - } - - @Transactional(readOnly=true) - @Override - public MemberEntity findByUsername(String username) { - return memberMapper.findByUsername(username); - } - - @Transactional(readOnly=false) - @Override - public void updateLastLoginTime(String uid) { - MemberEntity member = memberMapper.selectById(uid); - member.setLastLoginTime(DateUtil.getTime()); - memberMapper.updateById(member); - } - - @Override - public MemberEntity findCurrMember() { - MemberEntity member = memberMapper.selectById(ShiroUtils.getUserId()); - member.setPassword(null); - return member; - } - - @Transactional(readOnly=false) - @Override - public Result updatePwd(MemberEntity member) { - MemberEntity entity = memberMapper.selectById(ShiroUtils.getUserId()); - if(!entity.getPassword().equals(new Sha256Hash(member.getPassword()).toHex())) { - return Result.result(501, null, "旧密码错误!"); - } - if(!cacheService.isExist(TamguoConstant.ALIYUN_MOBILE_SMS_PREFIX + member.getMobile())){ - return Result.result(502, null, "验证码错误"); - } - entity.setPassword(new Sha256Hash(member.getNowPassword()).toHex()); - return Result.result(0, null, "修改成功"); - } - -} + cacheService.setObject(TamguoConstant.SECURITY_CHECK_PREFIX + key, username, 2 * 60 * 60); + return Result.result(200 \ No newline at end of file diff --git a/tamguo/src/main/java/com/tamguo/service/impl/MenuService.java b/tamguo/src/main/java/com/tamguo/service/impl/MenuService.java index 51f90d8..97505ae 100644 --- a/tamguo/src/main/java/com/tamguo/service/impl/MenuService.java +++ b/tamguo/src/main/java/com/tamguo/service/impl/MenuService.java @@ -12,101 +12,188 @@ import com.tamguo.model.MenuEntity; import com.tamguo.service.IMenuService; import com.tamguo.util.TamguoConstant; +// 使用 @Service 注解将这个类标记为Spring中的服务层组件,意味着它主要负责处理与菜单(Menu)相关的业务逻辑, +// Spring会自动扫描并将其纳入到Spring容器管理中,方便在其他地方进行依赖注入等操作,遵循了Spring的分层架构设计规范。 @Service -public class MenuService extends ServiceImpl implements IMenuService{ - +public class MenuService extends ServiceImpl implements IMenuService { + + // 通过Spring的依赖注入机制,自动注入 MenuMapper 接口的实现类实例, + // MenuMapper 是用于定义与菜单实体(MenuEntity)相关的数据库操作方法的接口,例如查询、插入、更新等操作, + // 在本服务类中会调用它的方法来与数据库进行交互,获取菜单相关的数据信息。 @Autowired private MenuMapper menuMapper; + + // 同样通过依赖注入注入 CacheService 实例,CacheService 大概率是用于处理缓存相关操作的服务类, + // 例如从缓存中获取数据、将数据存入缓存以及判断缓存是否存在等功能,在这个类中主要用于缓存不同类型的菜单数据, + // 以减少频繁访问数据库获取菜单信息的开销,提高系统性能,优化菜单相关业务操作的响应速度。 @Autowired private CacheService cacheService; + /** + * findMenus 方法 + * 实现了 IMenuService 接口中的 findMenus 方法,用于获取首页菜单信息列表,并加入了缓存相关逻辑来优化数据获取性能。 + */ @SuppressWarnings("unchecked") @Override public List findMenus() { + // 首先尝试从缓存中获取名为 TamguoConstant.INDEX_MENU 的缓存对象,并将其强制转换为 List 类型, + // 期望从缓存中获取到之前存储的首页菜单信息列表,如果缓存命中,就可以直接返回这个缓存中的菜单列表数据,避免重复查询数据库。 List menuList = ((List) cacheService.getObject(TamguoConstant.INDEX_MENU)); + // 判断从缓存中获取到的菜单列表是否为 null 或者为空列表(即没有缓存数据或者缓存数据不存在有效的菜单信息), + // 如果满足这个条件,就需要从数据库中重新获取首页菜单信息并构建菜单结构,然后存入缓存中供后续使用。 if (menuList == null || menuList.isEmpty()) { + // 通过注入的 MenuMapper 调用其 findFatherMenus 方法,获取首页的父级菜单信息列表,这些父级菜单将作为构建菜单树形结构的顶层节点, + // 后续会基于这些父菜单查找并添加对应的子菜单,形成完整的菜单结构。 menuList = menuMapper.findFatherMenus(); - for(MenuEntity menu : menuList){ + // 遍历获取到的父级菜单列表,对于每个父菜单(menu),通过调用 MenuMapper 的 findMenuByParentId 方法,传入当前父菜单的唯一标识符(menu.getUid())作为参数, + // 获取该父菜单对应的子菜单列表,然后将获取到的子菜单列表设置为当前父菜单的子节点(通过 menu.setChildSubjects(childSubjects) 方法设置), + // 这样就构建了首页菜单的树形结构,包含了父子菜单的层级关系。 + for (MenuEntity menu : menuList) { List childSubjects = menuMapper.findMenuByParentId(menu.getUid()); menu.setChildSubjects(childSubjects); } - cacheService.setObject(TamguoConstant.INDEX_MENU, menuList , 2 * 60 * 60); + // 将构建好的首页菜单树形结构数据存入缓存中,缓存的键名为 TamguoConstant.INDEX_MENU,同时设置了缓存的有效时间为 2 * 60 * 60 秒(即 2 小时), + // 这样在接下来的 2 小时内,如果再次调用 findMenus 方法,就可以直接从缓存中获取首页菜单数据,而不用再次从数据库中查询和构建菜单结构了,提高了数据获取的效率。 + cacheService.setObject(TamguoConstant.INDEX_MENU, menuList, 2 * 60 * 60); } + // 最后返回获取到的首页菜单信息列表,这个列表可能是从缓存中直接获取的,也可能是从数据库中查询后存入缓存再返回的, + // 取决于缓存中是否存在有效数据以及之前的逻辑执行情况。 return menuList; } + /** + * findAllMenus 方法 + * 实现了 IMenuService 接口中的 findAllMenus 方法,用于获取所有菜单信息列表,同样结合了缓存机制来优化数据获取过程,避免重复查询数据库。 + */ @SuppressWarnings("unchecked") @Override public List findAllMenus() { + // 首先尝试从缓存中获取名为 TamguoConstant.ALL_INDEX_MENU 的缓存对象,并将其强制转换为 List 类型, + // 期望获取到之前存储的所有菜单信息列表,如果缓存命中,直接返回该缓存中的菜单列表数据即可。 List allMenuList = ((List) cacheService.getObject(TamguoConstant.ALL_INDEX_MENU)); - if(allMenuList == null || allMenuList.isEmpty()){ + // 判断从缓存中获取到的所有菜单列表是否为 null 或者为空列表,如果是,就需要从数据库中重新获取并构建所有菜单的树形结构数据,然后存入缓存。 + if (allMenuList == null || allMenuList.isEmpty()) { + // 通过 MenuMapper 的 findAllFatherMenus 方法从数据库中获取所有的父级菜单信息列表,这些父菜单将作为构建整个菜单树形结构的顶层节点基础。 allMenuList = menuMapper.findAllFatherMenus(); - for(MenuEntity menu : allMenuList){ + // 遍历所有的父级菜单列表,对于每个父菜单(menu),调用 MenuMapper 的 findMenuByParentId 方法,传入父菜单的唯一标识符(menu.getUid())获取其对应的子菜单列表, + // 然后将子菜单列表设置为当前父菜单的子节点(通过 menu.setChildSubjects(childSubjects) 方法设置),以此构建出包含所有菜单的树形结构,展示完整的菜单层级关系。 + for (MenuEntity menu : allMenuList) { List childSubjects = menuMapper.findMenuByParentId(menu.getUid()); menu.setChildSubjects(childSubjects); } - cacheService.setObject(TamguoConstant.ALL_INDEX_MENU, allMenuList , 2 * 60 * 60); + // 将构建好的所有菜单树形结构数据存入缓存中,缓存键名为 TamguoConstant.ALL_INDEX_MENU,缓存有效时间设置为 2 * 60 * 60 秒(2 小时), + // 以便后续在缓存有效期内可以直接从缓存获取所有菜单数据,减少数据库查询操作,提高性能。 + cacheService.setObject(TamguoConstant.ALL_INDEX_MENU, allMenuList, 2 * 60 * 60); } + // 最后返回获取到的所有菜单信息列表,该列表可能来自缓存或者是新从数据库查询构建后存入缓存再返回的,具体取决于缓存情况和之前的逻辑执行结果。 return allMenuList; } + /** + * findLeftMenus 方法 + * 实现了 IMenuService 接口中的 findLeftMenus 方法,用于获取左侧菜单信息列表,同样在获取数据过程中运用了缓存逻辑来提升性能。 + */ @SuppressWarnings("unchecked") @Override public List findLeftMenus() { + // 先从缓存中获取名为 TamguoConstant.LEFT_INDEX_MENU 的缓存对象,并强制转换为 List 类型,尝试获取之前缓存的左侧菜单信息列表, + // 如果缓存中有数据,就直接返回该缓存中的菜单列表数据,避免再次查询数据库。 List leftMenuList = ((List) cacheService.getObject(TamguoConstant.LEFT_INDEX_MENU)); - if(leftMenuList == null || leftMenuList.isEmpty()){ + // 判断从缓存中获取到的左侧菜单列表是否为 null 或者为空,如果是,就需要从数据库中重新获取并构建左侧菜单的树形结构数据,然后存入缓存供后续使用。 + if (leftMenuList == null || leftMenuList.isEmpty()) { + // 通过 MenuMapper 的 findLeftFatherMenus 方法从数据库中获取左侧菜单的父级菜单信息列表,这些父菜单将作为左侧菜单树形结构的顶层节点。 leftMenuList = menuMapper.findLeftFatherMenus(); - for(MenuEntity menu : leftMenuList){ + // 遍历左侧菜单的父级菜单列表,对于每个父菜单(menu),调用 MenuMapper 的 findMenuByParentId 方法,传入父菜单的唯一标识符(menu.getUid())获取对应的子菜单列表, + // 再将子菜单列表设置为当前父菜单的子节点(通过 menu.setChildSubjects(childSubjects) 方法设置),以此构建出左侧菜单的树形结构,包含父子菜单的层级关系。 + for (MenuEntity menu : leftMenuList) { List childSubjects = menuMapper.findMenuByParentId(menu.getUid()); menu.setChildSubjects(childSubjects); } - cacheService.setObject(TamguoConstant.LEFT_INDEX_MENU, leftMenuList , 2 * 60 * 60); + // 将构建好的左侧菜单树形结构数据存入缓存中,缓存键名为 TamguoConstant.LEFT_INDEX_MENU,缓存有效时间设置为 2 * 60 * 60 秒(2 小时), + // 这样后续在有效期内再次调用 findLeftMenus 方法时,就可以直接从缓存获取左侧菜单数据,提高获取数据的效率。 + cacheService.setObject(TamguoConstant.LEFT_INDEX_MENU, leftMenuList, 2 * 60 * 60); } + // 最后返回获取到的左侧菜单信息列表,该列表的来源取决于缓存是否命中以及之前的逻辑执行情况,可能是缓存数据也可能是新从数据库获取后存入缓存再返回的数据。 return leftMenuList; } - + /** + * findChapterMenus 方法 + * 实现了 IMenuService 接口中的 findChapterMenus 方法,用于获取章节相关菜单信息列表,并且在获取过程中通过缓存机制来优化性能,减少数据库查询次数。 + */ @SuppressWarnings("unchecked") @Override public List findChapterMenus() { + // 首先从缓存中获取名为 TamguoConstant.CHAPTER_INDEX_MENU 的缓存对象,并强制转换为 List 类型,尝试获取之前缓存的章节相关菜单信息列表, + // 如果缓存中有有效数据,就直接返回该缓存中的菜单列表数据,无需再次查询数据库。 List chapterMenuList = ((List) cacheService.getObject(TamguoConstant.CHAPTER_INDEX_MENU)); - if(chapterMenuList == null || chapterMenuList.isEmpty()){ + // 判断从缓存中获取到的章节菜单列表是否为 null 或者为空,如果是这种情况,就需要从数据库中重新获取并构建章节菜单的树形结构数据,然后存入缓存以便后续使用。 + if (chapterMenuList == null || chapterMenuList.isEmpty()) { + // 通过 MenuMapper 的 findChapterFatherMenus 方法从数据库中获取章节相关菜单的父级菜单信息列表,这些父菜单将作为章节菜单树形结构的顶层节点。 chapterMenuList = menuMapper.findChapterFatherMenus(); - for(MenuEntity menu : chapterMenuList){ + // 遍历章节菜单的父级菜单列表,对于每个父菜单(menu),调用 MenuMapper 的 findMenuByParentId 方法,传入父菜单的唯一标识符(menu.getUid())获取对应的子菜单列表, + // 接着将子菜单列表设置为当前父菜单的子节点(通过 menu.setChildSubjects(childSubjects) 方法设置),以此构建出章节菜单的树形结构,体现父子菜单的层级关系。 + for (MenuEntity menu : chapterMenuList) { List childSubjects = menuMapper.findMenuByParentId(menu.getUid()); menu.setChildSubjects(childSubjects); } - cacheService.setObject(TamguoConstant.CHAPTER_INDEX_MENU, chapterMenuList , 2 * 60 * 60); + // 将构建好的章节菜单树形结构数据存入缓存中,缓存键名为 TamguoConstant.CHAPTER_INDEX_MENU,缓存有效时间设置为 2 * 60 * 60 秒(2 小时), + // 这样在后续的 2 小时内,再次调用 findChapterMenus 方法时,就可以直接从缓存获取章节菜单数据,提高数据获取效率,减少数据库操作开销。 + cacheService.setObject(TamguoConstant.CHAPTER_INDEX_MENU, chapterMenuList, 2 * 60 * 60); } + // 最后返回获取到的章节菜单信息列表,其来源取决于缓存是否命中以及之前的逻辑执行情况,可能是直接从缓存获取的数据,也可能是新从数据库获取后存入缓存再返回的数据。 return chapterMenuList; } - + /** + * findFooterMenus 方法 + * 实现了 IMenuService 接口中的 findFooterMenus 方法,用于获取底部菜单信息列表,同样结合了缓存逻辑来优化获取数据的性能,避免频繁查询数据库。 + */ @SuppressWarnings("unchecked") @Override public List findFooterMenus() { + // 先从缓存中获取名为 TamguoConstant.FOOTER_INDEX_MENU 的缓存对象,并强制转换为 List 类型,尝试获取之前缓存的底部菜单信息列表, + // 这里有个特殊处理,先将获取到的列表赋值为 null,不过从常规逻辑来看,这样可能不太合适,一般如果要清空缓存重新获取数据,会有更合理的删除缓存等操作方式, + // 暂时按照现有代码逻辑继续分析,后续可能需要确认此处的真实意图是否合理。 List footerMenuList = ((List) cacheService.getObject(TamguoConstant.FOOTER_INDEX_MENU)); footerMenuList = null; - if(footerMenuList == null || footerMenuList.isEmpty()){ + // 判断底部菜单列表是否为 null 或者为空,如果是这种情况,就需要从数据库中重新获取并构建底部菜单的树形结构数据,然后存入缓存供后续调用使用。 + if (footerMenuList == null || footerMenuList.isEmpty()) { + // 通过 MenuMapper 的 findFooterFatherMenus 方法从数据库中获取底部菜单的父级菜单信息列表,这些父菜单将作为底部菜单树形结构的顶层节点。 footerMenuList = menuMapper.findFooterFatherMenus(); - for(MenuEntity menu : footerMenuList){ + // 遍历底部菜单的父级菜单列表,对于每个父菜单(menu),调用 MenuMapper 的 findMenuByParentId 方法,传入父菜单的唯一标识符(menu.getUid())获取对应的子菜单列表, + // 再将子菜单列表设置为当前父菜单的子节点(通过 menu.setChildSubjects(childSubjects) 方法设置),以此构建出底部菜单的树形结构,展示父子菜单的层级关系。 + for (MenuEntity menu : footerMenuList) { List childSubjects = menuMapper.findMenuByParentId(menu.getUid()); menu.setChildSubjects(childSubjects); } - cacheService.setObject(TamguoConstant.FOOTER_INDEX_MENU, footerMenuList , 2 * 60 * 60); + // 将构建好的底部菜单树形结构数据存入缓存中,缓存键名为 TamguoConstant.FOOTER_INDEX_MENU,缓存有效时间设置为 2 * 60 * 60 秒(2 小时), + // 这样在后续的 2 小时内,再次调用 findFooterMenus 方法时,就可以直接从缓存获取底部菜单数据,提高获取数据的效率,减少数据库查询操作。 + cacheService.setObject(TamguoConstant.FOOTER_INDEX_MENU, footerMenuList, 2 * 60 * 60); } + // 最后返回获取到的底部菜单信息列表,其来源取决于缓存情况以及之前的逻辑执行结果,可能是缓存中的数据,也可能是新从数据库获取后存入缓存再返回的数据。 return footerMenuList; } + /** + * getMenuTree 方法 + * 实现了 IMenuService 接口中的 getMenuTree 方法,用于获取菜单的树形结构数据, + // 直接调用了 MenuMapper 的 selectList 方法,并传入 Condition.EMPTY 参数,这意味着使用默认的查询条件(通常是查询所有符合条件的记录,在这里就是所有菜单记录) + // 从数据库中获取菜单实体信息列表,该列表可以作为构建菜单树形结构或者其他菜单相关业务处理的基础数据,返回获取到的菜单实体列表。 + */ @SuppressWarnings("unchecked") @Override public List getMenuTree() { return menuMapper.selectList(Condition.EMPTY); } + /** + * findById 方法 + * 实现了 IMenuService 接口中的 findById 方法,用于根据菜单的唯一标识符(uid)从数据库中查询并获取对应的菜单实体信息, + // 直接调用了 MenuMapper 的 selectById 方法,传入菜单 UID 参数(uid),由 MenuMapper 具体实现与数据库的交互查询操作,返回查询到的菜单实体对象。 + */ @Override public MenuEntity findById(String uid) { return menuMapper.selectById(uid); } - -} +} \ No newline at end of file diff --git a/tamguo/src/main/java/com/tamguo/service/impl/PaperService.java b/tamguo/src/main/java/com/tamguo/service/impl/PaperService.java index c82c531..67c14f6 100644 --- a/tamguo/src/main/java/com/tamguo/service/impl/PaperService.java +++ b/tamguo/src/main/java/com/tamguo/service/impl/PaperService.java @@ -22,97 +22,192 @@ import com.tamguo.util.Result; import com.tamguo.util.ShiroUtils; import com.tamguo.util.TamguoConstant; +// 使用 @Service 注解将这个类标记为Spring中的服务层组件,表明它主要负责处理与试卷(Paper)相关的业务逻辑, +// Spring会自动扫描并将其纳入到Spring容器管理中,方便在其他地方进行依赖注入等操作,遵循Spring的分层架构设计。 @Service -public class PaperService extends ServiceImpl implements IPaperService{ - +public class PaperService extends ServiceImpl implements IPaperService { + + // 通过Spring的依赖注入机制,自动注入 PaperMapper 接口的实现类实例, + // PaperMapper 是用于定义与试卷实体(PaperEntity)相关的数据库操作方法的接口,比如查询、插入、更新、删除等操作, + // 在本服务类中会调用它的方法来与数据库进行交互,获取试卷相关的数据信息。 @Autowired private PaperMapper paperMapper; + + // 同样通过依赖注入注入 CacheService 实例,CacheService 大概率是用于处理缓存相关操作的服务类, + // 例如从缓存中获取数据、将数据存入缓存以及判断缓存是否存在等功能,在试卷相关业务中用于缓存不同类型的试卷列表数据, + // 以减少频繁访问数据库获取试卷信息的开销,提高系统性能,优化试卷相关业务操作的响应速度。 @Autowired private CacheService cacheService; + + // 通过依赖注入注入 QuestionMapper 接口的实现类实例,QuestionMapper 应该是用于定义与试题相关的数据库操作方法的接口, + // 在涉及试卷中试题相关的操作(如添加、删除、更新试题信息等)时会调用它的方法来与数据库进行交互。 @Autowired private QuestionMapper questionMapper; + /** + * findHistoryPaper 方法 + * 实现了 IPaperService 接口中的 findHistoryPaper 方法,用于获取历史试卷信息列表,并结合了缓存机制来优化数据获取性能,避免重复查询数据库。 + */ @SuppressWarnings("unchecked") @Override public List findHistoryPaper() { + // 首先尝试从缓存中获取名为 TamguoConstant.HISTORY_PAPER 的缓存对象,并将其强制转换为 List 类型, + // 期望从缓存中获取到之前存储的历史试卷信息列表,如果缓存命中,就可以直接返回这个缓存中的试卷列表数据,减少数据库查询操作。 List paperList = (List) cacheService.getObject(TamguoConstant.HISTORY_PAPER); - if(paperList == null || paperList.isEmpty()){ - Page page = new Page<>(1 , 6); - paperList = paperMapper.findByTypeAndAreaId(TamguoConstant.ZHENGTI_PAPER_ID , TamguoConstant.BEIJING_AREA_ID , page); - cacheService.setObject(TamguoConstant.ZHENGTI_PAPER_ID, paperList , 2 * 60 * 60); + // 判断从缓存中获取到的试卷列表是否为 null 或者为空列表(即没有缓存数据或者缓存数据不存在有效的试卷信息), + // 如果满足这个条件,就需要从数据库中重新获取历史试卷信息列表,并将其存入缓存中供后续使用。 + if (paperList == null || paperList.isEmpty()) { + // 创建一个 Page 对象,用于分页查询,设置当前页码为 1,每页显示的记录数为 6(这里的分页参数可以根据实际业务需求调整), + Page page = new Page<>(1, 6); + // 通过注入的 PaperMapper 的 findByTypeAndAreaId 方法,传入特定的试卷类型标识符(TamguoConstant.ZHENGTI_PAPER_ID)和地区标识符(TamguoConstant.BEIJING_AREA_ID)以及分页对象(page), + // 从数据库中获取符合条件的历史试卷信息列表,这里的具体查询逻辑由 PaperMapper 中对应的方法实现来确定,返回查询到的历史试卷列表赋值给 paperList 变量。 + paperList = paperMapper.findByTypeAndAreaId(TamguoConstant.ZHENGTI_PAPER_ID, TamguoConstant.BEIJING_AREA_ID, page); + // 将获取到的历史试卷列表存入缓存中,缓存的键名为 TamguoConstant.ZHENGTI_PAPER_ID,同时设置了缓存的有效时间为 2 * 60 * 60 秒(即 2 小时), + // 这样在接下来的 2 小时内,如果再次调用 findHistoryPaper 方法,就可以直接从缓存中获取历史试卷数据,而不用再次查询数据库了。 + cacheService.setObject(TamguoConstant.ZHENGTI_PAPER_ID, paperList, 2 * 60 * 60); } + // 最后返回获取到的历史试卷信息列表,这个列表可能是从缓存中直接获取的,也可能是从数据库中查询后存入缓存再返回的, + // 取决于缓存中是否存在有效数据以及之前的逻辑执行情况。 return paperList; } + /** + * findSimulationPaper 方法 + * 实现了 IPaperService 接口中的 findSimulationPaper 方法,用于获取模拟试卷信息列表,同样运用了缓存机制来提升数据获取效率,减少数据库访问次数。 + */ @SuppressWarnings("unchecked") @Override public List findSimulationPaper() { + // 先从缓存中获取名为 TamguoConstant.SIMULATION_PAPER 的缓存对象,并强制转换为 List 类型,尝试获取之前缓存的模拟试卷信息列表, + // 如果缓存中有数据,就直接返回该缓存中的试卷列表数据,避免再次查询数据库。 List paperList = (List) cacheService.getObject(TamguoConstant.SIMULATION_PAPER); - if(paperList == null || paperList.isEmpty()){ - Page page = new Page<>(1 , 6); - paperList = paperMapper.findByTypeAndAreaId(TamguoConstant.MONI_PAPER_ID , TamguoConstant.BEIJING_AREA_ID , page); - cacheService.setObject(TamguoConstant.SIMULATION_PAPER, paperList , 2 * 60 * 60); + // 判断从缓存中获取到的模拟试卷列表是否为 null 或者为空,如果是这种情况,就需要从数据库中重新获取模拟试卷信息列表并缓存起来供后续使用。 + if (paperList == null || paperList.isEmpty()) { + // 创建一个 Page 对象用于分页查询,设置当前页码为 1,每页显示记录数为 6,可根据实际情况调整分页参数。 + Page page = new Page<>(1, 6); + // 通过 PaperMapper 的 findByTypeAndAreaId 方法,传入模拟试卷类型标识符(TamguoConstant.MONI_PAPER_ID)和地区标识符(TamguoConstant.BEIJING_AREA_ID)以及分页对象(page), + // 从数据库中获取符合条件的模拟试卷信息列表,具体查询逻辑由 PaperMapper 中对应方法实现来确定,将查询到的模拟试卷列表赋值给 paperList 变量。 + paperList = paperMapper.findByTypeAndAreaId(TamguoConstant.MONI_PAPER_ID, TamguoConstant.BEIJING_AREA_ID, page); + // 将获取到的模拟试卷列表存入缓存中,缓存键名为 TamguoConstant.SIMULATION_PAPER,缓存有效时间设置为 2 * 60 * 60 秒(2 小时), + // 以便后续在有效期内再次调用 findSimulationPaper 方法时,能直接从缓存获取模拟试卷数据,提高获取数据的效率。 + cacheService.setObject(TamguoConstant.SIMULATION_PAPER, paperList, 2 * 60 * 60); } + // 最后返回获取到的模拟试卷信息列表,其来源取决于缓存是否命中以及之前的逻辑执行情况,可能是缓存数据也可能是新从数据库获取后存入缓存再返回的数据。 return paperList; } + /** + * findHotPaper 方法(第一个重载方法,接收一个 String 类型的 areaId 参数) + * 实现了 IPaperService 接口中的 findHotPaper 方法(这个方法有多个重载形式,此处是其中之一),用于获取热门试卷信息列表,同样结合了缓存逻辑来优化性能。 + */ @SuppressWarnings("unchecked") @Override public List findHotPaper(String areaId) { + // 首先从缓存中获取名为 TamguoConstant.HOT_PAPER 的缓存对象,并强制转换为 List 类型,尝试获取之前缓存的热门试卷信息列表, + // 这里有个特殊处理,先将获取到的列表赋值为 null,不过通常来说这样的操作不太符合常规逻辑,可能需要确认此处的真实意图是否合理, + // 暂时按照现有代码逻辑继续分析,后续可能需要优化此处代码。 List paperList = (List) cacheService.getObject(TamguoConstant.HOT_PAPER); paperList = null; - if(paperList == null || paperList.isEmpty()){ - Page page = new Page<>(1 , 10); - paperList = paperMapper.findByAreaId(areaId ,page); - cacheService.setObject(TamguoConstant.HOT_PAPER, paperList , 2 * 60 * 60); + // 判断热门试卷列表是否为 null 或者为空,如果是这种情况,就需要从数据库中重新获取热门试卷信息列表并缓存起来供后续调用使用。 + if (paperList == null || paperList.isEmpty()) { + // 创建一个 Page 对象用于分页查询,设置当前页码为 1,每页显示记录数为 10(可根据业务实际情况调整分页参数), + Page page = new Page<>(1, 10); + // 通过 PaperMapper 的 findByAreaId 方法,传入地区标识符(areaId)和分页对象(page),从数据库中获取该地区的热门试卷信息列表, + // 具体的热门试卷判定以及查询逻辑由 PaperMapper 中对应的方法实现来确定,将查询到的热门试卷列表赋值给 paperList 变量。 + paperList = paperMapper.findByAreaId(areaId, page); + // 将构建好的热门试卷列表存入缓存中,缓存键名为 TamguoConstant.HOT_PAPER,缓存有效时间设置为 2 * 60 * 60 秒(2 小时), + // 这样在后续的 2 小时内,再次调用 findHotPaper 方法时,就可以直接从缓存获取热门试卷数据,提高获取数据的效率,减少数据库查询操作。 + cacheService.setObject(TamguoConstant.HOT_PAPER, paperList, 2 * 60 * 60); } + // 最后返回获取到的热门试卷信息列表,其来源取决于缓存情况以及之前的逻辑执行结果,可能是缓存中的数据,也可能是新从数据库获取后存入缓存再返回的数据。 return paperList; } + /** + * findList 方法 + * 实现了 IPaperService 接口中的 findList 方法,用于根据多个条件(学科 ID、课程 ID、试卷类型、年份、地区以及页码等)查询试卷信息列表,并进行分页处理, + * 返回包含查询结果的 Page 对象,方便前端进行分页展示等操作。 + */ @Override - public Page findList(String subjectId , String courseId, - String paperType, String year, String area , Integer pageNum) { - Page page = new Page<>(pageNum , TamguoConstant.DEFAULT_PAGE_SIZE); - if("0".equals(courseId)) { + public Page findList(String subjectId, String courseId, + String paperType, String year, String area, Integer pageNum) { + // 创建一个 Page 对象用于分页查询,设置当前页码为传入的 pageNum 参数值,每页显示的记录数为 TamguoConstant.DEFAULT_PAGE_SIZE(这应该是预定义的每页默认显示记录数常量), + Page page = new Page<>(pageNum, TamguoConstant.DEFAULT_PAGE_SIZE); + // 判断课程 ID 是否为 "0",如果是,将其设置为空字符串,可能是表示不根据课程 ID 进行筛选的情况,具体业务含义取决于项目设计。 + if ("0".equals(courseId)) { courseId = ""; } - if("0".equals(paperType)) { + // 类似地,判断试卷类型是否为 "0",如果是,将其设置为空字符串,意味着不依据试卷类型进行筛选,具体根据业务逻辑来确定这样处理的意义。 + if ("0".equals(paperType)) { paperType = ""; } - if("0".equals(year)) { + // 判断年份是否为 "0",若是则设置为空字符串,可能表示不按照年份来筛选试卷,同样取决于业务中对年份筛选的具体规则。 + if ("0".equals(year)) { year = ""; } - if("0".equals(area)) { + // 判断地区是否为 "0",若是则设置为空字符串,即不通过地区来筛选试卷,这与业务中地区作为筛选条件的具体定义相关。 + if ("0".equals(area)) { area = ""; } - return page.setRecords(paperMapper.findList(subjectId , courseId , paperType , year , area , page)); + // 通过 PaperMapper 的 findList 方法,传入学科 ID、处理后的课程 ID、试卷类型、年份、地区以及分页对象(page)这些参数, + // 从数据库中获取符合条件的试卷信息列表,并将结果设置到 Page 对象中(通过 page.setRecords 方法),最后返回这个包含查询结果的 Page 对象, + // 方便后续进行分页相关的业务操作,如在前端展示分页后的试卷列表等。 + return page.setRecords(paperMapper.findList(subjectId, courseId, paperType, year, area, page)); } + /** + * find 方法 + * 实现了 IPaperService 接口中的 find 方法,用于根据试卷的唯一标识符(paperId)从数据库中查询并获取对应的试卷实体信息, + // 直接调用了 PaperMapper 的 selectById 方法,传入试卷 UID 参数(paperId),由 PaperMapper 具体实现与数据库的交互查询操作,返回查询到的试卷实体对象。 + */ @Override public PaperEntity find(String paperId) { return paperMapper.selectById(paperId); } + /** + * findPaperByAreaId 方法 + * 实现了 IPaperService 接口中的 findPaperByAreaId 方法,用于根据地区 ID 和试卷类型获取相应的试卷信息列表, + // 根据传入的 type 参数值判断不同的情况,如果 type 参数值为 "n",则调用本类中的 findHotPaper 方法(传入 areaId 参数)获取热门试卷信息列表并返回; + // 如果不是 "n",则创建一个 Page 对象用于分页查询(设置当前页码为 1,每页显示记录数为 8),然后通过 PaperMapper 的 findPaperByAreaId 方法,传入地区 ID、试卷类型以及分页对象, + // 从数据库中获取符合条件的试卷信息列表并返回,具体的查询逻辑由 PaperMapper 中对应的方法实现来确定。 + */ @Override - public List findPaperByAreaId(String areaId , String type) { - if("n".equals(type)){ + public List findPaperByAreaId(String areaId, String type) { + if ("n".equals(type)) { return this.findHotPaper(areaId); } - Page page = new Page<>(1 , 8); - return paperMapper.findPaperByAreaId(areaId , type , page); + Page page = new Page<>(1, 8); + return paperMapper.findPaperByAreaId(areaId, type, page); } + /** + * getPaperTotal 方法 + * 实现了 IPaperService 接口中的 getPaperTotal 方法,用于获取试卷的总数量, + // 直接调用了 PaperMapper 的 getPaperTotal 方法,由 PaperMapper 具体实现查询数据库中试卷总记录数的逻辑,返回查询到的试卷总数量(类型为 Long)。 + */ @Override public Long getPaperTotal() { return paperMapper.getPaperTotal(); } + /** + * findByCreaterId 方法 + * 实现了 IPaperService 接口中的 findByCreaterId 方法,用于根据创建者的唯一标识符(createrId)从数据库中查询并获取该创建者创建的试卷信息列表, + // 直接调用了 PaperMapper 的 findByCreaterId 方法,传入创建者 ID 参数(createrId),由 PaperMapper 具体实现与数据库的交互查询操作,返回查询到的试卷列表。 + */ @Override public List findByCreaterId(String createrId) { return paperMapper.findByCreaterId(createrId); } - @Transactional(readOnly=false) + /** + * updatePaperName 方法 + * 实现了 IPaperService 接口中的 updatePaperName 方法,用于更新试卷的名称, + // 首先通过 PaperMapper 的 selectById 方法根据传入的试卷 ID(paperId)从数据库中查询获取对应的试卷实体, + // 然后将试卷实体的名称属性(name)设置为传入的新名称(name),最后通过 PaperMapper 的 updateById 方法将更新后的试卷实体信息更新到数据库中,完成试卷名称的修改操作。 + */ + @Transactional(readOnly = false) @Override public void updatePaperName(String paperId, String name) { PaperEntity paper = paperMapper.selectById(paperId); @@ -120,122 +215,15 @@ public class PaperService extends ServiceImpl implemen paperMapper.updateById(paper); } + /** + * deletePaper 方法 + * 实现了 IPaperService 接口中的 deletePaper 方法,用于删除试卷以及与之相关的试题信息,被标记为可读写事务(通过 @Transactional(readOnly = false) 注解指定), + // 因为涉及到对数据库中试卷和试题数据的删除操作,需要保证事务的完整性,要么全部操作成功,要么全部回滚。 + */ @SuppressWarnings("unchecked") @Override public Result deletePaper(String paperId) { + // 首先通过 PaperMapper 的 selectById 方法根据传入的试卷 ID(paperId)从数据库中查询获取对应的试卷实体, PaperEntity paper = paperMapper.selectById(paperId); - if(!ShiroUtils.getUserId().equals(paper.getCreaterId())) { - return Result.result(501, null , "不能删除其他人的试卷!"); - } - paperMapper.deleteById(paperId); - // 删除试题 - questionMapper.delete(Condition.create().eq("paper_id", paperId)); - return Result.result(Result.SUCCESS_CODE, null , "删除成功!"); - } - - @Transactional(readOnly=false) - @Override - public void addPaperQuestionInfo(String paperId, String title, - String name, String type) { - PaperEntity paper = paperMapper.selectById(paperId); - String questionInfo = paper.getQuestionInfo(); - - JSONArray qList = JSONArray.parseArray(questionInfo); - JSONObject entity = new JSONObject(); - entity.put("name", name); - entity.put("title", title); - entity.put("type", type); - entity.put("uid", UUID.randomUUID().toString()); - qList.add(entity); - - paper.setQuestionInfo(qList.toString()); - paperMapper.updateById(paper); - } - - @Transactional(readOnly=false) - @Override - public void updatePaperQuestionInfo(String paperId, String title, - String name, String type , String uid) { - PaperEntity paper = paperMapper.selectById(paperId); - JSONArray qList = JSONArray.parseArray(paper.getQuestionInfo()); - for(int i =0 ; i memberPaperList(String name , String memberId , Page page) { - if(!StringUtils.isEmpty(name)){ - name = "%" + name + "%"; - } - return page.setRecords(paperMapper.queryPageByNameAndCreatorId(name , memberId , page)); - } - - @Transactional(readOnly=false) - @Override - public void addPaper(PaperEntity paper) { - paper.setDownHits(0); - paper.setOpenHits(0); - paper.setQuestionInfo("[]"); - - // 写入seo信息 - paper.setSeoTitle(paper.getName()); - paper.setSeoKeywords(paper.getName()); - paper.setSeoDescription(paper.getName()); - paperMapper.insert(paper); - } - - @Transactional(readOnly=true) - @Override - public List featuredPaper(String type, String subjectId) { - Page page = new Page<>(1,8); - return paperMapper.featuredPaper(type , subjectId , page); - } - - @Transactional(readOnly=true) - @Override - public List findHotPaper(String subjectId, String courseId) { - Page page = new Page<>(1,5); - return paperMapper.findHotPaper(subjectId , courseId , page); - } - - @Transactional(readOnly=false) - @Override - public Result updatePaper(PaperEntity paper) { - PaperEntity entity = paperMapper.selectById(paper.getUid()); - if(!entity.getCreaterId().equals(ShiroUtils.getMember().getUid())) { - return Result.failResult("试卷属于当前用户,不能修改!"); - } - paper.setCreaterId(ShiroUtils.getUserId()); - paperMapper.updateById(paper); - return Result.result(Result.SUCCESS_CODE, paper, "修改成功"); - } - -} + // 通过 ShiroUtils 获取当前用户的 ID,并与试卷的创建者 ID 进行比对,如果当前用户不是试卷的创建者, + // \ No newline at end of file diff --git a/tamguo/src/main/java/com/tamguo/service/impl/QuestionService.java b/tamguo/src/main/java/com/tamguo/service/impl/QuestionService.java index 89f84a4..00c2e5a 100644 --- a/tamguo/src/main/java/com/tamguo/service/impl/QuestionService.java +++ b/tamguo/src/main/java/com/tamguo/service/impl/QuestionService.java @@ -9,6 +9,7 @@ import org.springframework.transaction.annotation.Transactional; import com.baomidou.mybatisplus.plugins.Page; import com.baomidou.mybatisplus.service.impl.ServiceImpl; import com.tamguo.dao.PaperMapper; +import com.baomidou.mybatisplus.plugins.Page; import com.tamguo.dao.QuestionMapper; import com.tamguo.model.PaperEntity; import com.tamguo.model.QuestionEntity; @@ -16,88 +17,176 @@ import com.tamguo.service.IQuestionService; import com.tamguo.util.Result; import com.tamguo.util.ShiroUtils; +// 使用 @Service 注解将这个类标记为Spring中的服务层组件,意味着它主要负责处理与试题(Question)相关的业务逻辑, +// Spring会自动扫描并将其纳入到Spring容器管理中,方便在其他地方进行依赖注入等操作,遵循了Spring的分层架构设计规范。 @Service -public class QuestionService extends ServiceImpl implements IQuestionService{ - +public class QuestionService extends ServiceImpl implements IQuestionService { + + // 通过Spring的依赖注入机制,自动注入 QuestionMapper 接口的实现类实例, + // QuestionMapper 是用于定义与试题实体(QuestionEntity)相关的数据库操作方法的接口,例如查询、插入、更新、删除等操作, + // 在本服务类中会调用它的方法来与数据库进行交互,获取试题相关的数据信息。 @Autowired private QuestionMapper questionMapper; + + // 同样通过依赖注入注入 PaperMapper 接口的实现类实例,PaperMapper 用于定义与试卷实体(PaperEntity)相关的数据库操作方法, + // 在涉及试题与试卷关联相关的业务逻辑(如判断试题所属试卷的创建者等情况)时,会调用它的方法来获取试卷相关信息,辅助试题业务的处理。 @Autowired private PaperMapper paperMapper; + /** + * findByChapterId 方法 + * 实现了 IQuestionService 接口中的 findByChapterId 方法,用于根据章节的唯一标识符(chapterId)查询该章节下的试题信息列表,并进行分页处理, + * 返回包含查询结果的 Page 对象,方便前端进行分页展示等操作。 + */ @Override - public Page findByChapterId(String chapterId , Page page) { - return page.setRecords(questionMapper.findByChapterId(chapterId , page)); + public Page findByChapterId(String chapterId, Page page) { + // 通过 QuestionMapper 的 findByChapterId 方法,传入章节 ID(chapterId)和分页对象(page), + // 从数据库中获取该章节下的试题信息列表,并将结果设置到传入的 Page 对象中(通过 page.setRecords 方法), + // 最后返回这个包含查询结果的 Page 对象,以便后续进行分页相关的业务操作,比如在前端展示该章节下分页后的试题列表。 + return page.setRecords(questionMapper.findByChapterId(chapterId, page)); } - @Transactional(readOnly=true) + /** + * findNormalQuestion 方法 + * 实现了 IQuestionService 接口中的 findNormalQuestion 方法,用于查询普通类型的试题信息,该方法被标记为只读事务(通过 @Transactional(readOnly = true) 注解指定), + // 因为只是进行查询操作,不会对数据库中的数据进行修改,使用只读事务可以提高查询性能,同时避免不必要的数据库锁等问题, + // 直接调用了 QuestionMapper 的 findNormalQuestion 方法,传入试题的唯一标识符(uid),由 QuestionMapper 具体实现与数据库的交互查询操作,返回查询到的试题实体对象。 + */ + @Transactional(readOnly = true) @Override public QuestionEntity findNormalQuestion(String uid) { return questionMapper.findNormalQuestion(uid); } + /** + * findPaperQuestion 方法 + * 实现了 IQuestionService 接口中的 findPaperQuestion 方法,用于获取指定试卷下的所有试题信息列表, + // 直接调用了 QuestionMapper 的 findByPaperId 方法,传入试卷的唯一标识符(paperId),由 QuestionMapper 具体实现与数据库的交互查询操作,返回查询到的该试卷包含的试题列表。 + */ @Override public List findPaperQuestion(String paperId) { return questionMapper.findByPaperId(paperId); } + /** + * select 方法 + * 实现了 IQuestionService 接口中的 select 方法,用于根据试题的唯一标识符(questionId)从数据库中查询并获取对应的试题实体信息, + // 直接调用了 QuestionMapper 的 selectByUid 方法,传入试题 UID 参数(questionId),由 QuestionMapper 具体实现与数据库的交互查询操作,返回查询到的试题实体对象。 + */ @Override public QuestionEntity select(String questionId) { return questionMapper.selectByUid(questionId); } - @Transactional(readOnly=false) + /** + * addQuestion 方法 + * 实现了 IQuestionService 接口中的 addQuestion 方法,用于添加试题信息到数据库中,该方法被标记为可读写事务(通过 @Transactional(readOnly = false) 注解指定), + // 因为涉及到向数据库插入试题数据的操作,需要保证事务的完整性,确保插入操作要么全部成功,要么全部回滚,避免出现数据不一致的情况。 + */ + @Transactional(readOnly = false) @Override public Result addQuestion(QuestionEntity question) { + // 首先通过 PaperMapper 的 selectById 方法,根据传入的试题所属试卷的 ID(question.getPaperId().toString())从数据库中查询获取对应的试卷实体, + // 这是为了后续判断当前用户是否有权限向该试卷添加试题,需要确认试卷的创建者信息。 PaperEntity paper = paperMapper.selectById(question.getPaperId().toString()); - if(!ShiroUtils.getUserId().equals(paper.getCreaterId())) { + // 通过 ShiroUtils 获取当前用户的 ID,并与试卷的创建者 ID 进行比对,如果当前用户不是试卷的创建者, + // 则返回相应的错误提示结果(表示该试卷不属于当前用户,无权添加试题),将错误提示信息封装在 Result 对象中返回给调用者。 + if (!ShiroUtils.getUserId().equals(paper.getCreaterId())) { return Result.result(501, null, "改试卷不属于您!"); } + // 如果当前用户是试卷的创建者,将试题的课程 ID 设置为试卷的课程 ID(确保试题与所属试卷在课程方面的关联性), question.setCourseId(paper.getCourseId()); + // 通过 QuestionMapper 的 insert 方法将试题实体信息插入到数据库中,完成试题添加操作, + // 最后返回添加成功的提示结果,将成功信息封装在 Result 对象中返回给调用者,表示试题添加成功。 questionMapper.insert(question); return Result.result(0, null, "添加成功!"); } - @Transactional(readOnly=true) + /** + * queryQuestionList 方法 + * 实现了 IQuestionService 接口中的 queryQuestionList 方法,用于根据多个条件(试题类型、试题唯一标识符、试题内容、试卷 ID 以及分页信息等)查询试题信息列表,并进行分页处理, + * 返回包含查询结果的 Page 对象,同时在查询过程中加入了权限验证逻辑,确保只有试卷创建者能查询对应试卷下的试题信息。 + */ + @Transactional(readOnly = true) @Override - public Page queryQuestionList(String questionType , String uid , String content , String paperId , - Page page) { - if(!ShiroUtils.getUserId().equals(paperMapper.selectById(paperId).getCreaterId())) { + public Page queryQuestionList(String questionType, String uid, String content, String paperId, + Page page) { + // 通过 PaperMapper 的 selectById 方法,根据传入的试卷 ID(paperId)从数据库中查询获取对应的试卷实体, + // 然后获取试卷的创建者 ID,并与当前用户的 ID(通过 ShiroUtils 获取)进行比对,如果当前用户不是试卷的创建者, + // 则直接返回一个空的记录列表(通过 page.setRecords(null) 设置 Page 对象的记录列表为空),表示无权查询该试卷下的试题信息。 + if (!ShiroUtils.getUserId().equals(paperMapper.selectById(paperId).getCreaterId())) { return page.setRecords(null); } - if(!StringUtils.isEmpty(content)){ + // 判断传入的试题内容(content)是否为空字符串,如果不为空,则在内容前后添加 "%" 符号,用于构建模糊查询的条件, + // 可能在数据库查询中会根据这个模糊条件查找包含指定内容的试题信息,具体取决于数据库查询语句的实现逻辑。 + if (!StringUtils.isEmpty(content)) { content = "%" + content + "%"; } - return page.setRecords(questionMapper.queryQuestionList(questionType, uid , content , paperId , page)); + // 通过 QuestionMapper 的 queryQuestionList 方法,传入试题类型、试题唯一标识符、处理后的试题内容、试卷 ID 以及分页对象(page)这些参数, + // 从数据库中获取符合条件的试题信息列表,并将结果设置到 Page 对象中(通过 page.setRecords 方法), + // 最后返回这个包含查询结果的 Page 对象,方便后续进行分页相关的业务操作,如在前端展示分页后的试题列表等。 + return page.setRecords(questionMapper.queryQuestionList(questionType, uid, content, paperId, page)); } - @Transactional(readOnly=false) + /** + * updateQuestion 方法 + * 实现了 IQuestionService 接口中的 updateQuestion 方法,用于更新试题信息,该方法被标记为可读写事务(通过 @Transactional(readOnly = false) 注解指定), + // 因为涉及到对数据库中试题数据的更新操作,需要保证事务的完整性,确保更新操作要么全部成功,要么全部回滚,避免出现数据不一致的情况。 + */ + @Transactional(readOnly = false) @Override public Result updateQuestion(QuestionEntity question) { + // 首先通过 PaperMapper 的 selectById 方法,根据传入的试题所属试卷的 ID(question.getPaperId().toString())从数据库中查询获取对应的试卷实体, + // 这是为了后续判断当前用户是否有权限更新该试卷下的试题信息,需要确认试卷的创建者信息。 PaperEntity paper = paperMapper.selectById(question.getPaperId().toString()); - if(!ShiroUtils.getUserId().equals(paper.getCreaterId())) { + // 通过 ShiroUtils 获取当前用户的 ID,并与试卷的创建者 ID 进行比对,如果当前用户不是试卷的创建者, + // 则返回相应的错误提示结果(表示该试卷不属于当前用户,无权更新试题),将错误提示信息封装在 Result 对象中返回给调用者。 + if (!ShiroUtils.getUserId().equals(paper.getCreaterId())) { return Result.result(501, null, "改试卷不属于您!"); } + // 如果当前用户是试卷的创建者,通过 QuestionMapper 的 updateById 方法,将传入的更新后的试题实体信息更新到数据库中,完成试题更新操作, + // 最后返回更新成功的提示结果,将成功信息封装在 Result 对象中返回给调用者,表示试题更新成功。 questionMapper.updateById(question); return Result.result(0, null, "修改成功!"); } - @Transactional(readOnly=false) + /** + * delete 方法 + * 实现了 IQuestionService 接口中的 delete 方法,用于删除试题信息,该方法被标记为可读写事务(通过 @Transactional(readOnly = false) 注解指定), + // 因为涉及到对数据库中试题数据的删除操作,需要保证事务的完整性,确保删除操作要么全部成功,要么全部回滚,避免出现数据不一致的情况。 + */ + @Transactional(readOnly = false) @Override public Result delete(String uid) { + // 首先通过 QuestionMapper 的 selectById 方法,根据传入的试题的唯一标识符(uid)从数据库中查询获取对应的试题实体, QuestionEntity question = questionMapper.selectById(uid); + // 接着通过 PaperMapper 的 selectById 方法,根据试题所属试卷的 ID(question.getPaperId().toString())从数据库中查询获取对应的试卷实体, + // 这是为了判断当前用户是否有权限删除该试卷下的试题,需要确认试卷的创建者信息。 PaperEntity paper = paperMapper.selectById(question.getPaperId().toString()); - if(!ShiroUtils.getUserId().equals(paper.getCreaterId())) { + // 通过 ShiroUtils 获取当前用户的 ID,并与试卷的创建者 ID 进行比对,如果当前用户不是试卷的创建者, + // 则返回相应的错误提示结果(表示该试卷不属于当前用户,无权删除试题),将错误提示信息封装在 Result 对象中返回给调用者。 + if (!ShiroUtils.getUserId().equals(paper.getCreaterId())) { return Result.result(501, null, "改试卷不属于您!"); } + // 如果当前用户是试卷的创建者,通过 QuestionMapper 的 deleteById 方法,根据试题的唯一标识符(uid)从数据库中删除对应的试题信息,完成试题删除操作, + // 最后返回删除成功的提示结果,将成功信息封装在 Result 对象中返回给调用者,表示试题删除成功。 questionMapper.deleteById(uid); return Result.result(0, null, "删除成功!"); } - @Transactional(readOnly=true) + /** + * featuredQuestion 方法 + * 实现了 IQuestionService 接口中的 featuredQuestion 方法,用于获取特色试题信息列表,该方法被标记为只读事务(通过 @Transactional(readOnly = true) 注解指定), + // 因为只是进行查询操作,不会对数据库中的数据进行修改,使用只读事务可以提高查询性能,同时避免不必要的数据库锁等问题。 + */ + @Transactional(readOnly = true) @Override public List featuredQuestion(String subjectId, String courseId) { - Page page = new Page<>(1 , 5); - return questionMapper.featuredQuestion(subjectId , courseId , page); + // 创建一个 Page 对象用于分页查询,设置当前页码为 1,每页显示记录数为 5(这里的分页参数可以根据实际业务需求调整), + Page page = new Page<>(1, 5); + // 通过 QuestionMapper 的 featuredQuestion 方法,传入学科 ID(subjectId)和课程 ID(courseId)以及分页对象(page), + // 从数据库中获取符合条件的特色试题信息列表,具体的特色试题判定以及查询逻辑由 QuestionMapper 中对应的方法实现来确定, + // 最后返回查询到的特色试题列表,方便在业务中进行展示、推荐等相关操作。 + return questionMapper.featuredQuestion(subjectId, courseId, page); } - -} +} \ No newline at end of file diff --git a/tamguo/src/main/java/com/tamguo/service/impl/SchoolService.java b/tamguo/src/main/java/com/tamguo/service/impl/SchoolService.java index 2a2a285..8fdf9a6 100644 --- a/tamguo/src/main/java/com/tamguo/service/impl/SchoolService.java +++ b/tamguo/src/main/java/com/tamguo/service/impl/SchoolService.java @@ -19,54 +19,105 @@ import com.tamguo.model.SchoolEntity; import com.tamguo.service.ISchoolService; import com.tamguo.util.TamguoConstant; +// 使用 @Service 注解将这个类标记为Spring中的服务层组件,表明它主要负责处理与学校(School)相关的业务逻辑, +// Spring会自动扫描并将其纳入到Spring容器管理中,方便在其他地方进行依赖注入等操作,遵循Spring的分层架构设计。 @Service public class SchoolService extends ServiceImpl implements ISchoolService { + // 通过Spring的依赖注入机制,自动注入 SchoolMapper 接口的实现类实例, + // SchoolMapper 是用于定义与学校实体(SchoolEntity)相关的数据库操作方法的接口,例如查询、插入、更新等操作, + // 在本服务类中会调用它的方法来与数据库进行交互,获取学校相关的数据信息。 @Autowired private SchoolMapper schoolMapper; + + // 同样通过依赖注入注入 PaperMapper 接口的实现类实例,PaperMapper 用于定义与试卷实体(PaperEntity)相关的数据库操作方法, + // 在涉及学校与试卷关联相关的业务逻辑(如获取某个学校的试卷列表等情况)时,会调用它的方法来获取试卷相关信息,辅助学校业务的处理。 @Autowired private PaperMapper paperMapper; + + // 通过依赖注入注入 CacheService 实例,CacheService 大概率是用于处理缓存相关操作的服务类, + // 例如从缓存中获取数据、将数据存入缓存以及判断缓存是否存在等功能,在学校相关业务中用于缓存不同类型的学校列表数据以及关联的试卷数据等, + // 以减少频繁访问数据库获取学校及相关信息的开销,提高系统性能,优化学校相关业务操作的响应速度。 @Autowired private CacheService cacheService; + /** + * findEliteSchoolPaper 方法 + * 实现了 ISchoolService 接口中的 findEliteSchoolPaper 方法,用于获取名校的试卷相关信息列表,并结合了缓存机制来优化数据获取性能,避免重复查询数据库。 + */ @SuppressWarnings("unchecked") @Override public List findEliteSchoolPaper(String shcoolId) { + // 首先尝试从缓存中获取名为 TamguoConstant.ELITE_SCHOOL_PAPER 的缓存对象,并将其强制转换为 List 类型, + // 期望从缓存中获取到之前存储的名校试卷相关的学校信息列表,如果缓存命中,就可以直接返回这个缓存中的学校列表数据,减少数据库查询操作。 List schoolList = (List) cacheService.getObject(TamguoConstant.ELITE_SCHOOL_PAPER); - // 获取名校试卷 - if(schoolList == null || schoolList.isEmpty()){ + // 判断从缓存中获取到的学校列表是否为 null 或者为空列表(即没有缓存数据或者缓存数据不存在有效的学校信息), + // 如果满足这个条件,就需要从数据库中重新获取名校试卷相关的学校信息列表,并构建学校与试卷的关联信息,然后将其存入缓存中供后续使用。 + if (schoolList == null || schoolList.isEmpty()) { + // 创建一个 Page 对象用于分页查询学校信息,设置当前页码为 1,每页显示的记录数为 3(这里的分页参数可以根据实际业务需求调整), Page page = new Page<>(); page.setCurrent(1); page.setSize(3); - schoolList = schoolMapper.findByAreaId(shcoolId , page); - for(SchoolEntity school : schoolList){ + // 通过注入的 SchoolMapper 的 findByAreaId 方法,传入学校 ID(shcoolId)和分页对象(page), + // 从数据库中获取符合条件的学校信息列表,这里的具体查询逻辑由 SchoolMapper 中对应的方法实现来确定,返回查询到的学校列表赋值给 schoolList 变量。 + schoolList = schoolMapper.findByAreaId(shcoolId, page); + // 遍历获取到的学校列表,对于每个学校(school),需要获取其对应的试卷列表信息并进行关联设置。 + for (SchoolEntity school : schoolList) { + // 创建一个新的 Page 对象用于分页查询试卷信息,设置当前页码为 1,每页显示记录数为 3,同样可根据实际情况调整分页参数。 Page p = new Page<>(); p.setCurrent(1); p.setSize(3); - List paperList = paperMapper.findBySchoolId(school.getUid() , p); + // 通过 PaperMapper 的 findBySchoolId 方法,传入当前学校的唯一标识符(school.getUid())和分页对象(p), + // 从数据库中获取该学校对应的试卷信息列表,具体的查询逻辑由 PaperMapper 中对应方法实现来确定,将查询到的试卷列表赋值给 school 的 paperList 属性, + // 这样就构建了学校与试卷的关联关系,每个学校实体中包含了其对应的试卷列表信息。 + List paperList = paperMapper.findBySchoolId(school.getUid(), p); school.setPaperList(paperList); } - cacheService.setObject(TamguoConstant.ELITE_SCHOOL_PAPER, schoolList , 2 * 60 * 60); + // 将构建好的包含学校与试卷关联信息的学校列表存入缓存中,缓存的键名为 TamguoConstant.ELITE_SCHOOL_PAPER,同时设置了缓存的有效时间为 2 * 60 * 60 秒(即 2 小时), + // 这样在接下来的 2 小时内,如果再次调用 findEliteSchoolPaper 方法,就可以直接从缓存中获取名校试卷相关的学校数据,而不用再次查询数据库了。 + cacheService.setObject(TamguoConstant.ELITE_SCHOOL_PAPER, schoolList, 2 * 60 * 60); } + // 最后返回获取到的学校信息列表,这个列表可能是从缓存中直接获取的,也可能是从数据库中查询后存入缓存再返回的, + // 取决于缓存中是否存在有效数据以及之前的逻辑执行情况,每个学校实体中包含了对应的试卷列表信息,方便在业务中进行展示、操作等处理。 return schoolList; } + /** + * findEliteSchool 方法 + * 实现了 ISchoolService 接口中的 findEliteSchool 方法,用于获取名校信息列表,同样运用了缓存机制来提升数据获取效率,减少数据库访问次数。 + */ @SuppressWarnings("unchecked") @Override public List findEliteSchool() { + // 先从缓存中获取名为 TamguoConstant.ELITE_PAPER 的缓存对象,并强制转换为 List 类型,尝试获取之前缓存的名校信息列表, + // 如果缓存中有数据,就直接返回该缓存中的学校列表数据,避免再次查询数据库。 List schoolList = (List) cacheService.getObject(TamguoConstant.ELITE_PAPER); - if(schoolList == null || schoolList.isEmpty()){ - RowBounds row = new RowBounds(1 , 6); + // 判断从缓存中获取到的学校列表是否为 null 或者为空,如果是这种情况,就需要从数据库中重新获取名校信息列表并缓存起来供后续使用。 + if (schoolList == null || schoolList.isEmpty()) { + // 创建一个 RowBounds 对象用于设置分页参数,这里设置偏移量为 1(表示从第 1 条记录开始获取,通常第 1 条记录的索引是 0,所以实际是获取第 2 条开始的数据,不过具体含义可能根据数据库实现有差异), + // 每页获取的记录数为 6(可根据实际业务需求调整),用于在数据库查询中进行分页控制。 + RowBounds row = new RowBounds(1, 6); + // 通过 SchoolMapper 的 selectPage 方法,传入 RowBounds 对象(row)和 Condition.EMPTY(表示使用默认的查询条件,通常是查询所有符合条件的记录), + // 从数据库中获取符合条件的名校信息列表,具体的名校判定以及查询逻辑由 SchoolMapper 中对应的方法实现来确定,将查询到的名校列表赋值给 schoolList 变量。 schoolList = schoolMapper.selectPage(row, Condition.EMPTY); - cacheService.setObject(TamguoConstant.ELITE_PAPER, schoolList , 2 * 60 * 60); + // 将获取到的名校列表存入缓存中,缓存键名为 TamguoConstant.ELITE_PAPER,缓存有效时间设置为 2 * 60 * 60 秒(2 小时), + // 以便后续在有效期内再次调用 findEliteSchool 方法时,能直接从缓存获取名校数据,提高获取数据的效率。 + cacheService.setObject(TamguoConstant.ELITE_PAPER, schoolList, 2 * 60 * 60); } + // 最后返回获取到的名校信息列表,其来源取决于缓存是否命中以及之前的逻辑执行情况,可能是缓存数据也可能是新从数据库获取后存入缓存再返回的数据。 return schoolList; } - @Transactional(readOnly=true) + /** + * findSchoolByAreaId 方法 + * 实现了 ISchoolService 接口中的 findSchoolByAreaId 方法,用于根据地区 ID(多个地区 ID 以逗号分隔的字符串形式传入,然后进行分割处理)查询该地区的学校信息列表, + // 该方法被标记为只读事务(通过 @Transactional(readOnly = true) 注解指定),因为只是进行查询操作,不会对数据库中的数据进行修改,使用只读事务可以提高查询性能,同时避免不必要的数据库锁等问题。 + */ + @Transactional(readOnly = true) @Override public List findSchoolByAreaId(String areaId) { + // 将传入的以逗号分隔的地区 ID 字符串进行分割,得到一个包含各个地区 ID 的字符串数组, + // 然后通过 Arrays.asList 方法将其转换为 List 类型,以便后续作为参数传递给 SchoolMapper 的查询方法,用于根据地区 ID 列表查询学校信息。 return schoolMapper.findByAreaIds(Arrays.asList(areaId.split(","))); } - -} +} \ No newline at end of file diff --git a/tamguo/src/main/java/com/tamguo/service/impl/SubjectService.java b/tamguo/src/main/java/com/tamguo/service/impl/SubjectService.java index 3659042..3e799fc 100644 --- a/tamguo/src/main/java/com/tamguo/service/impl/SubjectService.java +++ b/tamguo/src/main/java/com/tamguo/service/impl/SubjectService.java @@ -14,76 +14,132 @@ import com.tamguo.model.CourseEntity; import com.tamguo.model.SubjectEntity; import com.tamguo.service.ISubjectService; +// 使用 @Service 注解将这个类标记为Spring中的服务层组件,意味着它主要负责处理与学科(Subject)相关的业务逻辑, +// Spring会自动扫描并将其纳入到Spring容器管理中,方便在其他地方进行依赖注入等操作,遵循Spring的分层架构设计。 @Service -public class SubjectService extends ServiceImpl implements ISubjectService{ +public class SubjectService extends ServiceImpl implements ISubjectService { + // 通过Spring的依赖注入机制,自动注入 SubjectMapper 接口的实现类实例, + // SubjectMapper 是用于定义与学科实体(SubjectEntity)相关的数据库操作方法的接口,比如查询、插入、更新等操作, + // 在本服务类中会调用它的方法来与数据库进行交互,获取学科相关的数据信息。 @Autowired private SubjectMapper subjectMapper; + + // 同样通过依赖注入注入 CourseMapper 接口的实现类实例,CourseMapper 用于定义与课程(CourseEntity)相关的数据库操作方法, + // 在涉及学科与课程关联相关的业务逻辑(如获取某个学科下的课程列表等情况)时,会调用它的方法来获取课程相关信息,辅助学科业务的处理。 @Autowired private CourseMapper courseMapper; + /** + * find 方法 + * 实现了 ISubjectService 接口中的 find 方法,用于根据学科的唯一标识符(uid)查询对应的学科信息,并关联查询该学科下的课程列表信息,然后将课程列表设置到学科实体对象中返回。 + */ @Override public SubjectEntity find(String uid) { + // 通过 SubjectMapper 的 selectById 方法,根据传入的学科 ID(uid)从数据库中查询获取对应的学科实体对象,这一步获取到了基本的学科信息。 SubjectEntity subject = subjectMapper.selectById(uid); + // 通过 CourseMapper 的 findBySubjectId 方法,传入学科 ID(uid),从数据库中查询获取该学科下的课程列表信息,得到一个包含课程实体对象的列表。 List courseList = courseMapper.findBySubjectId(uid); + // 将查询到的课程列表信息设置到学科实体对象的 courseList 属性中,这样返回的学科对象就包含了其关联的课程信息,方便后续在业务中进行统一处理和展示等操作。 subject.setCourseList(courseList); return subject; } + /** + * getCourseTree 方法 + * 用于构建课程的树形结构数据,以 JSONArray 的形式返回,该树形结构反映了学科与课程之间的层级关系,学科作为父节点,其下的课程作为子节点。 + */ @Override public JSONArray getCourseTree() { + // 创建一个空的 JSONArray 对象,用于存储最终构建好的课程树形结构数据,后续会不断向其中添加节点信息来构建完整的树形结构。 JSONArray courseTree = new JSONArray(); - + + // 通过 SubjectMapper 的 selectList 方法,传入 Condition.EMPTY(表示使用默认的查询条件,通常意味着查询所有符合条件的记录,在这里就是所有学科记录), + // 从数据库中获取所有的学科信息列表,得到一个包含所有学科实体对象的列表,用于后续构建树形结构时作为顶层节点(父节点)使用。 @SuppressWarnings("unchecked") List subjectList = subjectMapper.selectList(Condition.EMPTY); - for(int i=0 ; i courseList = courseMapper.findBySubjectId(subject.getUid()); - for(int k=0 ; k subjectList = subjectMapper.selectList(Condition.EMPTY); - for(int i=0 ; i courseList = courseMapper.findBySubjectId(subject.getUid()); - for(int k=0 ; k[] noArgs = null; - getStackTraceMethod = Throwable.class.getMethod("getStackTrace", noArgs); - Class stackTraceElementClass = Class.forName("java.lang.StackTraceElement"); - getClassNameMethod = stackTraceElementClass.getMethod("getClassName", noArgs); - getMethodNameMethod = stackTraceElementClass.getMethod("getMethodName", noArgs); - getFileNameMethod = stackTraceElementClass.getMethod("getFileName", noArgs); - getLineNumberMethod = stackTraceElementClass.getMethod("getLineNumber", noArgs); - } catch (ClassNotFoundException ex) { - LogDebug.debug("will use pre-JDK 1.4 methods to determine location."); - } catch (NoSuchMethodException ex) { - LogDebug.debug("will use pre-JDK 1.4 methods to determine location."); - } - } - - /** - * 获得指定class的StackTraceElement - * - * @param t - * @param fqnOfCallingClass - * - * @return - */ - protected StackTraceElement getRunningStackTrace(Throwable t, String fqnOfCallingClass) { - if (getLineNumberMethod != null) { - try { - Object[] noArgs = null; - Object[] elements = (Object[]) getStackTraceMethod.invoke(t, noArgs); - for (int i = elements.length - 1; i >= 0; i--) { - String thisClass = (String) getClassNameMethod.invoke(elements[i], noArgs); - if (fqnOfCallingClass.equals(thisClass)) { - // 执行class名称 - String className = fqnOfCallingClass; - // 执行方法名称 - String methodName = (String) getMethodNameMethod.invoke(elements[i], noArgs); - // 执行class文件名称 - String fileName = (String) getFileNameMethod.invoke(elements[i], noArgs); - // 执行到行号 - int lineNumber = ((Integer) getLineNumberMethod.invoke(elements[i], noArgs)).intValue(); - return new StackTraceElement(className, methodName, fileName, lineNumber); - } - } - } catch (IllegalAccessException ex) { - LogDebug.debug("failed using JDK 1.4 methods", ex); - } catch (InvocationTargetException ex) { - if (ex.getTargetException() instanceof InterruptedException - || ex.getTargetException() instanceof InterruptedIOException) { - Thread.currentThread().interrupt(); - } - LogDebug.debug("failed using JDK 1.4 methods", ex); - } catch (RuntimeException ex) { - LogDebug.debug("failed using JDK 1.4 methods", ex); - } - } - return this.createDefaultStackTrace(); - } - - /** - * 创建默认StackTraceElement - * - * @return - */ - private StackTraceElement createDefaultStackTrace() { - return new StackTraceElement(this.getClass().getName(), "log", this.getClass().getName(), 0); - } - - @Override - public void info(String msg, String fqnOfCallingClass) { - } - - @Override - public void info(String msg, Throwable t, String fqnOfCallingClass) { - } - - @Override - public void error(String msg, String fqnOfCallingClass) { - } - - @Override - public void error(String msg, Throwable t, String fqnOfCallingClass) { - } - - @Override - public void debug(String msg, String fqnOfCallingClass) { - } - - @Override - public void debug(String msg, Throwable t, String fqnOfCallingClass) { - } - - @Override - public void warning(String msg, String fqnOfCallingClass) { - } - - @Override - public void warning(String msg, Throwable t, String fqnOfCallingClass) { - } - -} + // 通过反射获取的用于获取堆栈跟踪信息的方法对象,对应 Throwable 类中的 getStackTrace 方法, + // 后续会利用这个方法来获取异常发生时的调用栈信息,以便确定日志相关的位置信息等,初始化为 null,在静态代码块中进行初始化赋值。 + private static Method getStackTraceMethod; + // 通过反射获取的用于获取类名的方法对象,对应 java.lang.StackTraceElement 类中的 getClassName 方法, + // 用于从堆栈跟踪元素中提取类名信息,同样初始化为 null,在静态代码块里完成初始化。 + private static Method getClassNameMethod; + // 通过反射获取的用于获取方法名的方法对象,对应 java.lang.StackTraceElement 类中的 getMethodName 方法, + // 用来从堆栈跟踪元素里获取具体执行的方法名称,初始值为 null,在静态块中初始化。 + private static Method getMethodNameMethod; + // 通过反射获取的用于获取文件名的方法对象,对应 java.lang.StackTraceElement 类中的 getFileName 方法, + // 可以从堆栈跟踪元素中获取对应的源文件名信息,初始设置为 null,后续在静态代码块里赋值。 + private static Method getFileNameMethod; + // 通过反射获取的用于获取行号的方法对象,对应 java.lang.StackTraceElement 类中的 getLineNumber 方法, + // 目的是从堆栈跟踪元素中获取代码执行到的具体行号信息,开始时为 null,在静态代码块中进行初始化操作。 + private static Method getLineNumberMethod; + + // 静态代码块,在类加载时执行,主要用于通过反射机制获取一些后续用于处理堆栈跟踪信息相关的方法对象, + // 如果获取过程中出现异常(比如类不存在或者方法不存在等情况),会打印相应的提示信息,表示将使用 JDK 1.4 之前的方式来确定位置信息(虽然这里并没有展示具体是什么替代方式,可能由具体子类去处理或者在其他相关逻辑中体现)。 + static { + try { + // 创建一个空的 Class 数组,表示无参数的情况,用于后续获取方法时指定参数类型,因为这里要获取的几个方法都是无参数的方法。 + Class[] noArgs = null; + // 通过反射获取 Throwable 类的 getStackTrace 方法对象,该方法用于获取异常的堆栈跟踪信息,后续可以基于这个信息进一步提取类、方法、文件、行号等详细信息, + getStackTraceMethod = Throwable.class.getMethod("getStackTrace", noArgs); + // 通过反射获取 java.lang.StackTraceElement 类的 Class 对象,因为后续要获取这个类中的几个方法对象,所以先获取其 Class 类型。 + Class stackTraceElementClass = Class.forName("java.lang.StackTraceElement"); + // 通过反射获取 StackTraceElement 类的 getClassName 方法对象,用于从堆栈跟踪元素中获取类名信息。 + getClassNameMethod = stackTraceElementClass.getMethod("getClassName", noArgs); + // 通过反射获取 StackTraceElement 类的 getMethodName 方法对象,以便从堆栈跟踪元素中获取方法名信息。 + getMethodNameMethod = stackTraceElementClass.getMethod("getMethodName", noArgs); + // 通过反射获取 StackTraceElement 类的 getFileName 方法对象,用于从堆栈跟踪元素中获取文件名信息。 + getFileNameMethod = stackTraceElementClass.getMethod("getFileName", noArgs); + // 通过反射获取 StackTraceElement 类的 getLineNumber 方法对象,使得可以从堆栈跟踪元素中获取行号信息。 + getLineNumberMethod = stackTraceElementClass.getMethod("getLineNumber", noArgs); + } catch (ClassNotFoundException ex) { + // 如果出现 ClassNotFoundException 异常,意味着找不到对应的类(比如这里的 StackTraceElement 类在某些特殊环境下不存在或者加载失败), + // 则打印提示信息,表示将使用 JDK 1.4 之前的方法来确定位置信息,这里只是简单打印提示,具体的替代逻辑可能在其他地方或者由子类去实现。 + LogDebug.debug("will use pre-JDK 1.4 methods to determine location."); + } catch (NoSuchMethodException ex) { + // 如果出现 NoSuchMethodException 异常,说明要获取的某个方法(比如上面获取的几个 StackTraceElement 类中的方法)不存在, + // 同样打印提示信息,表示将使用 JDK 1.4 之前的方法来确定位置信息,后续具体怎么处理依赖于具体的业务逻辑或者子类实现。 + LogDebug.debug("will use pre-JDK 1.4 methods to determine location."); + } + } + + /** + * 获得指定class的StackTraceElement + * 该方法用于根据传入的异常对象(t)以及调用类的全限定名(fqnOfCallingClass),从异常的堆栈跟踪信息中查找并提取出与指定调用类相关的堆栈跟踪元素(StackTraceElement), + * 如果成功获取到相关信息,则基于反射调用相应方法来构建并返回一个包含类名、方法名、文件名和行号的 StackTraceElement 对象; + * 如果在获取过程中出现反射相关的异常(比如非法访问、调用目标异常等情况),会进行相应的错误处理(比如打印调试信息、处理中断相关情况等),最后返回一个默认的 StackTraceElement 对象。 + * + * @param t 异常对象,通常是在代码执行出现异常时传递进来,用于获取其相关的堆栈跟踪信息。 + * @param fqnOfCallingClass 调用类的全限定名,用于在堆栈跟踪信息中定位到特定类相关的信息,通过比对类名来找到对应的堆栈跟踪元素。 + * + * @return 返回一个 StackTraceElement 对象,包含了与指定调用类相关的类名、方法名、文件名和行号等信息,如果获取失败则返回默认构建的 StackTraceElement 对象。 + */ + protected StackTraceElement getRunningStackTrace(Throwable t, String fqnOfCallingClass) { + if (getLineNumberMethod!= null) { + try { + // 创建一个空的 Object 数组,表示无参数的情况,用于后续通过反射调用方法时作为参数传递,因为这里要调用的几个方法都是无参数的方法。 + Object[] noArgs = null; + // 通过反射调用异常对象(t)的 getStackTrace 方法,获取其堆栈跟踪信息,返回的是一个 Object 数组(实际上是 StackTraceElement 数组,但通过反射获取时以 Object 数组形式返回), + Object[] elements = (Object[]) getStackTraceMethod.invoke(t, noArgs); + // 从后往前遍历堆栈跟踪元素数组,因为通常最新的调用信息在数组的末尾,通过倒序遍历可以更快地找到与指定调用类相关的元素。 + for (int i = elements.length - 1; i >= 0; i--) { + // 通过反射调用当前堆栈跟踪元素(elements[i])的 getClassName 方法,获取其类名信息,并转换为 String 类型,用于与传入的调用类全限定名进行比对。 + String thisClass = (String) getClassNameMethod.invoke(elements[i], noArgs); + if (fqnOfCallingClass.equals(thisClass)) { + // 如果找到了与传入的调用类全限定名匹配的堆栈跟踪元素,则获取其相关信息来构建一个 StackTraceElement 对象并返回。 + + // 获取类名,这里直接使用传入的调用类全限定名(fqnOfCallingClass)作为类名,也可以根据实际情况进行进一步处理,比如提取简单类名等,不过这里直接使用了传入的全限定名。 + String className = fqnOfCallingClass; + // 通过反射调用当前堆栈跟踪元素(elements[i])的 getMethodName 方法,获取其方法名信息,并转换为 String 类型,用于设置到要返回的 StackTraceElement 对象中。 + String methodName = (String) getMethodNameMethod.invoke(elements[i], noArgs); + // 通过反射调用当前堆栈跟踪元素(elements[i])的 getFileName 方法,获取其文件名信息,并转换为 String 类型,用于设置到要返回的 StackTraceElement 对象中。 + String fileName = (String) getFileNameMethod.invoke(elements[i], noArgs); + // 通过反射调用当前堆栈跟踪元素(elements[i])的 getLineNumber 方法,获取其行号信息,先转换为 Integer 类型,再获取其 int 值,用于设置到要返回的 StackTraceElement 对象中。 + int lineNumber = ((Integer) getLineNumberMethod.invoke(elements[i], noArgs)).intValue(); + return new StackTraceElement(className, methodName, fileName, lineNumber); + } + } + } catch (IllegalAccessException ex) { + // 如果出现 IllegalAccessException 异常,说明在通过反射调用方法时发生了非法访问的情况(比如方法不可访问等原因), + // 则打印调试信息,表示使用 JDK 1.4 相关方法失败,并将异常信息传递进去,方便后续排查问题,不过这里只是简单打印调试信息,具体的处理逻辑可能可以进一步完善。 + LogDebug.debug("failed using JDK 1.4 methods", ex); + } catch (InvocationTargetException ex) { + // 如果出现 InvocationTargetException 异常,说明在通过反射调用方法时,被调用方法内部抛出了异常,需要进一步判断这个内部抛出的异常类型。 + if (ex.getTargetException() instanceof InterruptedException + || ex.getTargetException() instanceof InterruptedIOException) { + // 如果内部抛出的异常是 InterruptedException 或者 InterruptedIOException,表示线程被中断相关的情况, + // 则重新设置当前线程的中断状态,以便让调用者知道线程被中断了,后续可以根据这个中断状态进行相应的处理,符合 Java 中处理线程中断的规范。 + Thread.currentThread().interrupt(); + } + // 无论内部抛出的异常具体是什么类型(除了上面特殊处理的中断相关异常外),都打印调试信息,表示使用 JDK 1.4 相关方法失败,并将异常信息传递进去,方便后续排查问题。 + LogDebug.debug("failed using JDK 1.4 methods", ex); + } catch (RuntimeException ex) { + // 如果出现 RuntimeException 异常,同样打印调试信息,表示使用 JDK 1.4 相关方法失败,并将异常信息传递进去,方便后续排查问题, + // 这里统一处理了各种运行时异常情况,保证方法在出现异常时能进行一定的错误提示,避免程序因为未处理的异常而崩溃。 + LogDebug.debug("failed using JDK 1.4 methods", ex); + } + } + // 如果在获取指定的 StackTraceElement 过程中出现了各种问题(比如反射方法获取失败或者在提取信息时出现异常等情况),则调用 createDefaultStackTrace 方法创建并返回一个默认的 StackTraceElement 对象。 + return this.createDefaultStackTrace(); + } + + /** + * 创建默认StackTraceElement + * 该方法用于创建一个默认的 StackTraceElement 对象,当无法从异常的堆栈跟踪信息中正确获取到与指定调用类相关的堆栈跟踪元素时使用, + * 返回的 StackTraceElement 对象的类名设置为当前类(this.getClass().getName()),方法名设置为 "log",文件名也设置为当前类名(this.getClass().getName()),行号设置为 0, + * 作为一种默认的占位或者表示错误情况的堆栈跟踪元素返回,具体使用场景取决于业务逻辑对堆栈跟踪信息的处理需求。 + * + * @return 返回一个默认构建的 StackTraceElement 对象,包含了默认设置的类名、方法名、文件名和行号信息。 + */ + private StackTraceElement createDefaultStackTrace() { + return new StackTraceElement(this.getClass().getName(), "log", this.getClass().getName(), 0); + } + + // 以下是实现 LogHandler 接口的各个日志处理方法,在当前抽象类中这些方法体为空,因为具体的日志处理逻辑(比如将日志信息输出到哪里、如何格式化等)需要由具体的子类根据不同的业务需求去实现, + // 这里只是定义了方法签名,遵循了接口的规范,保证了抽象类作为日志处理的基础框架,子类可以针对性地重写这些方法来完成实际的日志处理操作。 + + @Override + public void info(String msg, String fqnOfCallingClass) { + } + + @Override + public void info(String msg, Throwable t, String fqnOfCallingClass) { + } + + @Override + public void error(String msg, String fqnOfCallingClass) { + } + + @Override + public void error(String msg, Throwable t, String fqnOfCallingClass) { + } + + @Override + public void debug(String msg, String fqnOfCallingClass) { + } + + @Override + public void debug(String msg, Throwable t, String fqnOfCallingClass) { + } + + @Override + public void warning(String msg, String fqnOfCallingClass) { + } + + @Override + public void warning(String msg, Throwable t, String fqnOfCallingClass) { + } +} \ No newline at end of file diff --git a/tamguo/src/main/java/com/tamguo/util/DateUtils.java b/tamguo/src/main/java/com/tamguo/util/DateUtils.java index 0c65c5f..219ebc9 100644 --- a/tamguo/src/main/java/com/tamguo/util/DateUtils.java +++ b/tamguo/src/main/java/com/tamguo/util/DateUtils.java @@ -5,51 +5,86 @@ import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; +// 这个类提供了一系列与日期处理相关的实用方法,用于日期的格式化、解析、日期差值计算以及日期增减等操作,方便在项目中对日期进行各种常见的处理。 public class DateUtils { + + // 定义默认的日期格式字符串,格式为 "yyyy-MM-dd",用于在一些方法中作为默认的日期格式化样式,方便统一处理日期的显示格式。 public static final String DEFAULT_PATTERN = "yyyy-MM-dd"; + /** + * 获取相对于当前日期偏移指定天数后的日期字符串 + * 根据传入的天数参数(day),在当前日期基础上进行偏移,然后按照指定的日期格式(pattern)将偏移后的日期格式化为字符串返回。 + * 如果未传入日期格式参数,则使用默认格式(DEFAULT_PATTERN)进行格式化。 + * + * @param day 要偏移的天数,正数表示往后的日期,负数表示往前的日期。 + * @param pattern 日期格式字符串,用于指定返回日期的格式化样式,例如 "yyyy-MM-dd HH:mm:ss" 等。 + * @return 按照指定格式格式化后的日期字符串,表示偏移指定天数后的日期。 + */ public static String getOneDayFromNow(int day, String pattern) { + // 获取一个 Calendar 实例,它用于对日期进行各种操作,比如日期的增减、获取日期的各个字段等,这里获取的是当前系统时间对应的 Calendar 实例。 Calendar cal = Calendar.getInstance(); + // 将 Calendar 的时间设置为当前日期(通过传入一个 Date 对象表示当前时间,这里直接使用 new Date() 获取当前时间),以便后续在此基础上进行日期偏移操作。 cal.setTime(new Date()); + // 在当前日期基础上增加或减少指定的天数,通过调用 add 方法并指定 Calendar.DAY_OF_MONTH 字段以及要偏移的天数(day)来实现日期的偏移操作。 cal.add(Calendar.DAY_OF_MONTH, day); + // 创建一个 SimpleDateFormat 对象,用于按照指定的格式(pattern)对日期进行格式化操作,将日期转换为字符串形式。 SimpleDateFormat sdf = new SimpleDateFormat(pattern); + // 使用 SimpleDateFormat 的 format 方法将偏移后的日期(cal.getTime() 返回的是一个 Date 对象,表示偏移后的日期时间)格式化为字符串,并返回该格式化后的日期字符串。 return sdf.format(cal.getTime()); } + /** + * 获取相对于当前日期偏移指定天数后的日期字符串(使用默认格式) + * 该方法重载了 getOneDayFromNow 方法,当只传入要偏移的天数(day)参数时,会调用另一个 getOneDayFromNow 方法,并使用默认的日期格式(DEFAULT_PATTERN)进行日期格式化,返回相应的日期字符串。 + * + * @param day 要偏移的天数,正数表示往后的日期,负数表示往前的日期。 + * @return 按照默认格式("yyyy-MM-dd")格式化后的日期字符串,表示偏移指定天数后的日期。 + */ public static String getOneDayFromNow(int day) { return DateUtils.getOneDayFromNow(day, DEFAULT_PATTERN); } /** * 计算两个日期之间相差的天数 - * - * @param smdate - * 较小的时间 - * @param bdate - * 较大的时间 - * @return 相差天数 - * @throws ParseException - * @throws java.text.ParseException + * 通过将两个日期先按照指定格式("yyyy-MM-dd")进行格式化并解析为 Date 对象,然后获取它们对应的时间戳(以毫秒为单位), + * 计算时间戳差值并转换为天数,最后返回相差的天数。需要注意的是,传入的两个日期参数(smdate 和 bdate)需要确保一个是较小时间,一个是较大时间,否则结果可能不符合预期。 + * + * @param smdate 较小的时间,一般是较早的日期,类型为 Date。 + * @param bdate 较大的时间,通常是较晚的日期,类型为 Date。 + * @return 两个日期之间相差的天数,返回值为整数类型。 + * @throws ParseException 如果在日期格式化或解析过程中出现错误,会抛出此异常,调用者需要进行相应的异常处理。 */ - public static int daysBetween(Date smdate, Date bdate) - throws ParseException { + public static int daysBetween(Date smdate, Date bdate) throws ParseException { + // 创建一个 SimpleDateFormat 对象,指定日期格式为 "yyyy-MM-dd",用于统一将传入的日期格式化为该格式后再进行后续的时间戳计算等操作,确保日期格式的一致性。 SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); + // 先将传入的较小日期(smdate)按照指定格式进行格式化,再解析为 Date 对象,这样做的目的是去除日期中的时间部分(如果有的话),只保留日期部分,保证计算天数的准确性。 smdate = sdf.parse(sdf.format(smdate)); + // 同样地,对较大日期(bdate)进行格式化和解析操作,去除时间部分,只保留日期部分,以便后续基于日期进行天数差值的计算。 bdate = sdf.parse(sdf.format(bdate)); + // 获取一个 Calendar 实例,用于获取日期对应的时间戳等操作,这里以较小日期(smdate)为基础进行设置。 Calendar cal = Calendar.getInstance(); cal.setTime(smdate); + // 获取较小日期对应的时间戳(以毫秒为单位),通过 Calendar 的getTimeInMillis 方法获取,表示从 1970 年 1 月 1 日 00:00:00 UTC 到该日期时间的毫秒数。 long time1 = cal.getTimeInMillis(); + // 重新设置 Calendar 的时间为较大日期(bdate),以便获取其对应的时间戳。 cal.setTime(bdate); + // 获取较大日期对应的时间戳(以毫秒为单位)。 long time2 = cal.getTimeInMillis(); + // 计算两个日期时间戳的差值(以毫秒为单位),然后除以一天对应的毫秒数(1000 * 3600 * 24,即 1000 毫秒/秒 * 3600 秒/小时 * 24 小时/天),得到相差的天数,这里差值是一个 long 类型。 long between_days = (time2 - time1) / (1000 * 3600 * 24); + // 将计算得到的相差天数(long 类型)转换为整数类型并返回,通过将 long 值转换为 String 再解析为 Integer 的方式实现,虽然这种方式略显繁琐,但能确保类型转换的正确性。 return Integer.parseInt(String.valueOf(between_days)); } + // main 方法,用于简单测试 daysBetween 方法,不过这里传入的参数都是 null,实际运行时会抛出 NullPointerException, + // 正常使用时应该传入有效的 Date 对象参数进行测试或者在其他合适的地方调用 daysBetween 方法并传入正确的日期参数来计算日期差值。 public static void main(String[] args) throws ParseException { System.out.println(daysBetween(null, null)); } + // 定义一系列日期格式的常量字符串,方便在不同的日期格式化场景中直接使用,涵盖了英文简写、英文全称、精确到毫秒的完整时间以及对应的中文简写、中文全称、精确到毫秒的完整中文时间等多种格式。 + /** * 英文简写(默认)如:2010-12-01 */ @@ -77,6 +112,7 @@ public class DateUtils { /** * 获得默认的 date pattern + * 返回默认的日期格式字符串,当前设置为 FORMAT_LONG(即 "yyyy-MM-dd HH:mm:ss"),用于在一些需要使用默认格式的方法中获取默认的格式化样式。 */ public static String getDatePattern() { return FORMAT_LONG; @@ -84,8 +120,9 @@ public class DateUtils { /** * 根据预设格式返回当前日期 - * - * @return + * 使用默认的日期格式(通过 getDatePattern 方法获取,当前为 "yyyy-MM-dd HH:mm:ss")对当前日期(new Date() 获取当前时间)进行格式化,并返回格式化后的日期字符串。 + * + * @return 当前日期按照默认格式格式化后的字符串。 */ public static String getNow() { return format(new Date()); @@ -93,9 +130,10 @@ public class DateUtils { /** * 根据用户格式返回当前日期 - * - * @param format - * @return + * 根据传入的用户指定的日期格式(format)对当前日期(new Date() 获取当前时间)进行格式化,并返回格式化后的日期字符串,方便按照用户需求的格式展示当前日期。 + * + * @param format 用户指定的日期格式字符串,例如 "yyyy-MM-dd"、"yyyy年MM月dd日" 等。 + * @return 当前日期按照用户指定格式格式化后的字符串。 */ public static String getNow(String format) { return format(new Date(), format); @@ -103,9 +141,11 @@ public class DateUtils { /** * 使用预设格式格式化日期 - * - * @param date - * @return + * 使用默认的日期格式(通过 getDatePattern 方法获取,当前为 "yyyy-MM-dd HH:mm:ss")对传入的日期(date)进行格式化,并返回格式化后的日期字符串, + * 如果传入的日期为 null,则返回空字符串。 + * + * @param date 要格式化的日期对象,类型为 Date,可以为 null。 + * @return 按照默认格式格式化后的日期字符串,如果 date 为 null,则返回空字符串。 */ public static String format(Date date) { return format(date, getDatePattern()); @@ -113,17 +153,19 @@ public class DateUtils { /** * 使用用户格式格式化日期 - * - * @param date - * 日期 - * @param pattern - * 日期格式 - * @return + * 根据传入的用户指定的日期格式(pattern)对传入的日期(date)进行格式化操作,将日期转换为对应的字符串形式, + * 如果传入的日期为 null,则返回空字符串,确保方法的健壮性,避免出现空指针异常等情况。 + * + * @param date 要格式化的日期对象,类型为 Date,可以为 null。 + * @param pattern 用户指定的日期格式字符串,用于控制日期的格式化样式。 + * @return 按照用户指定格式格式化后的日期字符串,如果 date 为 null,则返回空字符串。 */ public static String format(Date date, String pattern) { String returnValue = ""; - if (date != null) { + if (date!= null) { + // 创建一个 SimpleDateFormat 对象,按照用户指定的格式(pattern)进行初始化,用于对日期进行格式化操作。 SimpleDateFormat df = new SimpleDateFormat(pattern); + // 使用 SimpleDateFormat 的 format 方法将日期(date)格式化为字符串,并赋值给 returnValue,以便后续返回。 returnValue = df.format(date); } return (returnValue); @@ -131,12 +173,12 @@ public class DateUtils { /** * 使用用户格式格式化时间戳 - * - * @param timestamp - * 时间戳 - * @param pattern - * 日期格式 - * @return + * 将传入的时间戳字符串(timestamp)解析为对应的日期对象(通过将时间戳转换为 long 类型后作为参数构造 Date 对象), + * 然后按照用户指定的日期格式(pattern)对该日期对象进行格式化操作,返回格式化后的日期字符串,方便将时间戳按照指定格式展示为日期时间形式。 + * + * @param timestamp 时间戳字符串,一般是表示从 1970 年 1 月 1 日 00:00:00 UTC 到某个时间点的毫秒数对应的字符串形式,例如 "1612345678901" 等。 + * @param pattern 用户指定的日期格式字符串,用于控制格式化后的日期时间样式。 + * @return 按照用户指定格式格式化后的日期字符串,将时间戳对应的日期时间按照指定格式进行展示。 */ public static String format(String timestamp, String pattern) { SimpleDateFormat sdf = new SimpleDateFormat(pattern); @@ -145,10 +187,11 @@ public class DateUtils { /** * 使用预设格式提取字符串日期 - * - * @param strDate - * 日期字符串 - * @return + * 将传入的日期字符串(strDate)按照默认的日期格式(通过 getDatePattern 方法获取,当前为 "yyyy-MM-dd HH:mm:ss")进行解析, + * 尝试将其转换为对应的 Date 对象并返回,如果解析过程中出现 ParseException 异常,则打印异常堆栈信息,并返回 null,表示解析失败。 + * + * @param strDate 要解析的日期字符串,需要符合默认的日期格式要求,否则可能解析失败。 + * @return 解析后的 Date 对象,如果解析失败则返回 null。 */ public static Date parse(String strDate) { return parse(strDate, getDatePattern()); @@ -156,12 +199,12 @@ public class DateUtils { /** * 使用用户格式提取字符串日期 - * - * @param strDate - * 日期字符串 - * @param pattern - * 日期格式 - * @return + * 根据传入的用户指定的日期格式(pattern)对传入的日期字符串(strDate)进行解析操作,尝试将其转换为对应的 Date 对象并返回, + * 如果在解析过程中出现 ParseException 异常,则打印异常堆栈信息,并返回 null,表示解析失败,调用者可以根据返回值判断解析是否成功并进行相应处理。 + * + * @param strDate 要解析的日期字符串,需要符合用户指定的日期格式要求,否则可能解析失败。 + * @param pattern 用户指定的日期格式字符串,用于控制解析的规则。 + * @return 解析后的 Date 对象,如果解析失败则返回 null。 */ public static Date parse(String strDate, String pattern) { SimpleDateFormat df = new SimpleDateFormat(pattern); @@ -175,12 +218,11 @@ public class DateUtils { /** * 在日期上增加数个整月 - * - * @param date - * 日期 - * @param n - * 要增加的月数 - * @return + * 通过 Calendar 实例对传入的日期(date)进行操作,在原日期基础上增加指定的月数(n),然后返回增加月数后的日期对象,方便进行日期的月份偏移操作。 + * + * @param date 要进行月份增加操作的日期对象,类型为 Date。 + * @param n 要增加的月数,正数表示往后增加的月数,负数表示往前减少的月数。 + * @return 增加指定月数后的日期对象。 */ public static Date addMonth(Date date, int n) { Calendar cal = Calendar.getInstance(); @@ -191,12 +233,11 @@ public class DateUtils { /** * 在日期上增加天数 - * - * @param date - * 日期 - * @param n - * 要增加的天数 - * @return + * 利用 Calendar 实例对传入的日期(date)进行操作,在原日期基础上增加指定的天数(n),然后返回增加天数后的日期对象,实现日期的天数偏移功能。 + * + * @param date 要进行天数增加操作的日期对象,类型为 Date。 + * @param n 要增加的天数,正数表示往后增加的天数,负数表示往前减少的天数。 + * @return 增加指定天数后的日期对象。 */ public static Date addDay(Date date, int n) { Calendar cal = Calendar.getInstance(); @@ -207,6 +248,8 @@ public class DateUtils { /** * 获取时间戳 + * 使用指定的日期格式(FORMAT_FULL,即 "yyyy-MM-dd HH:mm:ss.S")对当前时间(通过 Calendar.getInstance 获取当前系统时间对应的 Calendar 实例,再获取其对应的 Date 对象)进行格式化, + * 返回格式化后的日期时间字符串,该字符串实际上也可以看作是一种包含了精确到毫秒的时间戳信息的表示形式,方便在一些场景下记录精确的时间点。 */ public static String getTimeString() { SimpleDateFormat df = new SimpleDateFormat(FORMAT_FULL); @@ -216,59 +259,17 @@ public class DateUtils { /** * 获取日期年份 - * - * @param date - * 日期 - * @return + * 先将传入的日期(date)按照默认格式(通过 format 方法使用默认格式进行格式化)进行格式化,然后提取格式化后的字符串的前 4 个字符, + * 即获取表示年份的部分并返回,用于获取日期对应的年份信息,不过这种方式依赖于默认格式中年份处于字符串开头且长度为 4 位的前提条件。 + * + * @param date 要获取年份信息的日期对象,类型为 Date。 + * @return 日期对应的年份字符串,例如 "2024" 等。 */ public static String getYear(Date date) { return format(date).substring(0, 4); } - /** - * 按默认格式的字符串距离今天的天数 - * - * @param date - * 日期字符串 - * @return - */ - public static int countDays(String date) { - long t = Calendar.getInstance().getTime().getTime(); - Calendar c = Calendar.getInstance(); - c.setTime(parse(date)); - long t1 = c.getTime().getTime(); - return (int) (t / 1000 - t1 / 1000) / 3600 / 24; - } - - /** - * 按用户格式字符串距离今天的天数 - * - * @param date - * 日期字符串 - * @param format - * 日期格式 - * @return - */ - public static int countDays(String date, String format) { - long t = Calendar.getInstance().getTime().getTime(); - Calendar c = Calendar.getInstance(); - c.setTime(parse(date, format)); - long t1 = c.getTime().getTime(); - return (int) (t / 1000 - t1 / 1000) / 3600 / 24; - } - - public static String timeFormat(Date date, String format, Boolean flag, int beforeDay, int nowDay) { - if(date == null) { - date = new Date(); - } - Calendar cal = Calendar.getInstance(); - cal.setTime(date); - if(flag) { - cal.add(Calendar.DATE,-30); - } else { - cal.add(Calendar.DATE,0); - } - SimpleDateFormat sdf = new SimpleDateFormat(format); - return sdf.format(cal.getTime()); - } -} +/** + * 按默认格式的字符串距离今天的天数 + * 通过获取当前日期的时间戳(以毫秒为单位)以及将传入的日期字符串(date,需要符合默认格式要求)解析为日期对象后获取其时间戳, + * 计算两个时间戳的差值(转换为秒后再换算为天数) \ No newline at end of file diff --git a/tamguo/src/main/java/com/tamguo/util/IdGen.java b/tamguo/src/main/java/com/tamguo/util/IdGen.java index 36915e4..7ce214e 100644 --- a/tamguo/src/main/java/com/tamguo/util/IdGen.java +++ b/tamguo/src/main/java/com/tamguo/util/IdGen.java @@ -1,56 +1,86 @@ package com.tamguo.util; -public class IdGen -{ +// IdGen类用于生成唯一标识符(ID),很可能基于某种分布式唯一ID生成算法(从代码结构推测可能类似雪花算法的思路), +// 它可以根据配置的参数(如工作节点ID、数据中心ID等)以及一些规则来生成具有唯一性的ID,并且支持添加自定义后缀以及指定后缀位置等功能。 +public class IdGen { + + // 工作节点ID,用于在分布式环境中区分不同的工作节点,不同的工作节点生成的ID应该具有唯一性,通过位运算等规则来限定其取值范围。 private long workerId; + // 数据中心ID,用于区分不同的数据中心,同样在分布式系统架构里参与ID生成的唯一性保证,也有相应的取值范围限制。 private long datacenterId; + // 序列号,用于在同一毫秒内对生成的ID进行区分,在同一时间(同一毫秒)内,不同的序列号保证生成的ID各不相同,初始值为0。 private long sequence = 0L; + // 起始时间戳(单位:毫秒),通常是一个固定的历史时间点,作为ID生成算法中的时间基线,用于计算时间戳差值等操作,以保证生成的ID在时间维度上的唯一性和有序性。 private long twepoch = 1288834974657L; + // 用于表示 workerId 所占的位数,通过这些位数设定来确定 workerId 的取值范围,当前设置为5位。 private long workerIdBits = 5L; + // 表示 datacenterId 所占的位数,以此限定 datacenterId 的取值范围,同样设置为5位。 private long datacenterIdBits = 5L; + // 根据 workerIdBits 计算出的 workerId 的最大允许值,通过位运算得出,确保 workerId 在合理的取值范围内,避免超出导致ID生成出现问题。 private long maxWorkerId = -1L ^ (-1L << workerIdBits); + // 依据 datacenterIdBits 计算得到的 datacenterId 的最大取值,通过位运算来限定其范围,保证 datacenterId 的合法性。 private long maxDatacenterId = -1L ^ (-1L << datacenterIdBits); + // 序列号所占的位数,决定了在同一毫秒内可生成的不同ID的数量,这里设置为12位。 private long sequenceBits = 12L; + // 在生成ID时,用于将 sequence(序列号)左移的位数,其值等于 sequenceBits,用于在ID的二进制表示中为 sequence 预留合适的位置。 private long workerIdShift = sequenceBits; + // 在生成ID时,用于将 datacenterId 左移的位数,计算方式是 sequenceBits 与 workerIdBits 之和,确定 datacenterId 在ID二进制表示中的位置。 private long datacenterIdShift = sequenceBits + workerIdBits; + // 在生成ID时,用于将时间戳(timestamp)左移的位数,通过 sequenceBits、workerIdBits 和 datacenterIdBits 三者相加得出,为时间戳在ID二进制表示中预留正确的位置。 private long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits; + // 序列号的掩码,通过位运算生成,用于在同一毫秒内对 sequence 进行循环计数时的边界判断,保证序列号在规定的位数范围内循环使用。 private long sequenceMask = -1L ^ (-1L << sequenceBits); + // 记录上一次生成ID时的时间戳(单位:毫秒),用于和当前时间戳对比,判断是否在同一毫秒内以及处理时间回拨等情况,初始值为 -1L。 private long lastTimestamp = -1L; + // 自定义的后缀字符串,可用于给生成的ID添加额外的标识信息,方便在业务中根据后缀进行分类、识别等操作,初始值为 null。 private String suffix; + // 布尔值,用于指示后缀添加的位置,true 表示添加在ID的前缀位置,false 表示添加在ID的后缀位置,初始值为 false。 private boolean flag; + // 内部静态类,用于实现单例模式,保证整个应用程序中只有一个 IdGen 实例(如果使用默认无参构造函数获取实例的情况), + // 通过类加载机制保证线程安全地创建和获取这个唯一实例,避免多次实例化造成的资源浪费以及保证ID生成规则的一致性。 private static class IdGenHolder { + // 创建并持有一个静态的最终 IdGen 实例,在类加载时就会初始化这个实例,并且只会初始化一次,后续通过 get 方法获取的都是这个唯一实例。 private static final IdGen instance = new IdGen(); } - public static IdGen get(){ + // 静态方法,用于获取单例的 IdGen 实例(使用默认配置的情况),外部类可以通过这个方法方便地获取到用于生成ID的唯一实例,遵循单例模式的获取实例方式。 + public static IdGen get() { return IdGenHolder.instance; } /** * 获取字符串拼接id - * @param suffix 字符串 - * @param flag true:前缀 false:后缀 - * @return + * 该静态重载方法用于获取一个带有自定义后缀的 IdGen 实例,根据传入的后缀字符串(suffix)以及后缀添加位置标志(flag)来决定如何生成带有后缀的ID, + * 如果后缀字符串为空(null 或者去除空格后长度为0),则返回默认的单例实例(即不带后缀的情况),否则创建一个新的 IdGen 实例并传入后缀和标志信息,用于后续生成带后缀的ID。 + * + * @param suffix 字符串,要添加到生成ID上的后缀内容,如果为空则不添加后缀,使用默认的ID生成方式。 + * @param flag true表示将后缀添加在ID的前缀位置,false表示添加在ID的后缀位置,用于控制后缀在生成ID中的位置。 + * @return 返回一个 IdGen 实例,如果后缀为空则返回默认单例实例,否则返回带有指定后缀及后缀位置配置的新实例。 */ - public static IdGen get(String suffix,boolean flag){ - if(suffix == null || suffix.trim().length() == 0) + public static IdGen get(String suffix, boolean flag) { + if (suffix == null || suffix.trim().length() == 0) return IdGenHolder.instance; - else{ - return new IdGen(suffix,flag); + else { + return new IdGen(suffix, flag); } } - + // 默认的无参构造函数,调用另一个带有两个参数(均为0L)的构造函数来初始化对象,给 workerId 和 datacenterId 都赋予初始值0,适用于一些简单场景或者后续有默认配置的情况。 public IdGen() { this(0L, 0L); } - public IdGen(String suffix,boolean flag){ + // 构造函数,用于创建一个带有自定义后缀和后缀位置标志的 IdGen 实例,接收后缀字符串(suffix)和后缀位置标志(flag)作为参数, + // 将传入的参数赋值给相应的成员变量,以便后续在生成ID时根据这些配置来添加后缀。 + public IdGen(String suffix, boolean flag) { this.suffix = suffix; this.flag = flag; } + // 构造函数,用于创建一个 IdGen 实例并传入工作节点ID(workerId)和数据中心ID(datacenterId)参数, + // 在创建实例时会对传入的 workerId 和 datacenterId 进行合法性检查,确保它们在预先设定的取值范围内(通过与最大允许值比较以及判断是否小于0来验证), + // 如果超出范围则抛出 IllegalArgumentException 异常,保证ID生成参数的有效性,避免生成不符合规则的ID。 public IdGen(long workerId, long datacenterId) { if (workerId > maxWorkerId || workerId < 0) { throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId)); @@ -62,28 +92,57 @@ public class IdGen this.datacenterId = datacenterId; } + /** + * 生成下一个唯一ID + * 这是整个类的核心方法,用于生成下一个唯一的ID,其实现过程遵循了类似分布式ID生成算法(如雪花算法)的思路,综合考虑了时间戳、工作节点ID、数据中心ID以及序列号等因素, + * 在生成过程中会处理时间戳回拨等异常情况,确保生成的ID在分布式环境下的唯一性、有序性以及符合设定的格式要求(包含后缀处理等情况),最后以字符串形式返回生成的ID。 + * + * @return 返回生成的唯一ID字符串,可能带有自定义后缀(根据构造函数传入的配置决定),用于在业务中作为唯一标识使用,例如数据库主键等场景。 + */ public synchronized String nextId() { + // 获取当前时间戳(单位:毫秒),通过调用 timeGen 方法来获取,这个时间戳是生成ID的重要依据之一,用于区分不同时间生成的ID,保证时间维度上的唯一性。 long timestamp = timeGen(); + // 判断当前时间戳是否小于上一次生成ID的时间戳(lastTimestamp),如果小于则说明出现了时间回拨的情况(比如系统时间被手动调整回退了), + // 这种情况下为了保证ID的唯一性和时间顺序性,会抛出 RuntimeException 异常,拒绝生成ID,因为时间回拨可能导致ID重复等问题。 if (timestamp < lastTimestamp) { throw new RuntimeException(String.format( "Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp)); } + // 如果当前时间戳与上一次生成ID的时间戳相等,说明在同一毫秒内,需要对序列号(sequence)进行处理,通过自增并与 sequenceMask 进行按位与操作, + // 实现序列号在规定的位数范围内循环递增(保证在同一毫秒内不同调用生成不同的ID),如果序列号达到最大值(sequenceMask 对应的最大值,即循环一轮回到0), + // 则需要等待到下一个毫秒再生成ID,通过调用 tilNextMillis 方法来获取下一个可用的时间戳。 if (lastTimestamp == timestamp) { sequence = (sequence + 1) & sequenceMask; if (sequence == 0) { timestamp = tilNextMillis(lastTimestamp); } } else { + // 如果当前时间戳与上一次的不同,说明进入了新的一毫秒,此时将序列号重置为0,重新开始在这一毫秒内的ID生成计数。 sequence = 0L; } + // 更新 lastTimestamp 为当前的时间戳,为下一次生成ID时判断时间戳情况做准备,确保每次生成ID都能正确处理时间相关的逻辑。 lastTimestamp = timestamp; + // 通过位运算组合时间戳、数据中心ID、工作节点ID和序列号等信息,构建出一个唯一的长整型数值(serialNumber),这个数值就是ID的核心部分, + // 其位运算的顺序和位移的位数都是根据前面定义的各个参数(如 timestampLeftShift、datacenterIdShift、workerIdShift 等)来确定的,保证了各个部分在ID中的正确位置和唯一性体现。 long serialNumber = ((timestamp - twepoch) << timestampLeftShift) | (datacenterId << datacenterIdShift) | (workerId << workerIdShift) | sequence; - return (suffix == null || suffix.trim().length() == 0) ? serialNumber+"" : (flag ? (new StringBuffer()).append(suffix).append(serialNumber).toString() : (new StringBuffer()).append(serialNumber).append(suffix).toString()); + // 根据是否有自定义后缀(suffix)以及后缀添加位置标志(flag)来决定最终返回的ID字符串形式, + // 如果后缀为空或者去除空格后长度为0,则直接将 serialNumber 转换为字符串返回; + // 如果 flag 为 true,表示后缀添加在前面,则通过 StringBuffer 先拼接后缀再拼接 serialNumber 并转换为字符串返回; + // 如果 flag 为 false,表示后缀添加在后面,则通过 StringBuffer 先拼接 serialNumber 再拼接后缀并转换为字符串返回,从而生成符合要求的带有后缀(或不带后缀)的唯一ID字符串。 + return (suffix == null || suffix.trim().length() == 0)? serialNumber + "" : (flag? (new StringBuffer()).append(suffix).append(serialNumber).toString() : (new StringBuffer()).append(serialNumber).append(suffix).toString()); } + /** + * 等待下一个毫秒 + * 该方法用于在序列号达到最大值(同一毫秒内ID生成达到上限)时,等待系统时间进入下一个毫秒,以获取新的可用时间戳来继续生成ID, + * 通过不断获取当前时间戳(调用 timeGen 方法)并与传入的上一次时间戳(lastTimestamp)比较,直到获取到大于 lastTimestamp 的时间戳,保证进入下一个不同的毫秒,然后返回这个新的时间戳。 + * + * @param lastTimestamp 上一次生成ID时的时间戳(单位:毫秒),用于作为判断是否进入下一个毫秒的参考基准,避免在同一毫秒内继续生成ID导致序列号溢出等问题。 + * @return 返回下一个可用的时间戳(单位:毫秒),即大于 lastTimestamp 的时间戳,用于后续在生成ID时作为新的时间依据,保证ID生成的顺序性和唯一性。 + */ protected long tilNextMillis(long lastTimestamp) { long timestamp = timeGen(); while (timestamp <= lastTimestamp) { @@ -92,8 +151,14 @@ public class IdGen return timestamp; } + /** + * 获取当前时间戳 + * 该方法用于获取当前系统时间的时间戳(单位:毫秒),通过调用 System.currentTimeMillis 方法来获取,为ID生成过程中基于时间维度保证唯一性提供当前时间信息, + * 外部也可以通过继承该类并重写这个方法来实现自定义的时间获取逻辑(例如在测试等场景下模拟不同的时间情况),具有一定的扩展性。 + * + * @return 返回当前系统时间的时间戳(单位:毫秒),即从1970年1月1日00:00:00 UTC到当前时间的毫秒数,用于ID生成等时间相关的计算操作。 + */ protected long timeGen() { return System.currentTimeMillis(); } - -} +} \ No newline at end of file diff --git a/tamguo/src/main/java/com/tamguo/util/ObjectUtil.java b/tamguo/src/main/java/com/tamguo/util/ObjectUtil.java index 41e79a0..4ce1f0a 100644 --- a/tamguo/src/main/java/com/tamguo/util/ObjectUtil.java +++ b/tamguo/src/main/java/com/tamguo/util/ObjectUtil.java @@ -6,9 +6,21 @@ import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; +// ObjectUtil类继承自SerializeTranscoder(这里假设SerializeTranscoder是一个与序列化、反序列化相关的抽象类或者接口,定义了一些序列化和反序列化相关的方法签名), +// 主要提供了对象序列化、反序列化以及对象相等性比较的实用功能,方便在需要处理对象的持久化存储、网络传输以及对象比较等场景中使用。 public class ObjectUtil extends SerializeTranscoder { + + /** + * 序列化对象方法 + * 将传入的对象(value)转换为字节数组,实现对象的序列化操作。如果传入的对象为 null,则会抛出 NullPointerException 异常, + * 因为无法对 null 对象进行序列化。在序列化过程中,若出现 I/O 异常(比如写入流时发生错误等情况),则会抛出 IllegalArgumentException 异常,并将原始异常信息一并传递出去,方便排查问题。 + * + * @param value 要进行序列化的对象,必须是非 null 的对象,其类型需实现 Serializable 接口(Java 序列化要求)才能正确序列化。 + * @return 返回序列化后的字节数组,该字节数组可以用于存储到文件、发送到网络等场景,以便后续进行反序列化还原对象。 + */ @Override public byte[] serialize(Object value) { + // 首先检查传入的对象是否为 null,如果是则抛出异常,提示不能序列化 null 对象。 if (value == null) { throw new NullPointerException("Can't serialize null"); } @@ -16,54 +28,98 @@ public class ObjectUtil extends SerializeTranscoder { ByteArrayOutputStream bos = null; ObjectOutputStream os = null; try { + // 创建一个 ByteArrayOutputStream 对象,它用于在内存中缓冲要序列化的数据,后续可以方便地转换为字节数组获取序列化后的内容。 bos = new ByteArrayOutputStream(); + // 创建一个 ObjectOutputStream 对象,它用于将对象转换为字节流进行序列化,将其关联到 ByteArrayOutputStream,这样写入 ObjectOutputStream 的数据最终会存储到 ByteArrayOutputStream 中。 os = new ObjectOutputStream(bos); + // 通过 ObjectOutputStream 的 writeObject 方法将传入的对象(value)进行序列化并写入到流中,开始实际的序列化操作,将对象的状态信息转换为字节形式。 os.writeObject(value); + // 关闭 ObjectOutputStream,释放相关资源,注意关闭流时可能会抛出 IOException,这里需要进行异常处理,不过在 finally 块中还会进一步确保流的关闭操作。 os.close(); + // 关闭 ByteArrayOutputStream,同样是释放资源,也可能抛出 IOException,同样在 finally 块中再次保障其能正确关闭。 bos.close(); + // 将 ByteArrayOutputStream 中的字节数据获取出来,赋值给 result 变量,这个就是最终序列化后的字节数组,代表了传入对象序列化后的结果。 result = bos.toByteArray(); } catch (IOException e) { + // 如果在序列化过程中出现 I/O 异常(比如流写入失败、关闭流出错等情况),则抛出 IllegalArgumentException 异常,并将原始的 IOException 作为原因传递进去, + // 提示出现了不可序列化的对象问题,同时方便调用者获取详细的异常信息来排查到底是哪里的序列化操作出现了错误。 throw new IllegalArgumentException("Non-serializable object", e); } finally { + // 调用 close 方法(这里假设是类中定义的用于关闭流的辅助方法,确保流能正确关闭并释放资源,避免资源泄漏)关闭 ObjectOutputStream,无论前面是否出现异常,都要保证流能正确关闭。 close(os); + // 同样调用 close 方法关闭 ByteArrayOutputStream,保障资源的正确释放。 close(bos); } return result; } + /** + * 反序列化对象方法 + * 根据传入的字节数组(in),将其还原为对应的对象,实现反序列化操作。如果字节数组为 null,则直接返回 null,不会进行反序列化操作。 + * 在反序列化过程中,若出现 I/O 异常(比如读取流时出错)或者类找不到异常(ClassNotFoundException,例如反序列化时找不到对应的类定义等情况),会打印异常堆栈信息进行简单的错误提示, + * 但方法仍会继续执行并返回 null(可能导致反序列化失败),不过这种处理方式可以根据实际业务需求进一步优化,比如向上抛出异常让调用者进行更合适的处理等。 + * + * @param in 要进行反序列化的字节数组,其内容应该是之前通过序列化操作得到的,且对应的对象类型需要在当前类路径下能找到(否则会出现 ClassNotFoundException)。 + * @return 返回反序列化后的对象,如果反序列化失败(比如出现异常情况)则返回 null,调用者需要根据业务情况判断返回值是否有效并进行相应处理。 + */ @Override public Object deserialize(byte[] in) { Object result = null; ByteArrayInputStream bis = null; ObjectInputStream is = null; try { - if (in != null) { + // 首先判断传入的字节数组是否为 null,如果是则直接返回 null,不进行后续的反序列化操作,因为没有有效的数据可供反序列化。 + if (in!= null) { + // 创建一个 ByteArrayInputStream 对象,它用于从字节数组中读取数据,作为反序列化的数据源,将传入的字节数组作为参数传入,初始化流对象。 bis = new ByteArrayInputStream(in); + // 创建一个 ObjectInputStream 对象,它用于从字节流中读取并还原对象,将其关联到 ByteArrayInputStream,以便从字节数组对应的流中读取数据进行反序列化操作。 is = new ObjectInputStream(bis); + // 通过 ObjectInputStream 的 readObject 方法进行反序列化操作,从流中读取数据并尝试还原为对应的对象,将还原后的对象赋值给 result 变量。 result = is.readObject(); + // 关闭 ObjectInputStream,释放相关资源,同样关闭流时可能出现 IOException,需要进行异常处理,并且在 finally 块中会再次确保流的关闭。 is.close(); + // 关闭 ByteArrayInputStream,释放资源,也可能出现 IOException,同样在 finally 块中保障其正确关闭。 bis.close(); } } catch (IOException e) { + // 如果在反序列化过程中出现 I/O 异常(比如流读取失败、关闭流出错等情况),则打印异常堆栈信息进行简单的错误提示,不过当前方法仍会继续执行并返回 null, + // 这种处理方式可能导致反序列化失败后调用者难以察觉具体问题,可根据业务实际情况优化,比如向上抛出异常让调用者处理等。 e.printStackTrace(); } catch (ClassNotFoundException e) { + // 如果出现 ClassNotFoundException 异常,意味着在反序列化时找不到对应的类定义(可能是类所在的 JAR 包未引入、类名更改等原因),同样打印异常堆栈信息进行简单提示, + // 方法继续执行并返回 null,可根据实际需求改进处理方式,比如更明确地告知调用者类找不到导致反序列化失败等情况。 e.printStackTrace(); } finally { + // 调用 close 方法关闭 ObjectInputStream,确保资源能正确释放,无论前面是否出现异常,都要执行关闭操作,避免资源泄漏。 close(is); + // 调用 close 方法关闭 ByteArrayInputStream,保障资源的正确关闭和释放。 close(bis); } return result; } + /** + * 对象相等性比较方法 + * 用于比较两个对象是否相等,先通过简单的对象引用比较(判断是否是同一个对象),如果两个对象引用相同(即 o1 == o2),则直接返回 true,表示对象相等。 + * 接着判断是否有一个对象为 null,如果其中一个为 null,则返回 false,表示两个对象不相等。最后通过调用对象的 equals 方法(前提是对象所属类正确重写了 equals 方法)来比较对象内容是否相等, + * 返回相应的比较结果,整体上综合考虑了对象引用相等和内容相等的情况来判断两个对象是否相等,符合 Java 中比较对象相等性的常规逻辑。 + * + * @param o1 要比较的第一个对象,可以为 null。 + * @param o2 要比较的第二个对象,可以为 null。 + * @return 如果两个对象相等(引用相等或者内容相等)则返回 true,否则返回 false,表示两个对象不相等。 + */ public static boolean equals(Object o1, Object o2) { + // 首先判断两个对象引用是否相同,如果是则表示它们是同一个对象,直接返回 true,说明对象相等。 if (o1 == o2) { return true; } else if (o1 == null || o2 == null) { + // 如果两个对象引用不相同,接着判断是否有一个对象为 null,如果有,则返回 false,表示两个对象不相等,因为只有两个非 null 对象才有进一步比较内容相等的必要。 return false; } else { + // 如果两个对象都不为 null,且引用不同,那么调用对象的 equals 方法(这里要求对象所属类正确重写了 equals 方法,遵循 Java 中 equals 方法的重写规范)来比较对象内容是否相等, + // 返回相应的比较结果,以此确定两个对象是否真正相等。 return o1.equals(o2); } } - -} +} \ No newline at end of file diff --git a/tamguo/src/main/java/com/tamguo/util/PageUtils.java b/tamguo/src/main/java/com/tamguo/util/PageUtils.java index 5c4f16f..010a508 100644 --- a/tamguo/src/main/java/com/tamguo/util/PageUtils.java +++ b/tamguo/src/main/java/com/tamguo/util/PageUtils.java @@ -5,85 +5,116 @@ import java.util.List; import com.baomidou.mybatisplus.plugins.Page; +// PageUtils类主要用于对分页相关的数据进行处理和封装,将MyBatis Plus中Page对象里的分页信息以及对应的数据进行提取、整理, +// 并提供了方便获取和设置分页相关属性(如是否显示上一页/下一页按钮、页码列表、当前页码、总页数、总数量等)的方法,方便在前端展示分页信息以及进行分页交互操作。 public class PageUtils { - - // 是否下一页按钮 + + // 用于标识是否显示下一页按钮,初始值为 false,后续会根据实际的分页情况(当前页是否小于总页数)来设置其值,用于前端判断是否展示下一页的操作按钮。 private Boolean isShowNextBtn = false; - - // 是否上一页按钮 + + // 用于标识是否显示上一页按钮,初始值为 false,同样会依据当前页与总页数等分页情况来确定其值,方便前端判断是否展示上一页的操作按钮。 private Boolean isShowPreBtn = false; - - // 当前页 + + // 存储当前页码的字符串表示形式,会从传入的Page对象中获取当前页码并转换为字符串进行存储,用于前端展示当前所在的页码信息。 private String currPageNum; - - // 页码列表 + + // 用于存储页码列表的字符串集合,例如 ["1", "2", "3", "...", "5", "6", "7"] 这样的形式,表示分页的页码展示情况,会根据总页数等条件动态生成该列表,方便前端展示分页页码导航。 private List pageNums; - - // 总页数 + + // 存储总页数的字符串表示形式,从Page对象中获取总页数并转换为字符串保存,用于告知前端总共有多少页,方便进行分页范围的判断等操作。 private String totalPage; - - // 总数量 + + // 存储数据总量的字符串表示形式,从Page对象中获取总记录数并转换为字符串,用于在前端展示总共有多少条数据,让用户对数据规模有直观了解。 private String total; - - // 数据 + + // 用于存储当前页的数据列表,类型为泛型List,实际存放的是从Page对象中获取的对应页的记录数据,方便在前端展示该页的具体数据内容。 private List list; - - public static PageUtils getPage(Page page){ + + /** + * 根据Page对象获取PageUtils实例的静态方法 + * 该方法接收一个MyBatis Plus的Page对象作为参数,基于这个Page对象中的分页信息(如当前页、总页数、总记录数等)来初始化一个PageUtils实例, + * 对实例中的各个属性(是否显示上一页/下一页按钮、页码列表、当前页码、总页数、总数量、数据列表等)进行相应的设置,最后返回这个初始化好的PageUtils实例,方便后续在业务中使用其封装好的分页相关信息。 + * + * @param page MyBatis Plus的Page对象,包含了分页相关的各种信息(当前页、每页记录数、总记录数、总页数以及对应页的数据列表等),用于提取信息来初始化PageUtils实例。 + * @return 返回一个初始化好的PageUtils实例,其中包含了整理好的分页相关属性信息,可用于在前端展示分页情况以及传递分页相关数据等操作。 + */ + public static PageUtils getPage(Page page) { PageUtils pg = new PageUtils(); - if(page.getCurrent() > 1){ + // 判断传入的Page对象的当前页码(page.getCurrent())是否大于1,如果大于1,说明不是第一页,此时应该显示上一页按钮,所以将PageUtils实例中的isShowPreBtn属性设置为true。 + if (page.getCurrent() > 1) { pg.setIsShowPreBtn(true); } - if(page.getCurrent() < page.getPages()){ + // 判断传入的Page对象的当前页码(page.getCurrent())是否小于总页数(page.getPages()),如果小于,表明还有下一页,那么需要显示下一页按钮,故将PageUtils实例中的isShowNextBtn属性设置为true。 + if (page.getCurrent() < page.getPages()) { pg.setIsShowNextBtn(true); } + List pgNums = new ArrayList<>(); - if(page.getPages() > 1){ - if(page.getPages() > 10){ + // 判断总页数(page.getPages())是否大于1,如果大于1才需要构建页码列表,因为如果只有1页,就不需要展示页码导航等信息了。 + if (page.getPages() > 1) { + // 如果总页数大于10,采用一种特定的页码列表生成策略,以简化页码展示同时保证关键页码信息的显示,方便用户进行分页导航操作。 + if (page.getPages() > 10) { + // 先添加首页页码 "1",这是常见的分页页码展示习惯,方便用户快速回到第一页。 pgNums.add("1"); + // 添加第二页页码 "2",同样是为了展示较靠前的页码信息,便于用户快速定位到前面的页面。 pgNums.add("2"); + // 添加第三页页码 "3",继续完善前面部分的页码展示,让用户能看到初始的几页页码信息。 pgNums.add("3"); + // 添加省略号 "...",用于表示中间部分页码省略,当总页数较多时,避免全部显示页码导致页面过于冗长,通过省略号提示用户还有其他页码存在。 pgNums.add("..."); - if(page.getCurrent() == page.getPages()){ - pgNums.add(((Integer)(page.getCurrent() - 2)).toString()); - pgNums.add(((Integer)(page.getCurrent() - 1)).toString()); - pgNums.add(((Integer)page.getCurrent()).toString()); - }else{ - pgNums.add(((Integer)(page.getCurrent() - 1)).toString()); - pgNums.add(((Integer)page.getCurrent()).toString()); - pgNums.add(((Integer)(page.getCurrent() + 1)).toString()); + // 判断当前页是否等于总页数,如果是,说明当前处于最后一页,那么页码列表中要展示当前页的前两页页码以及当前页码,以方便用户进行前后页的切换查看等操作。 + if (page.getCurrent() == page.getPages()) { + pgNums.add(((Integer) (page.getCurrent() - 2)).toString()); + pgNums.add(((Integer) (page.getCurrent() - 1)).toString()); + pgNums.add(((Integer) page.getCurrent()).toString()); + } else { + // 如果当前页不等于总页数,说明不是最后一页,此时页码列表中展示当前页的前一页、当前页以及后一页的页码,方便用户进行临近页码的切换操作。 + pgNums.add(((Integer) (page.getCurrent() - 1)).toString()); + pgNums.add(((Integer) page.getCurrent()).toString()); + pgNums.add(((Integer) (page.getCurrent() + 1)).toString()); } - }else{ + } else { + // 如果总页数不超过10页,采用简单的顺序添加页码的方式生成页码列表,从1开始,依次添加每一页的页码,直到达到总页数为止。 Integer n = 1; - if(page.getTotal() > 0){ - while(true){ + if (page.getTotal() > 0) { + while (true) { pgNums.add(n.toString()); - if(n >= page.getPages()){ + if (n >= page.getPages()) { break; } - n ++; + n++; } } } } else { + // 如果总页数等于1页,同样采用顺序添加页码的方式(这里其实只会添加页码 "1")生成页码列表,从1开始,添加到总页数为止,虽然只有1页,但保持页码列表生成逻辑的一致性。 Integer n = 1; - if(page.getTotal() > 0){ - while(true){ + if (page.getTotal() > 0) { + while (true) { pgNums.add(n.toString()); - if(n >= page.getPages()){ + if (n >= page.getPages()) { break; } - n ++; + n++; } } } + + // 将生成好的页码列表设置到PageUtils实例的pageNums属性中,以便后续通过相应的获取方法(getPageNums)提供给外部使用,展示分页页码信息。 pg.setPageNums(pgNums); + // 将Page对象中当前页的数据列表(page.getRecords())设置到PageUtils实例的list属性中,方便后续获取该页的数据用于前端展示等操作。 pg.setList(page.getRecords()); - pg.setCurrPageNum(((Integer)page.getCurrent()).toString()); - pg.setTotal(((Integer)page.getTotal()).toString()); - pg.setTotalPage(((Integer)page.getPages()).toString()); + // 将Page对象的当前页码(page.getCurrent())转换为字符串后设置到PageUtils实例的currPageNum属性中,用于前端展示当前所在页码信息。 + pg.setCurrPageNum(((Integer) page.getCurrent()).toString()); + // 将Page对象的总记录数(page.getTotal())转换为字符串后设置到PageUtils实例的total属性中,用于前端展示数据总量信息。 + pg.setTotal(((Integer) page.getTotal()).toString()); + // 将Page对象的总页数(page.getPages())转换为字符串后设置到PageUtils实例的totalPage属性中,用于前端展示总页数信息,方便用户了解分页的整体范围。 + pg.setTotalPage(((Integer) page.getPages()).toString()); + return pg; } + // 以下是各个属性的Getter和Setter方法,遵循JavaBean规范,方便外部类对PageUtils实例中的属性进行获取和设置操作,保证数据的封装性和可访问性。 public Boolean getIsShowNextBtn() { return isShowNextBtn; @@ -109,42 +140,34 @@ public class PageUtils { this.list = list; } - public Boolean getIsShowPreBtn() { return isShowPreBtn; } - public void setIsShowPreBtn(Boolean isShowPreBtn) { this.isShowPreBtn = isShowPreBtn; } - public String getCurrPageNum() { return currPageNum; } - public void setCurrPageNum(String currPageNum) { this.currPageNum = currPageNum; } - public String getTotalPage() { return totalPage; } - public void setTotalPage(String totalPage) { this.totalPage = totalPage; } - public String getTotal() { return total; } - public void setTotal(String total) { this.total = total; } diff --git a/tamguo/src/main/java/com/tamguo/util/RequestHelper.java b/tamguo/src/main/java/com/tamguo/util/RequestHelper.java index de0222b..acdbd0c 100644 --- a/tamguo/src/main/java/com/tamguo/util/RequestHelper.java +++ b/tamguo/src/main/java/com/tamguo/util/RequestHelper.java @@ -6,36 +6,54 @@ import javax.servlet.http.HttpServletRequest; import org.apache.log4j.Logger; +// RequestHelper类主要用于辅助处理与HTTP请求相关的一些通用操作,当前类中仅展示了一个获取客户端IP地址的方法, +// 该方法会尝试从不同的请求头信息中获取IP地址,以应对客户端通过代理服务器等方式访问时获取真实IP地址的情况,同时会进行相应的日志记录。 public class RequestHelper { + + // 创建一个Logger对象,用于记录与这个类相关的日志信息,这里将日志记录器的名称设置为RequestHelper类的全限定名, + // 通过这个日志记录器可以在合适的地方输出不同级别的日志(如INFO级别等),方便调试和查看相关操作信息,例如记录获取IP地址的过程和结果等情况。 private static Logger logger = Logger.getLogger(RequestHelper.class); /** - * 获取请求主机IP地址,如果通过代理进来,则透过防火墙获取真实IP地址; - * - * @param request - * @return - * @throws IOException + * 获取请求主机IP地址,如果通过代理进来,则透过防火墙获取真实IP地址 + * 该方法用于从HTTP请求(HttpServletRequest)中获取客户端的IP地址,它会按照一定的顺序尝试从多个请求头字段中查找IP地址信息, + * 因为在实际网络环境中,客户端可能通过代理服务器访问服务端,此时真实的客户端IP地址可能被隐藏在特定的请求头字段中,而不是简单地通过 request.getRemoteAddr() 获取到, + * 所以需要依次检查不同的请求头来获取准确的客户端IP地址,同时还会对获取到的IP地址进行一些格式上的处理(如处理多个IP地址以逗号分隔的情况),最后返回获取到的IP地址,并且会根据日志级别记录获取IP地址的相关信息到日志中。 + * + * @param request HttpServletRequest对象,代表客户端发送到服务器端的HTTP请求,从中提取与IP地址相关的请求头信息来获取客户端IP地址。 + * @return 返回获取到的客户端IP地址字符串,如果所有尝试获取的方式都失败,则可能返回默认的IP地址(如通过 request.getRemoteAddr() 获取到的IP地址等情况)。 + * @throws IOException 虽然当前方法体内部没有直接抛出此异常的代码逻辑,但声明抛出该异常是为了遵循可能的接口规范或者在后续方法扩展时存在I/O相关操作导致异常抛出的可能性,调用者需要进行相应的异常处理。 */ public final static String getIpAddress(HttpServletRequest request) { - // 获取请求主机IP地址,如果通过代理进来,则透过防火墙获取真实IP地址 + // 首先尝试从 "X-Forwarded-For" 请求头字段中获取IP地址,这个请求头通常在客户端通过代理服务器访问时,由代理服务器添加,用于记录原始客户端的IP地址以及经过的代理服务器IP地址列表(以逗号分隔), + // 如果该请求头不存在或者其值为空字符串或者值为 "unknown"(表示无法获取到有效IP地址的一种约定情况),则继续尝试从其他请求头获取IP地址。 String ip = request.getHeader("X-Forwarded-For"); if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { + // 如果从 "X-Forwarded-For" 请求头中未获取到有效IP地址,再次尝试从 "Proxy-Client-IP" 请求头获取IP地址, + // 某些代理服务器会使用这个请求头来传递客户端的IP地址信息,同样判断其是否为空或者值为 "unknown",如果是则继续尝试下一个请求头。 if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("Proxy-Client-IP"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { + // 接着尝试从 "WL-Proxy-Client-IP" 请求头获取IP地址,这是WebLogic服务器作为代理时可能使用的请求头来传递客户端IP地址,继续进行有效性判断和后续尝试。 ip = request.getHeader("WL-Proxy-Client-IP"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { + // 再尝试从 "HTTP_CLIENT_IP" 请求头获取IP地址,一些其他的代理服务器或者特定的网络环境可能会通过这个请求头传递客户端IP信息,同样检查是否有效并按需继续查找。 ip = request.getHeader("HTTP_CLIENT_IP"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { + // 然后尝试从 "HTTP_X_FORWARDED_FOR" 请求头获取IP地址,它与 "X-Forwarded-For" 类似,也是可能用于记录客户端IP经过代理的相关情况的请求头,继续判断并尝试获取有效IP地址。 ip = request.getHeader("HTTP_X_FORWARDED_FOR"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { + // 如果前面所有尝试从请求头获取IP地址的方式都失败了,最后通过 request.getRemoteAddr() 获取IP地址, + // 这个方法通常获取的是与服务器直接建立连接的客户端(可能是代理服务器或者真实客户端,如果没有代理的话)的IP地址,作为一种兜底获取IP地址的方式。 ip = request.getRemoteAddr(); } } else if (ip.length() > 15) { + // 如果从 "X-Forwarded-For" 请求头中获取到的IP地址字符串长度大于15,说明可能包含了多个IP地址(以逗号分隔的情况,记录了经过的多个代理服务器IP等情况), + // 需要对其进行处理,将IP地址字符串按照逗号分割为字符串数组,然后遍历数组查找第一个不为 "unknown" 的IP地址,将其作为客户端的真实IP地址,因为按照约定,真实客户端IP地址通常在最前面。 String[] ips = ip.split(","); for (int index = 0; index < ips.length; index++) { String strIp = (String) ips[index]; @@ -45,9 +63,13 @@ public class RequestHelper { } } } + + // 判断日志记录器的INFO级别是否启用(通过配置文件等方式设置日志级别),如果启用了INFO级别日志, + // 则记录一条INFO级别的日志信息,将获取到的IP地址信息以及方法相关的标识信息记录下来,方便后续查看获取IP地址的具体情况,例如排查IP地址获取是否正确等问题。 if (logger.isInfoEnabled()) { logger.info("getIpAddress(HttpServletRequest) - Proxy-Client-IP - String ip=" + ip); } + return ip; } -} +} \ No newline at end of file diff --git a/tamguo/src/main/java/com/tamguo/util/Result.java b/tamguo/src/main/java/com/tamguo/util/Result.java index 8ac6e41..8ac2b52 100644 --- a/tamguo/src/main/java/com/tamguo/util/Result.java +++ b/tamguo/src/main/java/com/tamguo/util/Result.java @@ -5,23 +5,33 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +// Result类用于封装操作结果的通用类,实现了Serializable接口,意味着该类的对象可以被序列化,方便在网络传输、持久化存储等场景下使用。 +// 它定义了表示操作结果的相关属性(如状态码、具体结果数据、提示信息等)以及一系列静态方法来便捷地创建不同类型(成功或失败)的结果对象,同时提供了一些用于构建特定格式结果数据的方法,适用于构建接口返回数据等情况。 public class Result implements Serializable { + // 序列化版本号,用于在对象序列化和反序列化过程中保证版本兼容性,当类的结构发生变化(如新增、删除字段等情况)时,若版本号不一致可能导致反序列化失败,这里是一个固定的长整型数值。 private static final long serialVersionUID = -1651614836984397356L; + // 用于表示操作结果的状态码,不同的整数值代表不同的操作结果状态,例如常见的用0表示成功,1表示失败等,方便调用者根据状态码判断操作是否成功以及进行相应的后续处理。 private int code; + // 用于存储操作的具体结果数据,可以是任意类型的对象(如实体对象、数据集合等),根据具体的业务操作来决定存放的内容,方便将业务相关的数据返回给调用者。 private Object result; + // 用于存储与操作结果相关的提示信息,比如操作成功时可以为空字符串,操作失败时存放具体的错误提示内容,便于告知调用者操作的执行情况。 private String message; + // 定义表示成功的状态码常量,方便在创建成功结果对象时统一使用,使代码更具可读性和可维护性,当前设置为0,表示操作成功的情况。 public static final int SUCCESS_CODE = 0; + // 定义表示失败的状态码常量,同样用于在创建失败结果对象时保持一致性,当前设定为1,用于标识操作出现问题、未成功执行的情况。 public static final int FAIL_CODE = 1; + // 私有构造函数,用于限制外部直接通过构造函数创建Result对象,保证结果对象的创建只能通过类中提供的静态方法来进行,遵循封装的设计原则,便于统一管理结果对象的创建逻辑。 private Result() { } + // 私有构造函数,用于在类内部创建Result对象时传入状态码、具体结果数据以及提示信息,通过这种方式初始化Result对象的各个属性,同样外部无法直接调用这个构造函数,只能通过静态方法间接使用。 private Result(int code, Object result, String message) { this.code = code; this.result = result; @@ -29,67 +39,139 @@ public class Result implements Serializable { } /** - * 成功响应 - * - * @param result - * @return + * 创建成功响应结果对象的静态方法(仅传入结果数据) + * 该方法用于创建一个表示操作成功的Result对象,接收操作的具体结果数据(result)作为参数,将状态码设置为成功状态码(SUCCESS_CODE,即0),提示信息设置为空字符串, + * 并将传入的结果数据存入Result对象的result属性中,最后返回这个构建好的表示成功的Result对象,方便在业务中当操作成功时快速构建并返回相应的结果对象给调用者。 + * + * @param result 操作的具体结果数据,可以是任意类型的对象,例如查询到的实体对象、数据列表等,根据具体业务场景而定。 + * @return 返回一个表示成功的Result对象,其中包含了传入的结果数据以及成功状态码和空的提示信息,可用于接口等场景返回成功的操作结果。 */ public static Result successResult(Object result) { return result(SUCCESS_CODE, result, ""); } + /** + * 创建成功响应结果对象的静态方法(用于分页相关数据,部分参数重载) + * 该方法重载了successResult方法,用于在分页相关业务场景下创建表示成功的Result对象,它接收记录数据(records)、记录总数(recordSum)、每页行数(rowsOfPage)作为参数, + * 会调用另一个重载的successResult方法,并将构建好的包含分页相关数据的Map对象作为结果数据传入,最终返回表示成功且包含分页数据的Result对象,方便在分页查询等操作成功时返回相应结果。 + * + * @param records 分页查询获取到的当前页记录数据,可以是实体对象列表等形式,具体取决于业务查询的结果类型。 + * @param recordSum 总的记录数量,用于表示满足查询条件的所有记录的总数,方便前端进行分页展示等相关操作时知晓数据总量情况。 + * @param rowsOfPage 每页显示的行数,用于明确分页的每页数据量大小,辅助前端进行分页逻辑处理。 + * @return 返回一个表示成功的Result对象,其中包含了封装好的分页相关数据以及成功状态码和空的提示信息,适用于分页查询成功后的结果返回场景。 + */ public static Result successResult(Object records, Long recordSum, Long rowsOfPage) { return successResult(records, recordSum, rowsOfPage, null); } + /** + * 创建成功响应结果对象的静态方法(用于分页相关数据且包含用户自定义数据) + * 该方法再次重载successResult方法,用于在分页业务场景且可能包含用户自定义额外数据(userData)时创建表示成功的Result对象, + * 它会先调用resultOfList方法构建一个包含分页相关数据(记录数据、记录总数、每页行数)以及用户自定义数据的Map对象,然后将这个Map对象作为结果数据传入另一个successResult方法中, + * 最终返回一个包含了所有相关数据且表示成功的Result对象,方便在更复杂的分页业务场景下(如需要传递额外用户相关数据等情况)返回合适的结果给调用者。 + * + * @param records 分页查询获取到的当前页记录数据,类型通常为数据列表等,由具体业务查询决定。 + * @param recordSum 总的记录数量,表明满足查询条件的全部记录的个数,供前端进行分页展示及相关操作参考。 + * @param rowsOfPage 每页显示的行数,定义了分页时每页包含的数据量大小,便于前端进行分页逻辑处理。 + * @param userData 用户自定义的额外数据,可以是任意类型的对象,用于在结果中传递一些与业务相关的、不属于常规分页数据但又需要返回给前端的信息,例如用户权限相关数据等。 + * @return 返回一个表示成功的Result对象,其中包含了封装好的分页相关数据、用户自定义数据以及成功状态码和空的提示信息,适用于复杂分页查询成功且有额外数据要返回的场景。 + */ public static Result successResult(Object records, Long recordSum, Long rowsOfPage, Object userData) { Map result = resultOfList(records, recordSum, rowsOfPage, userData); return successResult(result); } + /** + * 构建包含分页相关数据的Map对象的静态方法(部分参数重载) + * 该方法用于构建一个包含分页相关核心数据(记录数据、记录总数、每页行数)的Map对象,方便在构建分页结果时统一组织数据格式, + * 如果不需要传递用户自定义数据,可调用这个方法来创建基本的分页数据Map,返回的Map对象后续可用于进一步构建Result对象或者直接在其他需要分页数据结构的地方使用。 + * + * @param records 分页查询获取到的当前页记录数据,一般为数据列表等形式,取决于具体业务查询的结果类型。 + * @param recordSum 总的记录数量,代表满足查询条件的所有记录的总数,便于前端知晓数据总量情况进行分页展示等操作。 + * @param rowsOfPage 每页显示的行数,明确了分页时每页的数据量大小,有助于前端处理分页逻辑。 + * @return 返回一个Map对象,其中包含了以固定键值对形式存放的分页相关数据("rows"对应记录数据、"records"对应记录总数、"total"对应每页行数),可用于后续的结果构建等操作。 + */ public static Map resultOfList(Object records, Long recordSum, Long rowsOfPage) { return resultOfList(records, recordSum, rowsOfPage, null); } + /** + * 构建包含分页相关数据及用户自定义数据的Map对象的静态方法 + * 该方法用于构建一个包含分页相关核心数据(记录数据、记录总数、每页行数)以及用户自定义数据(userData)的Map对象,通过创建一个新的HashMap对象, + * 向其中按照固定的键(如 "rows"、"records"、"total"、"userdata"等)添加相应的值(传入的参数数据),构建出符合特定格式要求的分页数据结构,方便在复杂的分页业务场景下传递完整的数据信息, + * 最后返回这个构建好的包含所有相关数据的Map对象,用于后续构建Result对象或者其他需要该数据结构的地方使用。 + * + * @param Obj 分页查询获取到的当前页记录数据,通常是数据列表等形式,根据具体业务查询结果而定。 + * @param records 总的记录数量,表明满足查询条件的全部记录个数,供前端进行分页展示及相关操作参考。 + * @param rowsOfPage 每页显示的行数,定义了分页时每页的数据量大小,便于前端进行分页逻辑处理。 + * @param userData 用户自定义的额外数据,可以是任意类型的对象,用于传递一些不属于常规分页数据但又与业务相关且需要返回给前端的信息,例如用户权限相关数据等。 + * @return 返回一个Map对象,其中包含了以固定键值对形式存放的分页相关数据以及用户自定义数据("rows"对应记录数据、"records"对应记录总数、"total"对应每页行数、"userdata"对应用户自定义数据),方便后续使用。 + */ public static Map resultOfList(Object Obj, Long records, Long rowsOfPage, Object userData) { Map result = new HashMap(); result.put("rows", Obj); result.put("records", records); result.put("total", rowsOfPage); - if (null != userData) { + if (null!= userData) { result.put("userdata", userData); } - ; return result; } + /** + * 构建适用于jqGrid组件的分页结果数据的Map对象的静态方法 + * 该方法用于构建一个符合jqGrid组件(一种常用于前端展示表格数据且支持分页等功能的插件)要求的分页结果数据的Map对象, + * 通过创建新的HashMap对象,按照jqGrid组件期望的键(如 "list"、"totalCount"、"pageSize"、"currPage"、"totalPage"等)添加相应的参数值(传入的列表数据、总记录数、每页大小、当前页码、总页码等), + * 构建出特定格式的数据结构,方便将后端分页查询结果以合适的格式返回给前端使用jqGrid组件进行展示,最后返回这个构建好的Map对象,用于后续的结果返回或者其他相关操作。 + * + * @param list 分页查询获取到的当前页记录数据,一般是数据列表形式,具体由业务查询决定,对应jqGrid组件中的数据列表展示内容。 + * @param totalCount 总的记录数量,代表满足查询条件的所有记录的总数,供jqGrid组件知晓数据总量以便进行分页展示等操作,对应jqGrid组件中的总记录数概念。 + * @param pageSize 每页显示的行数,明确了分页时每页的数据量大小,符合jqGrid组件中每页数据量的定义,便于其进行分页逻辑处理。 + * @param currPage 当前页码,用于告知jqGrid组件当前处于第几页,方便其进行页码导航等相关操作。 + * @param totalPage 总页码,表明整个分页结果一共有多少页,同样是jqGrid组件进行分页展示时需要知晓的关键信息之一。 + * @return 返回一个Map对象,其中包含了以固定键值对形式存放的适用于jqGrid组件的分页相关数据("list"对应记录数据、"totalCount"对应总记录数、"pageSize"对应每页行数、"currPage"对应当前页码、"totalPage"对应总页码),方便前端使用jqGrid组件展示分页数据。 + */ public static Map jqGridResult(List list, long totalCount, int pageSize, int currPage, - int totalPage) { + int totalPage) { Map result = new HashMap(); result.put("list", list); result.put("totalCount", totalCount); + result.put("totalPage", totalPage); result.put("pageSize", pageSize); result.put("currPage", currPage); - result.put("totalPage", totalPage); return result; } /** - * 失败响应 - * - * @param errorMsg - * @return + * 创建失败响应结果对象的静态方法 + * 该方法用于创建一个表示操作失败的Result对象,接收具体的错误提示信息(errorMsg)作为参数,将状态码设置为失败状态码(FAIL_CODE,即1), + * 把传入的错误提示信息存入Result对象的message属性中,具体结果数据属性(result)设置为空字符串,最后返回这个构建好的表示失败的Result对象,方便在业务操作失败时返回相应的结果给调用者,告知其失败原因。 + * + * @param errorMsg 具体的错误提示信息,用于说明操作失败的原因,例如数据库查询失败的具体错误描述等,是一个字符串类型的内容。 + * @return 返回一个表示失败的Result对象,其中包含了传入的错误提示信息、失败状态码以及空的结果数据,可用于接口等场景返回失败的操作结果。 */ public static Result failResult(String errorMsg) { return result(FAIL_CODE, "", errorMsg); } + /** + * 创建通用响应结果对象的静态方法 + * 该方法是一个较为通用的创建Result对象的静态方法,接收状态码(code)、具体结果数据(result)以及提示信息(message)作为参数, + * 通过调用私有构造函数来创建一个Result对象,并传入相应的参数初始化其各个属性,最后返回这个构建好的Result对象,方便在各种需要自定义状态码、结果数据和提示信息的场景下创建合适的结果对象。 + * + * @param code 表示操作结果的状态码,可根据业务定义不同的值来代表不同的结果状态,如0表示成功、1表示失败等,具体取值由业务需求决定。 + * @param result 操作的具体结果数据,可以是任意类型的对象,用于存放与业务操作相关的返回内容,例如查询到的实体对象、数据列表等。 + * @param message 与操作结果相关的提示信息,用于向调用者说明操作的执行情况,例如成功时可以为空字符串,失败时为具体的错误描述等,是一个字符串类型的内容。 + * @return 返回一个根据传入参数构建好的Result对象,包含了指定的状态码、结果数据和提示信息,适用于各种自定义结果返回的业务场景。 + */ public static Result result(int code, Object result, String message) { Result res = new Result(code, result, message); return res; } + // 以下是Result类中各个属性的Getter方法,遵循JavaBean规范,方便外部类获取Result对象中封装的状态码、结果数据以及提示信息等内容,保证数据的封装性和可访问性。 + public int getCode() { return code; } @@ -101,5 +183,4 @@ public class Result implements Serializable { public String getMessage() { return message; } - -} +} \ No newline at end of file diff --git a/tamguo/src/main/java/com/tamguo/util/Uploader.java b/tamguo/src/main/java/com/tamguo/util/Uploader.java index 2307e55..4c83e27 100644 --- a/tamguo/src/main/java/com/tamguo/util/Uploader.java +++ b/tamguo/src/main/java/com/tamguo/util/Uploader.java @@ -8,253 +8,153 @@ import org.apache.commons.codec.binary.Base64; import org.apache.commons.fileupload.*; import org.apache.commons.fileupload.FileUploadBase.InvalidContentTypeException; import org.apache.commons.fileupload.FileUploadBase.SizeLimitExceededException; -import org.apache.commons.fileupload.util.*; -import org.apache.commons.fileupload.servlet.*; +import org.apache.commons.fileupload.util.Streams; +import org.apache.commons.fileupload.servlet.ServletFileUpload; import org.apache.commons.fileupload.FileItemIterator; import org.apache.commons.fileupload.disk.DiskFileItemFactory; import javax.servlet.http.HttpServletRequest; -/** - * UEditor文件上传辅助类 - * - */ + +// Uploader类是一个UEditor文件上传辅助类,用于处理文件上传的各种操作,包括文件格式检查、大小限制判断、文件保存路径处理以及实际的文件上传逻辑等, +// 同时支持常规的表单文件上传和接收Base64格式文件上传两种方式,并且会根据上传过程中的不同情况设置相应的状态信息,方便调用者知晓上传结果。 public class Uploader { - // 输出文件地址 + + // 用于存储上传文件的输出文件地址,即文件最终保存的相对路径或者完整路径(根据具体使用场景确定),初始值为空字符串,在文件上传成功后会被赋值为实际的保存路径。 private String url = ""; - // 上传文件名 + // 用于存储上传后的文件名,这个文件名可能是经过处理后生成的新文件名(例如添加随机数、时间戳等以保证文件名的唯一性等情况),初始值为空字符串,上传成功后会被赋予实际的文件名。 private String fileName = ""; - // 状态 + // 用于表示文件上传的状态,通过设置不同的状态值(从errorInfo这个HashMap中获取对应的状态描述字符串)来反馈上传过程中出现的各种情况,例如成功、文件格式错误、大小超出限制等,初始值为空字符串,会在上传操作过程中根据情况进行更新。 private String state = ""; - // 文件类型 + // 用于存储文件的类型,一般通过文件扩展名来确定,例如 ".jpg"、".pdf" 等,在文件上传过程中会进行相应的提取和赋值操作,初始值为空字符串。 private String type = ""; - // 原始文件名 + // 用于存储上传文件的原始文件名,即用户在客户端选择文件时的原始文件名称,方便记录和后续可能的展示等操作,初始值为空字符串,上传时会获取并赋值。 private String originalName = ""; - // 文件大小 + // 用于存储上传文件的大小,单位为字节,在文件上传完成后会获取并记录文件的实际大小,初始值为0。 private long size = 0; + // 用于接收来自客户端的HTTP请求对象(HttpServletRequest),通过这个对象可以获取上传文件相关的信息(如请求头、请求参数等),在整个文件上传过程中起着关键的信息获取作用,初始值为null,通过构造函数传入。 private HttpServletRequest request = null; + // 用于存储文件的标题信息(可能在某些特定的业务场景下使用,例如图片有对应的标题描述等情况),初始值为空字符串,会在处理表单中对应字段时进行赋值。 private String title = ""; - // 保存路径 + // 用于指定文件的保存路径,默认值为 "upload",表示文件将会被保存到这个相对路径下(实际的物理路径会根据服务器配置等情况进一步确定),可以通过相应的Setter方法进行修改设置。 private String savePath = "upload"; - // 文件允许格式 - private String[] allowFiles = { ".rar", ".doc", ".docx", ".zip", ".pdf",".txt", ".swf", ".wmv", ".gif", ".png", ".jpg", ".jpeg", ".bmp" }; - // 文件大小限制,单位KB + // 用于定义允许上传的文件格式数组,只有文件名后缀符合这个数组中定义的格式的文件才允许被上传,初始包含了常见的一些文件格式,如压缩文件、文档文件、图片文件等类型的后缀名,可通过Setter方法重新设置允许的文件格式。 + private String[] allowFiles = { ".rar", ".doc", ".docx", ".zip", ".pdf", ".txt", ".swf", ".wmv", ".gif", ".png", ".jpg", ".jpeg", ".bmp" }; + // 用于设置文件大小的限制,单位为KB,默认值为10000KB,即10MB,如果上传文件的大小超过这个限制,将会在上传过程中被拦截并设置相应的错误状态,可通过Setter方法调整文件大小限制值。 private int maxSize = 10000; - + + // 用于存储各种文件上传错误信息的HashMap,键为表示错误类型的字符串(如 "SUCCESS"、"NOFILE" 等),值为对应的详细错误描述字符串,方便根据不同的上传情况设置相应的状态信息并反馈给调用者具体的错误原因。 private HashMap errorInfo = new HashMap(); + /** + * 构造函数,用于初始化Uploader实例并设置相关的错误信息映射 + * 该构造函数接收一个HttpServletRequest对象作为参数,将其赋值给成员变量request,以便后续在文件上传过程中获取请求相关的信息。 + * 同时初始化errorInfo这个HashMap,添加了一系列表示不同上传结果状态(成功或各种错误情况)的键值对,用于后续根据上传过程中的实际情况设置相应的状态信息并返回给调用者详细的反馈。 + * + * @param request HttpServletRequest对象,代表客户端发送的HTTP请求,包含了文件上传相关的信息,如请求头、请求参数以及上传的文件内容等,是进行文件上传操作的重要数据来源。 + */ public Uploader(HttpServletRequest request) { this.request = request; HashMap tmp = this.errorInfo; - tmp.put("SUCCESS", "SUCCESS"); //默认成功 - tmp.put("NOFILE", "未包含文件上传域"); - tmp.put("TYPE", "不允许的文件格式"); - tmp.put("SIZE", "文件大小超出限制"); - tmp.put("ENTYPE", "请求类型ENTYPE错误"); - tmp.put("REQUEST", "上传请求异常"); - tmp.put("IO", "IO异常"); - tmp.put("DIR", "目录创建失败"); - tmp.put("UNKNOWN", "未知错误"); - + tmp.put("SUCCESS", "SUCCESS"); //默认成功,设置表示上传成功的状态对应的描述信息为 "SUCCESS",方便后续判断和返回结果时使用。 + tmp.put("NOFILE", "未包含文件上传域"); // 设置表示请求中未包含文件上传相关字段的错误状态对应的描述信息,用于在没有文件上传时反馈相应的错误情况。 + tmp.put("TYPE", "不允许的文件格式"); // 设置表示上传文件的格式不符合允许的文件格式列表(allowFiles)的错误状态对应的描述信息,用于文件格式验证不通过时反馈错误原因。 + tmp.put("SIZE", "文件大小超出限制"); // 设置表示上传文件大小超过了设定的最大文件大小限制(maxSize)的错误状态对应的描述信息,用于文件大小超出限制时告知调用者具体错误情况。 + tmp.put("ENTYPE", "请求类型ENTYPE错误"); // 设置表示请求的类型(如Content-Type等相关请求头不符合文件上传要求)出现错误的状态对应的描述信息,用于请求类型不合法时反馈错误情况。 + tmp.put("REQUEST", "上传请求异常"); // 设置表示在文件上传请求处理过程中出现其他未知异常的错误状态对应的描述信息,用于捕获到一般性的请求相关异常时反馈错误原因。 + tmp.put("IO", "IO异常"); // 设置表示在文件输入输出(如读取文件、写入文件等操作)过程中出现I/O异常的错误状态对应的描述信息,用于处理文件读写等操作出现问题时反馈错误情况。 + tmp.put("DIR", "目录创建失败"); // 设置表示在创建文件保存目录(例如根据日期创建子目录等情况)时失败的错误状态对应的描述信息,用于目录创建出现问题时反馈错误原因。 + tmp.put("UNKNOWN", "未知错误"); // 设置表示其他未明确归类的、未知的错误情况对应的错误状态描述信息,用于捕获到意料之外的异常时进行统一的错误反馈。 } + /** + * 执行文件上传的方法(处理常规表单文件上传) + * 该方法用于处理通过常规表单形式进行的文件上传操作,首先判断请求是否为多部分表单数据(即是否包含文件上传域),如果不是,则设置相应的错误状态(NOFILE)并返回,不进行后续的上传操作。 + * 若是多部分表单数据,则创建DiskFileItemFactory用于配置文件上传的一些基础设置(如临时文件存储位置等),接着设置文件保存路径、创建ServletFileUpload对象并配置其相关属性(如文件大小限制、请求头编码等), + * 然后通过迭代器遍历请求中的文件项,对于每个非表单字段的文件项(即真正的上传文件),进行文件格式检查、生成新文件名、获取文件类型、构建文件保存路径等操作, + * 并将文件内容从输入流复制到输出流实现文件的保存,同时更新文件上传状态、记录文件大小等信息,若遇到文件大小超出限制、请求类型错误、文件上传异常等各种问题,则会相应地设置对应的错误状态并停止后续操作, + * 对于表单字段(如标题字段等)则进行相应的读取和处理(当前代码中仅处理名为 "pictitle" 的字段作为标题字段),最后若成功上传一个文件(UE中按只处理单张上传的逻辑)则直接退出循环。 + * + * @throws Exception 由于文件上传过程中涉及到很多可能抛出异常的操作(如文件读写、流操作、文件上传组件相关异常等),所以统一抛出Exception,调用者需要进行相应的异常处理,根据具体的业务需求来决定如何应对不同的异常情况。 + */ public void upload() throws Exception { + // 判断当前请求是否为多部分表单数据(即是否包含文件上传域),通过ServletFileUpload提供的静态方法进行判断,这是进行文件上传操作的前提条件,如果不是多部分表单数据,则不能进行后续的文件上传处理。 boolean isMultipart = ServletFileUpload.isMultipartContent(this.request); if (!isMultipart) { + // 如果请求不是多部分表单数据,说明没有文件上传相关内容,设置文件上传状态为 "NOFILE"(表示未包含文件上传域),然后直接返回,不再进行后续的文件上传操作流程。 this.state = this.errorInfo.get("NOFILE"); return; } + + // 创建一个DiskFileItemFactory对象,它用于配置文件上传过程中的一些基础设置,例如设置临时文件的存储位置等,这里后续会将其与ServletFileUpload结合使用来处理文件上传操作。 DiskFileItemFactory dff = new DiskFileItemFactory(); + // 获取文件的保存路径,通过调用getFolder方法根据配置的保存路径(this.savePath)以及当前日期生成具体的保存目录路径,确保文件按照日期进行分类保存,同时也处理了目录创建等相关操作。 String savePath = this.getFolder(this.savePath); + // 设置DiskFileItemFactory的临时文件存储目录,将其指定为前面获取到的保存路径对应的File对象,这样在文件上传过程中如果需要临时存储文件(例如文件较大时分块处理等情况),会将临时文件存放在这个目录下。 dff.setRepository(new File(savePath)); + try { + // 创建一个ServletFileUpload对象,用于处理文件上传的具体操作,将前面配置好的DiskFileItemFactory对象传入,使其基于配置好的基础设置来进行文件上传相关的处理工作,如解析请求、获取文件项等。 ServletFileUpload sfu = new ServletFileUpload(dff); + // 设置文件上传的最大允许大小,通过将以KB为单位的maxSize乘以1024转换为字节单位,限制上传文件的大小不能超过这个设定值,若超过则会抛出SizeLimitExceededException异常进行相应的错误处理。 sfu.setSizeMax(this.maxSize * 1024); + // 设置请求头的编码格式为 "utf-8",确保在处理包含中文等特殊字符的文件名等信息时能够正确解析,避免出现乱码问题,保证文件上传过程中字符编码的一致性。 sfu.setHeaderEncoding("utf-8"); + + // 通过ServletFileUpload的getItemIterator方法获取一个FileItemIterator对象,它用于迭代请求中的文件项(包括上传的文件以及表单中的其他字段信息等),方便后续逐个进行处理。 FileItemIterator fii = sfu.getItemIterator(this.request); while (fii.hasNext()) { + // 获取下一个文件项,返回的FileItemStream对象可以用于获取文件项的相关信息(如文件名、文件内容流等)以及判断是否为表单字段等操作,是处理文件上传过程中每个文件或字段的关键对象。 FileItemStream fis = fii.next(); if (!fis.isFormField()) { + // 如果当前文件项不是表单字段(即代表是一个真正要上传的文件),则进行以下文件上传相关的处理操作。 + + // 获取上传文件的原始文件名,通过截取文件名中最后一个文件分隔符(根据系统的文件分隔符来获取,通过System.getProperty("file.separator")获取系统对应的文件分隔符)之后的部分作为原始文件名, + // 去除了文件的路径信息,只保留文件名本身,方便后续处理和记录。 this.originalName = fis.getName().substring(fis.getName().lastIndexOf(System.getProperty("file.separator")) + 1); + // 检查文件的格式是否符合允许的文件格式列表(allowFiles),调用checkFileType方法进行检查,如果不符合则设置文件上传状态为 "TYPE"(表示不允许的文件格式),并跳过当前文件的后续上传操作,继续处理下一个文件项(如果有的话)。 if (!this.checkFileType(this.originalName)) { this.state = this.errorInfo.get("TYPE"); continue; } + + // 生成新的文件名,调用getName方法根据原始文件名生成一个带有随机数、时间戳等信息的新文件名,以保证文件名的唯一性和避免文件名冲突等问题,同时将新文件名赋值给fileName成员变量。 this.fileName = this.getName(this.originalName); + // 获取文件的类型,通过调用getFileExt方法截取文件名的后缀部分(从最后一个 "." 开始截取到末尾)作为文件类型,例如 ".jpg"、".pdf" 等,并赋值给type成员变量,方便后续记录和使用。 this.type = this.getFileExt(this.fileName); + // 构建文件的输出文件地址(即文件最终保存的路径),将保存路径(savePath)和新生成的文件名(fileName)组合起来,形成完整的文件保存路径,赋值给url成员变量,用于后续保存文件时确定保存位置。 this.url = savePath + "/" + this.fileName; + + // 创建一个缓冲输入流(BufferedInputStream),用于读取上传文件的内容,通过调用fis.openStream方法打开文件项对应的输入流,并将其包装为缓冲输入流,提高文件读取效率,准备将文件内容读取出来进行保存操作。 BufferedInputStream in = new BufferedInputStream(fis.openStream()); + // 根据前面构建的文件保存路径创建一个对应的File对象,用于表示要保存的目标文件,方便后续通过文件输出流将读取到的文件内容写入到这个文件中,完成文件的保存操作。 File file = new File(this.getPhysicalPath(this.url)); - FileOutputStream out = new FileOutputStream( file ); + // 创建一个文件输出流(FileOutputStream),用于将文件内容写入到目标文件中,将前面创建的File对象作为参数传入,初始化文件输出流对象,准备进行文件写入操作。 + FileOutputStream out = new FileOutputStream(file); + // 创建一个缓冲输出流(BufferedOutputStream),将前面的文件输出流包装起来,进一步提高文件写入的效率和稳定性,通过缓冲机制优化文件输出操作,减少频繁的磁盘I/O操作次数。 BufferedOutputStream output = new BufferedOutputStream(out); + // 使用Streams工具类(来自Apache Commons FileUpload组件)的copy方法,将输入流(in,即读取的上传文件内容)中的数据复制到输出流(output,即要写入到目标文件的输出流)中,实现文件的保存操作, + // 最后一个参数true表示在复制完成后自动关闭输入流和输出流,释放相关资源,确保文件内容正确地从客户端上传并保存到服务器指定的位置。 Streams.copy(in, output, true); - this.state=this.errorInfo.get("SUCCESS"); + + // 如果文件上传成功,设置文件上传状态为 "SUCCESS"(表示成功),通过从errorInfo这个HashMap中获取对应的成功状态描述字符串来设置,告知调用者文件上传操作顺利完成。 + this.state = this.errorInfo.get("SUCCESS"); + // 获取并记录上传文件的大小,通过获取保存后的文件的长度(单位为字节),赋值给size成员变量,方便后续获取文件大小信息进行相关的展示或其他业务操作。 this.size = file.length(); - //UE中只会处理单张上传,完成后即退出 + + // 在UE(UEditor,一种富文本编辑器,推测此处是针对其文件上传逻辑进行的代码编写)中按只处理单张上传的逻辑,所以一旦成功上传一个文件后,就直接退出循环,不再处理后续的文件项(如果还有的话)。 break; } else { + // 如果当前文件项是表单字段,则进行以下处理操作(当前代码中仅处理名为 "pictitle" 的字段作为标题字段,对于其他表单字段可以根据业务需求自行扩展处理逻辑)。 + + // 获取表单字段的名称,通过调用fis.getFieldName方法获取字段的名称,用于后续判断是否是需要处理的特定字段(如这里的 "pictitle")。 String fname = fis.getFieldName(); - //只处理title,其余表单请自行处理 - if(!fname.equals("pictitle")){ + // 判断表单字段名称是否等于 "pictitle",如果不等于,则直接跳过当前字段,继续处理下一个文件项,因为当前代码只关注这个特定的标题字段进行处理。 + if (!fname.equals("pictitle")) { continue; } - BufferedInputStream in = new BufferedInputStream(fis.openStream()); - BufferedReader reader = new BufferedReader(new InputStreamReader(in)); - StringBuffer result = new StringBuffer(); - while (reader.ready()) { - result.append((char)reader.read()); - } - this.title = new String(result.toString().getBytes(),"utf-8"); - reader.close(); - - } - } - } catch (SizeLimitExceededException e) { - this.state = this.errorInfo.get("SIZE"); - } catch (InvalidContentTypeException e) { - this.state = this.errorInfo.get("ENTYPE"); - } catch (FileUploadException e) { - this.state = this.errorInfo.get("REQUEST"); - } catch (Exception e) { - this.state = this.errorInfo.get("UNKNOWN"); - } - } - - /** - * 接受并保存以base64格式上传的文件 - * @param fieldName - */ - public void uploadBase64(String fieldName){ - String savePath = this.getFolder(this.savePath); - String base64Data = this.request.getParameter(fieldName); - this.fileName = this.getName("test.png"); - this.url = savePath + "/" + this.fileName; - try { - File outFile = new File(this.getPhysicalPath(this.url)); - OutputStream ro = new FileOutputStream(outFile); - byte[] b = Base64.decodeBase64(base64Data); - for (int i = 0; i < b.length; ++i) { - if (b[i] < 0) { - b[i] += 256; - } - } - ro.write(b); - ro.flush(); - ro.close(); - this.state=this.errorInfo.get("SUCCESS"); - } catch (Exception e) { - this.state = this.errorInfo.get("IO"); - } - } - - /** - * 文件类型判断 - * - * @param fileName - * @return - */ - private boolean checkFileType(String fileName) { - Iterator type = Arrays.asList(this.allowFiles).iterator(); - while (type.hasNext()) { - String ext = type.next(); - if (fileName.toLowerCase().endsWith(ext)) { - return true; - } - } - return false; - } - - /** - * 获取文件扩展名 - * - * @return string - */ - private String getFileExt(String fileName) { - return fileName.substring(fileName.lastIndexOf(".")); - } - - /** - * 依据原始文件名生成新文件名 - * @return - */ - private String getName(String fileName) { - Random random = new Random(); - return this.fileName = "" + random.nextInt(10000) - + System.currentTimeMillis() + this.getFileExt(fileName); - } - - /** - * 根据字符串创建本地目录 并按照日期建立子目录返回 - * @param path - * @return - */ - private String getFolder(String path) { - SimpleDateFormat formater = new SimpleDateFormat("yyyyMMdd"); - path += "/" + formater.format(new Date()); - File dir = new File(this.getPhysicalPath(path)); - if (!dir.exists()) { - try { - dir.mkdirs(); - } catch (Exception e) { - this.state = this.errorInfo.get("DIR"); - return ""; - } - } - return path; - } - - /** - * 根据传入的虚拟路径获取物理路径 - * - * @param path - * @return - */ - private String getPhysicalPath(String path) { - String servletPath = this.request.getServletPath(); - String realPath = this.request.getSession().getServletContext() - .getRealPath(servletPath); - return new File(realPath).getParent() +"/" +path; - } - - public void setSavePath(String savePath) { - this.savePath = savePath; - } - - public void setAllowFiles(String[] allowFiles) { - this.allowFiles = allowFiles; - } - - public void setMaxSize(int size) { - this.maxSize = size; - } - - public long getSize() { - return this.size; - } - - public String getUrl() { - return this.url; - } - - public String getFileName() { - return this.fileName; - } - - public String getState() { - return this.state; - } - - public String getTitle() { - return this.title; - } - - public String getType() { - return this.type; - } - - public String getOriginalName() { - return this.originalName; - } -} + // 创建一个缓冲输入流(BufferedInputStream),用于读取表单字段对应的输入流内容(这里假设表单字段中的内容是以流的形式传递的,例如文本信息等),通过fis.openStream方法打开输入流并包装为缓冲输入流,方便后续读取操作。 + BufferedInputStream in = new BufferedInputStream(fis.openStream()); + // 创建一个BufferedReader对象,将前面的缓冲输入流包装为字符流读取器,通过指定字符编码为 "utf-8"( \ No newline at end of file diff --git a/tamguo/src/main/java/com/tamguo/web/BookController.java b/tamguo/src/main/java/com/tamguo/web/BookController.java index 45911a2..015c3bb 100644 --- a/tamguo/src/main/java/com/tamguo/web/BookController.java +++ b/tamguo/src/main/java/com/tamguo/web/BookController.java @@ -19,40 +19,86 @@ import com.tamguo.service.IChapterService; import com.tamguo.service.ICourseService; import com.tamguo.service.ISubjectService; +// 标识这是一个Spring的控制器类,用于处理与书籍相关的Web请求,并返回相应的视图或数据 @Controller public class BookController { + // 自动注入IBookService,用于处理书籍相关的业务逻辑,比如根据ID查询书籍等操作 @Autowired IBookService iBookService; + + // 自动注入IChapterService,用于处理章节相关的业务逻辑,例如查询某书籍下的章节列表等操作 @Autowired IChapterService iChapterService; + + // 自动注入ISubjectService,用于处理学科相关的业务逻辑,像根据学科ID查询学科信息等操作 @Autowired ISubjectService iSubjectService; + + // 自动注入ICourseService,用于处理课程相关的业务逻辑,例如查询课程信息等操作 @Autowired ICourseService iCourseService; - + + /** + * 处理书籍详情页面的GET请求,根据传入的书籍唯一标识符(uid),查询并获取与该书籍相关的各种信息(如所属学科、课程、同课程下的其他书籍、自身的章节列表等), + * 将这些信息添加到ModelAndView对象中,设置视图名称为"book",最后返回该ModelAndView对象用于渲染相应的书籍详情页面视图给用户。 + * 如果在查询过程中出现异常,则将视图名称设置为"404",返回相应的ModelAndView对象,表示出现错误情况(可能展示404页面给用户)。 + * + * @param uid 书籍的唯一标识符,用于定位要展示详情的具体书籍 + * @param model 用于传递数据到视图的ModelAndView对象,在这里将各种相关数据添加到该对象中 + * @return 返回包含视图名称和相关书籍及关联数据的ModelAndView对象,用于渲染相应的页面视图 + */ @SuppressWarnings("unchecked") @RequestMapping(value = {"book/{uid}"}, method = RequestMethod.GET) - public ModelAndView index(@PathVariable String uid , ModelAndView model) { + public ModelAndView index(@PathVariable String uid, ModelAndView model) { try { + // 根据传入的书籍ID,通过书籍服务查询并获取对应的书籍实体信息 BookEntity book = iBookService.selectById(uid); + + // 根据书籍所属的学科ID,通过学科服务查询并获取对应的学科实体信息 SubjectEntity subject = iSubjectService.find(book.getSubjectId()); + + // 获取该学科下包含的课程列表信息(假设SubjectEntity类中有相应的字段存储课程列表,这里获取该列表) List courseList = subject.getCourseList(); + + // 通过条件查询,查找与当前书籍属于同一课程的其他书籍列表,用于可能在页面展示相关推荐书籍等场景 List bookList = iBookService.selectList(Condition.create().eq("course_id", book.getCourseId())); + + // 根据书籍所属的课程ID,通过课程服务查询并获取对应的课程实体信息 CourseEntity course = iCourseService.selectById(book.getCourseId()); + + // 通过条件查询,获取当前书籍下的所有章节列表信息,用于在书籍详情页面展示书籍的章节结构等内容 List chapterList = iChapterService.selectList(Condition.create().eq("book_id", uid)); + + // 将查询到的书籍实体信息添加到ModelAndView对象中,以便在视图中可以获取并展示书籍的详细内容 model.addObject("book", book); + + // 将查询到的学科实体信息添加到ModelAndView对象中,以便在视图中展示书籍所属学科等相关信息 model.addObject("subject", subject); + + // 将查询到的课程实体信息添加到ModelAndView对象中,以便在视图中展示书籍所属课程等相关信息 model.addObject("course", course); - model.addObject("chapterList" , chapterList); + + // 将查询到的书籍章节列表信息添加到ModelAndView对象中,以便在视图中展示书籍的章节情况 + model.addObject("chapterList", chapterList); + + // 将查询到的同课程下的其他书籍列表信息添加到ModelAndView对象中,以便在视图中展示相关推荐书籍等内容 model.addObject("courseList", courseList); + + // 将查询到的本课程下的所有书籍列表信息添加到ModelAndView对象中,以便在视图中展示相关推荐书籍等内容 model.addObject("bookList", bookList); + + // 设置视图名称为"book",对应相应的书籍详情页面模板,后续会根据这个名称去查找并渲染对应的视图 model.setViewName("book"); + + // 返回包含视图名称和各种相关数据的ModelAndView对象,以便进行视图渲染并展示给用户 return model; } catch (Exception e) { + // 如果在查询相关数据过程中出现异常,将视图名称设置为"404",通常用于表示请求的资源不存在等错误情况 model.setViewName("404"); + // 返回包含错误视图名称的ModelAndView对象,可能会渲染出404页面展示给用户 return model; } } - -} + +} \ No newline at end of file diff --git a/tamguo/src/main/java/com/tamguo/web/CourseController.java b/tamguo/src/main/java/com/tamguo/web/CourseController.java index 6b1e23f..b15682e 100644 --- a/tamguo/src/main/java/com/tamguo/web/CourseController.java +++ b/tamguo/src/main/java/com/tamguo/web/CourseController.java @@ -10,9 +10,9 @@ import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.ResponseBody; /** * Controller - 科目 - * - * @author tamguo + * 该类主要作为Spring的控制器,用于处理与课程(Course)相关的Web请求,比如获取课程详情、课程下的章节信息等,并返回相应的视图或数据。 * + * @author tamguo */ import org.springframework.web.servlet.ModelAndView; @@ -27,54 +27,112 @@ import com.tamguo.service.ICourseService; import com.tamguo.service.ISubjectService; import com.tamguo.util.Result; +// 标识这是一个Spring的控制器类,用于处理课程相关的Web请求,并返回相应的视图或数据 @Controller public class CourseController { - + + // 自动注入IChapterService,用于处理章节相关的业务逻辑,例如查询课程对应的章节信息、获取章节树结构等操作 @Autowired IChapterService iChapterService; + + // 自动注入ICourseService,用于处理课程相关的业务逻辑,像根据课程ID查找课程、按学科ID查找课程列表等操作 @Autowired ICourseService iCourseService; + + // 自动注入ISubjectService,用于处理学科相关的业务逻辑,例如根据学科ID查找学科信息等操作 @Autowired ISubjectService iSubjectService; + + // 自动注入IBookService,用于处理书籍相关的业务逻辑,比如根据课程ID查询相关书籍列表等操作 @Autowired IBookService iBookService; + /** + * 处理课程详情页面的GET请求,根据传入的课程唯一标识符(uid),查询并获取与该课程相关的各种信息(如所属学科、包含的书籍列表、首本书籍、课程下的章节列表、同学科下的其他课程列表等), + * 将这些信息添加到ModelAndView对象中,设置视图名称为"chapter",最后返回该ModelAndView对象用于渲染相应的课程详情页面视图给用户。 + * 如果在查询过程中出现异常,则将视图名称设置为"404",返回相应的ModelAndView对象,表示出现错误情况(可能展示404页面给用户)。 + * + * @param uid 课程的唯一标识符,用于定位要展示详情的具体课程 + * @param model 用于传递数据到视图的ModelAndView对象,在这里将各种相关数据添加到该对象中 + * @return 返回包含视图名称和相关课程及关联数据的ModelAndView对象,用于渲染相应的页面视图 + */ @SuppressWarnings("unchecked") @RequestMapping(value = {"course/{uid}"}, method = RequestMethod.GET) - public ModelAndView index(@PathVariable String uid , ModelAndView model) { + public ModelAndView index(@PathVariable String uid, ModelAndView model) { try { + // 根据传入的课程ID,通过课程服务查询并获取对应的课程实体信息 CourseEntity course = iCourseService.find(uid); + + // 通过条件查询,查找与当前课程相关的所有书籍列表,可能用于展示该课程下有哪些相关书籍等场景 List bookList = iBookService.selectList(Condition.create().eq("course_id", uid)); + + // 获取书籍列表中的第一本图书实体信息(这里假设取第一本作为某种默认展示或后续处理的依据,具体业务逻辑可能需根据实际情况调整) BookEntity book = bookList.get(0); + + // 根据课程所属的学科ID,通过学科服务查询并获取对应的学科实体信息 SubjectEntity subject = iSubjectService.find(course.getSubjectId()); + + // 通过章节服务,查询并获取当前课程对应的章节列表信息,用于在课程详情页面展示课程包含的章节内容等 List chapterList = iChapterService.findCourseChapter(book.getUid()); + + // 通过课程服务,查找与当前课程属于同一学科的其他课程列表,可能用于相关课程推荐等场景 List courseList = iCourseService.findBySubjectId(course.getSubjectId()); - + + // 将查询到的课程章节列表信息添加到ModelAndView对象中,以便在视图中展示课程的章节情况 model.addObject("chapterList", chapterList); + + // 将查询到的同学科下的其他课程列表信息添加到ModelAndView对象中,以便在视图中展示相关课程推荐等内容 model.addObject("courseList", courseList); + + // 将查询到的课程实体信息添加到ModelAndView对象中,以便在视图中展示课程的详细信息 model.addObject("course", course); + + // 将查询到的学科实体信息添加到ModelAndView对象中,以便在视图中展示课程所属学科等相关信息 model.addObject("subject", subject); + + // 将查询到的课程相关的书籍列表信息添加到ModelAndView对象中,以便在视图中展示课程包含的书籍等内容 model.addObject("bookList", bookList); - model.addObject("book" , book); - + + // 将获取到的首本图书实体信息添加到ModelAndView对象中(具体用途根据业务而定) + model.addObject("book", book); + + // 设置视图名称为"chapter",对应相应的课程详情页面模板,后续会根据这个名称去查找并渲染对应的视图 model.setViewName("chapter"); + + // 返回包含视图名称和各种相关数据的ModelAndView对象,以便进行视图渲染并展示给用户 return model; } catch (Exception e) { + // 如果在查询相关数据过程中出现异常,将视图名称设置为"404",通常用于表示请求的资源不存在等错误情况 model.setViewName("404"); + // 返回包含错误视图名称的ModelAndView对象,可能会渲染出404页面展示给用户 return model; } } - + + /** + * 处理根据课程ID查找课程章节的GET请求,直接调用章节服务的相应方法,根据传入的课程ID查找对应的章节列表信息,并将查找结果(章节列表)返回给前端。 + * 该方法返回的是具体的章节实体列表,方便前端直接使用这些数据进行展示或其他相关操作。 + * + * @param courseId 课程的唯一标识符,用于定位要查找章节的具体课程 + * @return 返回包含指定课程下所有章节实体信息的List列表,若未找到则返回空列表 + */ @RequestMapping(value = {"course/findChapter"}, method = RequestMethod.GET) @ResponseBody - public List findChapterByCourseId(String courseId){ + public List findChapterByCourseId(String courseId) { return iChapterService.findCourseChapter(courseId); } - + + /** + * 处理根据课程ID查找课程章节树的GET请求,调用章节服务的相应方法获取指定课程ID对应的章节树结构数据, + * 将获取到的数据包装在表示成功的Result对象中返回给前端,方便前端根据统一的格式进行解析和展示章节树结构。 + * + * @param courseId 课程的唯一标识符,用于定位要查找章节树的具体课程 + * @return 返回包含章节树结构数据的Result对象,若成功获取则Result对象中的数据字段包含相应的章节树信息,若出现问题则可能包含相应的错误提示等信息 + */ @RequestMapping(value = {"course/findChapterTreeByCourseId"}, method = RequestMethod.GET) @ResponseBody - public Result findChapterTreeByCourseId(String courseId){ + public Result findChapterTreeByCourseId(String courseId) { return Result.successResult(iChapterService.getChapterTree(courseId)); } - -} + +} \ No newline at end of file diff --git a/tamguo/src/main/java/com/tamguo/web/FileUploadController.java b/tamguo/src/main/java/com/tamguo/web/FileUploadController.java index 47f678d..1a17e62 100644 --- a/tamguo/src/main/java/com/tamguo/web/FileUploadController.java +++ b/tamguo/src/main/java/com/tamguo/web/FileUploadController.java @@ -24,72 +24,113 @@ import com.tamguo.util.DateUtils; /** * 文件上传 + * 该类作为一个Spring的控制器,主要负责处理文件上传相关的功能,接收前端上传的文件,将其保存到服务器指定的位置,并返回相应的上传结果信息给前端。 */ @RestController public class FileUploadController { - + + // 创建日志记录器,用于记录文件上传过程中的关键信息,例如文件保存的位置、上传是否成功等情况,方便后续查看日志进行调试和问题排查 private Logger logger = org.slf4j.LoggerFactory.getLogger(getClass()); + + // 通过配置文件注入文件存储路径属性值,指定了服务器上用于存放上传文件的基础路径,后续会根据日期等规则在此基础上构建具体的文件存储目录 @Value("${file.storage.path}") private String fileStoragePath; + + // 自动注入CacheService,用于缓存相关操作,在这里主要是借助缓存来生成具有唯一性且按一定规则递增的文件编号(文件名的一部分) @Autowired private CacheService cacheService; + + // 定义文件编号的默认格式化字符串,用于格式化生成的文件编号,保证编号格式统一,使其具有固定的长度和格式要求 private static final String FILES_NO_FORMAT = "00000"; + // 定义文件编号的前缀,方便识别文件相关的编号,使其在整体文件名中有一定的标识性,便于区分不同类型或用途的文件 private static final String FILES_PREFIX = "FP"; - + + /** + * 处理文件上传的POST请求,接收前端传来的MultipartFile类型的文件,进行文件保存到服务器的操作,若保存成功则返回表示成功的Ueditor对象,包含文件相关信息; + * 若出现异常或文件为空等情况,则返回相应的表示错误的Ueditor对象给前端,告知上传结果。 + * + * @param upfile 前端上传的MultipartFile类型文件,包含了文件的内容以及相关的元数据(如文件名、文件类型等) + * @return 返回Ueditor对象,该对象包含了文件上传的状态(SUCCESS或ERROR)、文件名以及文件在服务器上的访问路径等信息,前端可根据这些信息进行相应的展示或后续处理 + * @throws IOException 如果在文件读写等操作过程中出现I/O异常则抛出,例如读取文件输入流、写入文件输出流时可能出现的异常情况 + */ @RequestMapping(value = "/uploadFile", method = RequestMethod.POST) @ResponseBody - public Ueditor imgUpload(MultipartFile upfile) throws IOException { - if (!upfile.isEmpty()) { + public Ueditor imgUpload(MultipartFile upfile) throws IOException { + // 判断上传的文件是否为空,如果为空则直接返回表示文件为空的错误信息的Ueditor对象给前端 + if (!upfile.isEmpty()) { InputStream in = null; OutputStream out = null; - + try { + // 根据当前日期构建文件存储的路径,将配置的基础存储路径与格式化后的当前日期(格式为yyyyMMdd)拼接起来,方便按日期分类存储文件,便于管理和查找 String path = fileStoragePath + DateUtils.format(new Date(), "yyyyMMdd"); File dir = new File(path); + // 如果目录不存在,则创建相应的目录,确保文件有存储的位置,避免后续保存文件时因目录不存在而出现错误 if (!dir.exists()) dir.mkdirs(); + + // 生成文件名,结合通过缓存获取的文件编号以及原文件名的后缀组成完整的文件名,保证文件名的唯一性以及符合一定的命名规则 String fileName = this.getTeacherNo() + upfile.getOriginalFilename().substring(upfile.getOriginalFilename().lastIndexOf(".")); + // 创建表示服务器上要保存的文件的File对象,指定其完整的保存路径和文件名 File serverFile = new File(dir + File.separator + fileName); + // 获取文件输入流,用于读取上传文件的内容,以便后续将内容写入到服务器的文件中 in = upfile.getInputStream(); + // 创建文件输出流,用于将读取的文件内容写入到服务器指定的存储位置,即上面创建的serverFile所代表的文件中 out = new FileOutputStream(serverFile); + byte[] b = new byte[1024]; int len = 0; + // 通过循环读取输入流中的文件内容,并写入到输出流中,实现文件的复制保存操作,将上传的文件从临时存储位置复制到服务器指定的存储位置 while ((len = in.read(b)) > 0) { out.write(b, 0, len); } + // 关闭输出流,释放相关资源,避免资源泄露 out.close(); + // 关闭输入流,释放相关资源,避免资源泄露 in.close(); + // 记录文件在服务器上的存储位置信息到日志中,方便后续查看和排查问题,例如确认文件是否保存成功以及保存的具体位置是否正确等 logger.info("Server File Location=" + serverFile.getAbsolutePath()); - Ueditor ueditor = new Ueditor(); - ueditor.setState("SUCCESS"); - ueditor.setTitle(upfile.getOriginalFilename()); - ueditor.setUrl("files" + "/" +DateUtils.format(new Date(), "yyyyMMdd") + "/" + fileName); - return ueditor; + // 创建表示文件上传成功的Ueditor对象,并设置相应的成功状态、原文件名(作为标题)以及文件在服务器上的访问路径等信息 + Ueditor ueditor = new Ueditor(); + ueditor.setState("SUCCESS"); + ueditor.setTitle(upfile.getOriginalFilename()); + ueditor.setUrl("files" + "/" + DateUtils.format(new Date(), "yyyyMMdd") + "/" + fileName); + return ueditor; } catch (Exception e) { + // 如果在文件上传过程中出现异常,创建表示文件上传失败的Ueditor对象,并设置相应的错误状态和提示信息(这里统一设置为"上传失败"),然后返回该对象告知前端上传失败 Ueditor ueditor = new Ueditor(); - ueditor.setState("ERROR"); - ueditor.setTitle("上传失败"); - return ueditor; + ueditor.setState("ERROR"); + ueditor.setTitle("上传失败"); + return ueditor; } finally { - if (out != null) { + // 在最终块中确保输出流资源被正确关闭,避免资源泄露,即使在前面出现异常的情况下也能保证资源的正确释放 + if (out!= null) { out.close(); out = null; } - if (in != null) { + // 在最终块中确保输入流资源被正确关闭,避免资源泄露,即使在前面出现异常的情况下也能保证资源的正确释放 + if (in!= null) { in.close(); in = null; } } } else { + // 如果上传的文件为空,创建表示文件为空的错误的Ueditor对象,并设置相应的错误状态和提示信息,然后返回该对象告知前端文件为空 Ueditor ueditor = new Ueditor(); - ueditor.setState("ERROR"); - ueditor.setTitle("File is empty"); - return ueditor; + ueditor.setState("ERROR"); + ueditor.setTitle("File is empty"); + return ueditor; } - } - + } + + /** + * 生成文件编号的方法,先根据当前日期格式化得到年月格式的字符串作为缓存的键前缀,然后通过缓存服务获取自增的编号,再按照指定格式进行格式化,最后拼接前缀生成完整的文件编号。 + * 这样生成的文件编号具有唯一性且按照一定规则递增,方便对文件进行管理和识别。 + * + * @return 返回生成的文件编号字符串,用于组成文件名的一部分,保证文件名的唯一性和规范性 + */ private String getTeacherNo() { SimpleDateFormat sdf = new SimpleDateFormat("yyyyMM"); String format = sdf.format(new Date()); @@ -100,4 +141,4 @@ public class FileUploadController { return avatorNo; } -} +} \ No newline at end of file diff --git a/tamguo/src/main/java/com/tamguo/web/IndexController.java b/tamguo/src/main/java/com/tamguo/web/IndexController.java index d3dda61..0d5b2e7 100644 --- a/tamguo/src/main/java/com/tamguo/web/IndexController.java +++ b/tamguo/src/main/java/com/tamguo/web/IndexController.java @@ -5,36 +5,71 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.servlet.ModelAndView; +// 标识这是一个Spring的控制器类,用于处理不同路径的Web请求,并返回相应的视图给客户端展示不同的页面内容。 @Controller public class IndexController { - - @RequestMapping(value = "/", method = RequestMethod.GET) + + /** + * 处理根路径("/")的GET请求,将视图名称设置为"index",意味着会查找并渲染名为"index"的视图模板(例如JSP、Thymeleaf等模板文件,具体取决于项目配置)返回给客户端, + * 一般用于展示网站的首页内容。 + * + * @param model 用于传递数据到视图的ModelAndView对象,此处仅设置了视图名称,暂未添加额外数据。 + * @return 返回包含视图名称的ModelAndView对象,以便Spring的视图解析器根据该名称查找并渲染相应的视图。 + */ + @RequestMapping(value = "/", method = RequestMethod.GET) public ModelAndView indexAction(ModelAndView model) { - model.setViewName("index"); + model.setViewName("index"); return model; } - + + /** + * 处理"/index"路径的GET请求,同样将视图名称设置为"index",和上面的根路径请求类似,也是用于展示网站的首页内容,可能是为了兼容不同用户访问首页时使用不同的路径形式。 + * + * @param model 用于传递数据到视图的ModelAndView对象,此处仅设置了视图名称,暂未添加额外数据。 + * @return 返回包含视图名称的ModelAndView对象,以便Spring的视图解析器根据该名称查找并渲染相应的视图。 + */ @RequestMapping(value = "/index", method = RequestMethod.GET) public ModelAndView mainAction(ModelAndView model) { - model.setViewName("index"); + model.setViewName("index"); return model; } - + + /** + * 处理"/baidu_verify_5agfTbCO3Q"路径的GET请求,将视图名称设置为"thirdparty/baidu_verify_5agfTbCO3Q", + * 意味着会查找并渲染对应的视图模板(可能是用于百度验证相关的页面,具体取决于业务需求)返回给客户端,用于满足百度相关验证服务对特定页面的要求。 + * + * @param model 用于传递数据到视图的ModelAndView对象,此处仅设置了视图名称,暂未添加额外数据。 + * @return 返回包含视图名称的ModelAndView对象,以便Spring的视图解析器根据该名称查找并渲染相应的视图。 + */ @RequestMapping(value = "/baidu_verify_5agfTbCO3Q", method = RequestMethod.GET) public ModelAndView baidu_verify_5agfTbCO3Q(ModelAndView model) { - model.setViewName("thirdparty/baidu_verify_5agfTbCO3Q"); + model.setViewName("thirdparty/baidu_verify_5agfTbCO3Q"); return model; } - + + /** + * 处理"/baidu_verify_iAm7387J0l"路径的GET请求,将视图名称设置为"thirdparty/baidu_verify_iAm7387J0l", + * 意味着会查找并渲染对应的视图模板(同样可能是用于百度验证相关的页面,具体取决于业务需求)返回给客户端,用于满足百度相关验证服务对特定页面的要求。 + * + * @param model 用于传递数据到视图的ModelAndView对象,此处仅设置了视图名称,暂未添加额外数据。 + * @return 返回包含视图名称的ModelAndView对象,以便Spring的视图解析器根据该名称查找并渲染相应的视图。 + */ @RequestMapping(value = "/baidu_verify_iAm7387J0l", method = RequestMethod.GET) public ModelAndView baidu_verify_iAm7387J0l(ModelAndView model) { - model.setViewName("thirdparty/baidu_verify_iAm7387J0l"); + model.setViewName("thirdparty/baidu_verify_iAm7387J0l"); return model; } - + + /** + * 处理"/sogousiteverification"路径的GET请求,将视图名称设置为"thirdparty/sogousiteverification", + * 意味着会查找并渲染对应的视图模板(可能是用于搜狗网站验证相关的页面,具体取决于业务需求)返回给客户端,用于满足搜狗相关验证服务对特定页面的要求。 + * + * @param model 用于传递数据到视图的ModelAndView对象,此处仅设置了视图名称,暂未添加额外数据。 + * @return 返回包含视图名称的ModelAndView对象,以便Spring的视图解析器根据该名称查找并渲染相应的视图。 + */ @RequestMapping(value = "/sogousiteverification", method = RequestMethod.GET) public ModelAndView sogousiteverification(ModelAndView model) { - model.setViewName("thirdparty/sogousiteverification"); + model.setViewName("thirdparty/sogousiteverification"); return model; } -} +} \ No newline at end of file diff --git a/tamguo/src/main/java/com/tamguo/web/LoginController.java b/tamguo/src/main/java/com/tamguo/web/LoginController.java index aece24b..65f5807 100644 --- a/tamguo/src/main/java/com/tamguo/web/LoginController.java +++ b/tamguo/src/main/java/com/tamguo/web/LoginController.java @@ -27,94 +27,176 @@ import com.google.code.kaptcha.Producer; import com.tamguo.util.Result; import com.tamguo.util.ShiroUtils; +// 标识这是一个Spring的控制器类,主要用于处理用户登录相关的Web请求,包括生成验证码、展示登录页面以及处理登录提交等操作。 @Controller public class LoginController { - + + // 自动注入Producer,用于生成验证码相关的操作,比如创建验证码文本和对应的图片验证码。 @Autowired private Producer producer; + /** + * 处理生成验证码图片的请求,该请求对应的路径为 "captcha.jpg",用于生成并返回一个图片格式的验证码给客户端显示。 + * 主要步骤包括设置响应头信息(禁止缓存)、指定响应内容类型为图片(JPEG格式),生成验证码文本和对应的图片, + * 将验证码文本保存到Shiro的会话中以便后续验证,最后将图片通过输出流输出给客户端。 + * + * @param response HttpServletResponse对象,用于设置响应相关的信息,如响应头、输出流等,以便向客户端返回验证码图片。 + * @throws ServletException 如果在Servlet处理过程中出现异常则抛出,例如设置响应头、获取输出流等操作出现问题时可能抛出此异常。 + * @throws IOException 如果在读写图片数据到输出流等I/O操作过程中出现异常则抛出。 + */ @RequestMapping("captcha.jpg") public void captcha(HttpServletResponse response) throws ServletException, IOException { + // 设置响应头,禁止客户端缓存验证码图片,确保每次获取的验证码都是新生成的。 response.setHeader("Cache-Control", "no-store, no-cache"); + // 设置响应的内容类型为JPEG格式的图片,告知客户端返回的数据是图片类型,以便正确解析和显示。 response.setContentType("image/jpeg"); - // 生成文字验证码 + + // 生成验证码的文本内容,通常是由数字、字母等组成的随机字符串,具体生成规则由所使用的验证码生成组件(这里是Producer)决定。 String text = producer.createText(); - // 生成图片验证码 + // 根据生成的验证码文本创建对应的图片验证码,生成的是一个BufferedImage类型的图片对象,包含了验证码文本的可视化表示形式。 BufferedImage image = producer.createImage(text); - // 保存到shiro session + + // 将生成的验证码文本保存到Shiro的会话中,以Constants.KAPTCHA_SESSION_KEY作为键,方便后续在验证用户输入的验证码时从会话中获取并比对。 ShiroUtils.setSessionAttribute(Constants.KAPTCHA_SESSION_KEY, text); + + // 获取ServletResponse的输出流,用于将生成的图片数据写入到响应中,发送给客户端进行显示。 ServletOutputStream out = response.getOutputStream(); + // 使用ImageIO将BufferedImage格式的图片以JPEG格式写入到输出流中,实现将验证码图片返回给客户端的功能。 ImageIO.write(image, "jpg", out); } - + + /** + * 处理登录页面的GET请求,设置视图名称为"login",意味着会查找并渲染名为"login"的视图模板(例如JSP、Thymeleaf等模板文件,具体取决于项目配置)返回给客户端展示登录页面, + * 同时向视图模型中添加一个名为"isVerifyCode",值为"0"的属性,可能用于前端页面判断是否显示验证码相关的逻辑(具体用途根据前端代码确定)。 + * + * @param model 用于传递数据到视图的ModelAndView对象,在这里向其添加了登录页面相关的数据(如是否显示验证码的标识)。 + * @return 返回包含视图名称和相关数据的ModelAndView对象,以便Spring的视图解析器根据该名称查找并渲染相应的视图展示给用户。 + */ @RequestMapping(value = "/login", method = RequestMethod.GET) - public ModelAndView login(ModelAndView model){ + public ModelAndView login(ModelAndView model) { model.setViewName("login"); - model.addObject("isVerifyCode" , "0"); + model.addObject("isVerifyCode", "0"); return model; } - + + /** + * 处理用户提交登录信息的POST请求,接收用户名、密码和验证码等参数,进行登录验证相关的一系列操作,包括验证码验证、Shiro框架的登录认证等, + * 根据验证结果设置相应的提示信息,并返回登录页面或者重定向到登录成功后的页面(如果登录成功)。 + * + * @param username 用户输入的用户名,用于登录认证。 + * @param password 用户输入的密码,用于登录认证。 + * @param verifyCode 用户输入的验证码,用于与生成并保存在会话中的验证码进行比对验证。 + * @param model 用于传递数据到视图的ModelAndView对象,根据登录验证结果向其添加相应的提示信息、用户名等数据,以便在登录页面展示给用户。 + * @param session 当前的HttpSession对象,用于获取或设置会话相关的属性,如保存登录后的用户信息等操作。 + * @param response HttpServletResponse对象,用于在登录成功时进行重定向操作,将用户引导到登录成功后的页面。 + * @throws IOException 如果在重定向等操作过程中出现I/O异常则抛出,例如向客户端发送重定向响应时可能出现的异常情况。 + */ @RequestMapping(value = "/submitLogin", method = RequestMethod.POST) - public ModelAndView submitLogin(String username , String password , String verifyCode , ModelAndView model , HttpSession session , HttpServletResponse response) throws IOException{ + public ModelAndView submitLogin(String username, String password, String verifyCode, ModelAndView model, + HttpSession session, HttpServletResponse response) throws IOException { + // 创建一个表示登录结果的Result对象,初始化为成功状态(这里先设置为成功,后续根据实际验证情况进行修改),其内容暂时为空。 Result result = Result.successResult(null); - if(StringUtils.isEmpty(verifyCode)) { + + // 判断用户输入的验证码是否为空,如果为空则表示用户未输入验证码,设置相应的错误提示信息到Result对象中。 + if (StringUtils.isEmpty(verifyCode)) { result = Result.result(202, null, "请输入验证码"); - } else if(StringUtils.isNotEmpty(verifyCode)){ + } else if (StringUtils.isNotEmpty(verifyCode)) { + // 如果用户输入了验证码,从Shiro会话中获取之前生成并保存的验证码文本,用于和用户输入的验证码进行比对验证。 String kaptcha = ShiroUtils.getKaptcha(Constants.KAPTCHA_SESSION_KEY); + // 忽略大小写比较用户输入的验证码和会话中的验证码是否一致,如果不一致则表示验证码错误,设置相应的错误提示信息到Result对象中。 if (!verifyCode.equalsIgnoreCase(kaptcha)) { result = Result.result(205, null, "验证码错误"); } else { + // 如果验证码验证通过,获取Shiro的Subject对象,它代表了当前执行的用户主体,用于后续进行登录认证等操作。 Subject subject = ShiroUtils.getSubject(); + // 创建一个UsernamePasswordToken对象,将用户输入的用户名和密码封装进去,作为登录认证的凭证传递给Shiro框架。 UsernamePasswordToken token = new UsernamePasswordToken(username, password); try { + // 通过Subject对象发起登录认证操作,Shiro框架会根据配置的认证逻辑(如从数据库查询用户信息并比对密码等)进行验证。 subject.login(token); - + + // 如果登录成功,将登录后的用户信息保存到HttpSession中,方便在后续的请求处理中获取当前登录用户的相关信息,键为"currMember"。 session.setAttribute("currMember", ShiroUtils.getMember()); + // 重定向到登录成功后的页面(这里是"member/index.html",可能是会员相关的首页之类的页面,具体根据业务需求确定)。 response.sendRedirect("member/index.html"); + // 登录成功后直接返回null,因为已经进行了重定向操作,不需要再返回视图相关的内容了。 return null; } catch (UnknownAccountException e) { + // 如果Shiro认证过程中抛出UnknownAccountException异常,表示用户名不存在或者未找到对应的用户信息,设置相应的错误提示信息到Result对象中。 result = Result.result(201, null, "用户名或密码有误,请重新输入或找回密码"); } catch (IncorrectCredentialsException e) { + // 如果Shiro认证过程中抛出IncorrectCredentialsException异常,表示密码错误,设置相应的错误提示信息到Result对象中。 result = Result.result(202, null, "用户名或密码有误,请重新输入或找回密码"); } catch (LockedAccountException e) { + // 如果Shiro认证过程中抛出LockedAccountException异常,表示账号被锁定,设置相应的错误提示信息到Result对象中。 result = Result.result(203, null, "账号被锁定"); - } + } } - } - model.setViewName("login"); + } + + // 如果登录验证未通过(出现各种错误情况),设置视图名称为"login",表示返回登录页面展示给用户相应的错误提示信息。 + model.setViewName("login"); + // 将登录结果的状态码添加到视图模型中,方便前端页面根据状态码进行不同的提示信息展示等逻辑处理。 model.addObject("code", result.getCode()); - model.addObject("msg" , result.getMessage()); + // 将登录结果的提示信息添加到视图模型中,以便在登录页面展示给用户具体的错误原因等内容。 + model.addObject("msg", result.getMessage()); + // 将用户输入的用户名添加到视图模型中,可能用于前端页面保留用户之前输入的用户名,提升用户体验等用途(具体根据前端需求确定)。 model.addObject("username", username); return model; } - + + /** + * 处理小型登录(可能是用于移动端或者特定场景下简化的登录方式)的GET请求,接收用户名、密码和验证码等参数,进行登录验证相关的一系列操作, + * 根据验证结果返回相应的Result对象,包含登录成功后的用户信息(如果登录成功)或者错误提示信息(如果登录失败)。 + * 与submitLogin方法类似,也进行了验证码验证、Shiro框架的登录认证以及各种异常情况的处理,但这里是通过ResponseBody注解直接返回Result对象给客户端,而不是返回视图相关内容。 + * + * @param username 用户输入的用户名,用于登录认证。 + * @param password 用户输入的密码,用于登录认证。 + * @param captcha 用户输入的验证码,用于与生成并保存在会话中的验证码进行比对验证。 + * @param model 用于传递数据到视图的ModelAndView对象(这里在该方法中未实际使用到,可能是为了与其他类似方法保持参数形式一致而添加的)。 + * @param session 当前的HttpSession对象,用于获取或设置会话相关的属性,如保存登录后的用户信息等操作。 + * @return 返回表示登录结果的Result对象,包含登录成功后的用户信息或者相应的错误提示信息,客户端可根据返回结果进行后续的处理,如展示提示信息或者跳转到相应页面等。 + */ @RequestMapping(value = "/miniLogin", method = RequestMethod.GET) @ResponseBody - public Result miniLogin(String username , String password , String captcha, ModelAndView model , HttpSession session) { + public Result miniLogin(String username, String password, String captcha, ModelAndView model, + HttpSession session) { Result result = null; - if(StringUtils.isEmpty(captcha)) { + // 判断用户输入的验证码是否为空,如果为空则表示用户未输入验证码,设置相应的错误提示信息到Result对象中。 + if (StringUtils.isEmpty(captcha)) { result = Result.result(204, null, "请输入验证码"); - } else if(StringUtils.isNotEmpty(captcha)){ + } else if (StringUtils.isNotEmpty(captcha)) { + // 如果用户输入了验证码,从Shiro会话中获取之前生成并保存的验证码文本,用于和用户输入的验证码进行比对验证。 String kaptcha = ShiroUtils.getKaptcha(Constants.KAPTCHA_SESSION_KEY); + // 忽略大小写比较用户输入的验证码和会话中的验证码是否一致,如果不一致则表示验证码错误,设置相应的错误提示信息到Result对象中。 if (!captcha.equalsIgnoreCase(kaptcha)) { result = Result.result(205, null, "验证码错误"); - }else { + } else { + // 如果验证码验证通过,获取Shiro的Subject对象,它代表了当前执行的用户主体,用于后续进行登录认证等操作。 Subject subject = ShiroUtils.getSubject(); + // 创建一个UsernamePasswordToken对象,将用户输入的用户名和密码封装进去,作为登录认证的凭证传递给Shiro框架。 UsernamePasswordToken token = new UsernamePasswordToken(username, password); try { + // 通过Subject对象发起登录认证操作,Shiro框架会根据配置的认证逻辑(如从数据库查询用户信息并比对密码等)进行验证。 subject.login(token); + + // 如果登录成功,将登录后的用户信息保存到HttpSession中,方便在后续的请求处理中获取当前登录用户的相关信息,键为"currMember"。 session.setAttribute("currMember", ShiroUtils.getMember()); + // 创建表示登录成功的Result对象,并将登录后的用户信息设置到Result对象中,以便返回给客户端。 result = Result.successResult(ShiroUtils.getMember()); } catch (UnknownAccountException e) { + // 如果Shiro认证过程中抛出UnknownAccountException异常,表示用户名不存在或者未找到对应的用户信息,设置相应的错误提示信息到Result对象中。 result = Result.result(201, null, "用户名或密码有误,请重新输入或找回密码"); } catch (IncorrectCredentialsException e) { + // 如果Shiro认证过程中抛出IncorrectCredentialsException异常,表示密码错误,设置相应的错误提示信息到Result对象中。 result = Result.result(202, null, "用户名或密码有误,请重新输入或找回密码"); } catch (LockedAccountException e) { + // 如果Shiro认证过程中抛出LockedAccountException异常,表示账号被锁定,设置相应的错误提示信息到Result对象中。 result = Result.result(203, null, "账号被锁定"); - } + } } } return result; - } + } -} +} \ No newline at end of file diff --git a/tamguo/src/main/java/com/tamguo/web/PaperController.java b/tamguo/src/main/java/com/tamguo/web/PaperController.java index 2c26b6d..020913b 100644 --- a/tamguo/src/main/java/com/tamguo/web/PaperController.java +++ b/tamguo/src/main/java/com/tamguo/web/PaperController.java @@ -25,84 +25,191 @@ import com.tamguo.util.TamguoConstant; /** * Controller - 试卷 - * - * @author candy.tam + * 该类作为Spring的控制器,主要用于处理与试卷相关的各种Web请求,例如展示试卷列表页面、试卷详情页面,以及根据地区查找试卷等功能, + * 通过调用相应的服务层接口获取数据,并将数据添加到ModelAndView对象中返回给视图进行展示,或者直接以JSON格式返回数据给前端(如通过@ResponseBody注解的方法)。 * + * @author candy.tam */ @Controller public class PaperController { - + + // 自动注入ICourseService,用于处理课程相关的业务逻辑,比如根据课程ID查找课程、按学科ID查找课程列表等操作,在获取试卷相关信息时可能会涉及课程信息的查询。 @Autowired private ICourseService iCourseService; + + // 自动注入IAreaService,用于处理地区相关的业务逻辑,例如查找根地区列表等操作,可能在按地区筛选试卷等功能中会用到地区相关信息的查询。 @Autowired private IAreaService iAreaService; + + // 自动注入IPaperService,用于处理试卷相关的业务逻辑,像查找试卷、获取试卷列表、根据各种条件筛选试卷、获取推荐试卷等操作都是通过该服务来完成。 @Autowired private IPaperService iPaperService; + + // 自动注入IQuestionService,用于处理试题相关的业务逻辑,例如查找试卷对应的试题列表等操作,在试卷详情页面展示试题信息时会调用该服务。 @Autowired private IQuestionService iQuestionService; + + // 自动注入ISubjectService,用于处理学科相关的业务逻辑,例如根据学科ID查找学科信息等操作,试卷通常与特定学科相关,查询试卷相关信息时可能需要获取学科相关内容。 @Autowired private ISubjectService iSubjectService; + /** + * 处理试卷列表页面的GET请求,根据传入的多个路径变量(学科ID、课程ID、试卷类型、年份、地区、页码),查询并获取与这些条件相关的试卷列表及其他相关信息(如课程、学科、地区列表等), + * 将这些信息添加到ModelAndView对象中,设置视图名称为"paperlist",最后返回该ModelAndView对象用于渲染相应的试卷列表页面视图给用户。 + * 如果在查询过程中出现异常,则将视图名称设置为"404",返回相应的ModelAndView对象,表示出现错误情况(可能展示404页面给用户)。 + * + * @param subjectId 学科的唯一标识符,用于筛选特定学科下的试卷列表。 + * @param courseId 课程的唯一标识符,用于进一步筛选属于该课程的试卷。 + * @param paperType 试卷的类型,用于按类型筛选试卷(具体类型值根据业务定义,可能是真题、模拟题等分类)。 + * @param year 试卷对应的年份,用于按年份筛选试卷,方便用户查找特定年份的试卷。 + * @param area 试卷所属的地区,用于按地区筛选试卷,满足不同地区用户查看对应地区试卷的需求。 + * @param pageNum 当前页码,用于分页查询试卷列表,确定要展示的那一页试卷信息。 + * @param model 用于传递数据到视图的ModelAndView对象,在这里将各种相关数据添加到该对象中。 + * @return 返回包含视图名称和相关试卷及关联数据的ModelAndView对象,用于渲染相应的试卷列表页面视图。 + */ @RequestMapping(value = {"paperlist/{subjectId}-{courseId}-{paperType}-{year}-{area}-{pageNum}"}, method = RequestMethod.GET) - public ModelAndView indexAction(@PathVariable String subjectId , @PathVariable String courseId , @PathVariable String paperType, - @PathVariable String year , @PathVariable String area , @PathVariable Integer pageNum, ModelAndView model) { - try { - model.setViewName("paperlist"); - - CourseEntity course = iCourseService.find(courseId); - List courseList = iCourseService.findBySubjectId(subjectId); - SubjectEntity subject = iSubjectService.find(subjectId); - List areaList = iAreaService.findRootArea(); - PageUtils page = PageUtils.getPage(iPaperService.findList(subjectId , courseId , paperType , year , area , pageNum)); - if(course == null) { - course = courseList.get(0); - } - Long total = iPaperService.getPaperTotal(); - model.addObject("courseList", courseList); - model.addObject("subject", subject); - model.addObject("course", course); - model.addObject("areaList", areaList); - model.addObject("paperPage" , page); - model.addObject("total" , total); - model.addObject("courseId", course.getUid()); - model.addObject("paperType", paperType); - model.addObject("year", year); - model.addObject("area", area); - return model; + public ModelAndView indexAction(@PathVariable String subjectId, @PathVariable String courseId, + @PathVariable String paperType, @PathVariable String year, @PathVariable String area, + @PathVariable Integer pageNum, ModelAndView model) { + try { + // 设置视图名称为"paperlist",对应相应的试卷列表页面模板,后续会根据这个名称去查找并渲染对应的视图展示给用户。 + model.setViewName("paperlist"); + + // 根据传入的课程ID,通过课程服务查询并获取对应的课程实体信息,用于在试卷列表页面展示试卷所属课程等相关信息。 + CourseEntity course = iCourseService.find(courseId); + + // 通过课程服务,查找与传入学科ID对应的所有课程列表,可能用于在试卷列表页面展示该学科下的所有课程,方便用户切换查看不同课程的试卷情况。 + List courseList = iCourseService.findBySubjectId(subjectId); + + // 根据传入的学科ID,通过学科服务查询并获取对应的学科实体信息,用于在试卷列表页面展示试卷所属学科等相关信息。 + SubjectEntity subject = iSubjectService.find(subjectId); + + // 通过地区服务查找根地区列表信息,可能用于在试卷列表页面提供地区筛选的下拉菜单等功能,展示所有可选的地区范围(具体根据业务需求而定)。 + List areaList = iAreaService.findRootArea(); + + // 调用试卷服务的findList方法,根据传入的多个条件(学科ID、课程ID、试卷类型、年份、地区、页码)获取相应的试卷列表数据,并通过PageUtils工具类进行分页相关的处理,得到包含分页信息的PageUtils对象。 + PageUtils page = PageUtils.getPage(iPaperService.findList(subjectId, courseId, paperType, year, area, pageNum)); + + // 如果通过课程ID未查询到对应的课程实体(可能课程ID不存在或者其他原因导致查询失败),则取课程列表中的第一个课程作为默认展示(具体根据业务逻辑需求调整)。 + if (course == null) { + course = courseList.get(0); + } + + // 调用试卷服务的getPaperTotal方法获取试卷的总数量,可能用于在试卷列表页面展示总共有多少试卷等相关统计信息。 + Long total = iPaperService.getPaperTotal(); + + // 将查询到的课程列表信息添加到ModelAndView对象中,以便在视图中展示该学科下的所有课程情况,方便用户切换查看不同课程的试卷。 + model.addObject("courseList", courseList); + + // 将查询到的学科实体信息添加到ModelAndView对象中,以便在视图中展示试卷所属学科等相关信息。 + model.addObject("subject", subject); + + // 将查询到的课程实体信息添加到ModelAndView对象中,以便在视图中展示试卷所属课程等相关信息。 + model.addObject("course", course); + + // 将查询到的地区列表信息添加到ModelAndView对象中,以便在视图中展示可筛选的地区范围,供用户按地区查找试卷。 + model.addObject("areaList", areaList); + + // 将包含分页信息的试卷列表数据(通过PageUtils包装)添加到ModelAndView对象中,以便在视图中展示具体的试卷列表内容。 + model.addObject("paperPage", page); + + // 将试卷的总数量添加到ModelAndView对象中,以便在视图中展示试卷的统计信息,如告知用户总共有多少试卷等。 + model.addObject("total", total); + + // 将当前试卷所属的课程ID添加到ModelAndView对象中,可能用于前端页面的一些交互逻辑或者链接生成等用途(具体根据前端需求确定)。 + model.addObject("courseId", course.getUid()); + + // 将当前试卷的类型添加到ModelAndView对象中,可能用于在视图中对试卷进行分类展示或者筛选等相关操作(具体根据前端展示需求确定)。 + model.addObject("paperType", paperType); + + // 将当前试卷对应的年份添加到ModelAndView对象中,可能用于在视图中按年份对试卷进行筛选或者展示等相关操作(具体根据前端展示需求确定)。 + model.addObject("year", year); + + // 将当前试卷所属的地区添加到ModelAndView对象中,可能用于在视图中按地区对试卷进行筛选或者展示等相关操作(具体根据前端展示需求确定)。 + model.addObject("area", area); + + // 返回包含视图名称和各种相关数据的ModelAndView对象,以便进行视图渲染并展示给用户试卷列表页面内容。 + return model; } catch (Exception e) { + // 如果在查询相关数据过程中出现异常,将视图名称设置为"404",通常用于表示请求的资源不存在等错误情况。 model.setViewName("404"); + // 返回包含错误视图名称的ModelAndView对象,可能会渲染出404页面展示给用户。 return model; } - - } - + + } + + /** + * 处理试卷详情页面的GET请求,根据传入的试卷ID,查询并获取与该试卷相关的各种信息(如试卷本身信息、所属学科、课程、试题列表以及各种推荐试卷列表等), + * 将这些信息添加到ModelAndView对象中,设置视图名称为"paper",最后返回该ModelAndView对象用于渲染相应的试卷详情页面视图给用户。 + * 如果在查询过程中出现异常,则将视图名称设置为"404",返回相应的ModelAndView对象,表示出现错误情况(可能展示404页面给用户)。 + * + * @param paperId 试卷的唯一标识符,用于定位要展示详情的具体试卷。 + * @param model 用于传递数据到视图的ModelAndView对象,在这里将各种相关数据添加到该对象中。 + * @return 返回包含视图名称和相关试卷及关联数据的ModelAndView对象,用于渲染相应的试卷详情页面视图。 + */ @RequestMapping(value = {"/paper/{paperId}.html"}, method = RequestMethod.GET) - public ModelAndView indexAction(@PathVariable String paperId , ModelAndView model){ + public ModelAndView indexAction(@PathVariable String paperId, ModelAndView model) { try { + // 设置视图名称为"paper",对应相应的试卷详情页面模板,后续会根据这个名称去查找并渲染对应的视图展示给用户。 model.setViewName("paper"); + + // 根据传入的试卷ID,通过试卷服务查询并获取对应的试卷实体信息,用于在试卷详情页面展示试卷的详细内容。 PaperEntity paper = iPaperService.find(paperId); + + // 将查询到的试卷实体信息添加到ModelAndView对象中,以便在视图中展示试卷的详细内容,如试卷名称、考试时间等信息。 model.addObject("paper", paper); - model.addObject("subject", StringUtils.isEmpty(paper.getSubjectId()) ? null : iSubjectService.find(paper.getSubjectId())); - model.addObject("course", StringUtils.isEmpty(paper.getCourseId()) ? null : iCourseService.find(paper.getCourseId())); + + // 判断试卷的学科ID是否为空,如果不为空则通过学科服务查询并获取对应的学科实体信息,然后添加到ModelAndView对象中,用于在试卷详情页面展示试卷所属学科等相关信息; + // 如果学科ID为空,则添加null,表示没有对应的学科信息(这种情况可能是数据异常等原因导致,具体根据业务逻辑处理)。 + model.addObject("subject", StringUtils.isEmpty(paper.getSubjectId())? null : iSubjectService.find(paper.getSubjectId())); + + // 判断试卷的课程ID是否为空,如果不为空则通过课程服务查询并获取对应的课程实体信息,然后添加到ModelAndView对象中,用于在试卷详情页面展示试卷所属课程等相关信息; + // 如果课程ID为空,则添加null,表示没有对应的课程信息(这种情况可能是数据异常等原因导致,具体根据业务逻辑处理)。 + model.addObject("course", StringUtils.isEmpty(paper.getCourseId())? null : iCourseService.find(paper.getCourseId())); + + // 通过试题服务,查找该试卷对应的试题列表信息,用于在试卷详情页面展示试卷包含的具体试题内容等情况。 model.addObject("questionList", iQuestionService.findPaperQuestion(paperId)); - // 获取推荐试卷 + // 获取真题推荐试卷列表,调用试卷服务的featuredPaper方法,传入特定的真题标识(TamguoConstant.ZHENGTI_PAPER_ID)和当前试卷的学科ID,获取相关的真题推荐试卷列表, + // 并添加到ModelAndView对象中,用于在试卷详情页面展示相关的推荐试卷内容,方便用户查看其他类似的真题试卷。 model.addObject("zhentiPaperList", iPaperService.featuredPaper(TamguoConstant.ZHENGTI_PAPER_ID, paper.getSubjectId())); + + // 获取模拟题推荐试卷列表,调用试卷服务的featuredPaper方法,传入特定的模拟题标识(TamguoConstant.MONI_PAPER_ID)和当前试卷的学科ID,获取相关的模拟题推荐试卷列表, + // 并添加到ModelAndView对象中,用于在试卷详情页面展示相关的推荐试卷内容,方便用户查看其他类似的模拟题试卷。 model.addObject("moniPaperList", iPaperService.featuredPaper(TamguoConstant.MONI_PAPER_ID, paper.getSubjectId())); + + // 获取押题推荐试卷列表,调用试卷服务的featuredPaper方法,传入特定的押题标识(TamguoConstant.YATI_PAPER_ID)和当前试卷的学科ID,获取相关的押题推荐试卷列表, + // 并添加到ModelAndView对象中,用于在试卷详情页面展示相关的推荐试卷内容,方便用户查看其他类似的押题试卷。 model.addObject("yatiPaperList", iPaperService.featuredPaper(TamguoConstant.YATI_PAPER_ID, paper.getSubjectId())); + + // 获取热门试卷列表,调用试卷服务的findHotPaper方法,传入当前试卷的学科ID和课程ID,获取相关的热门试卷列表, + // 并添加到ModelAndView对象中,用于在试卷详情页面展示相关的推荐试卷内容,方便用户查看其他热门的试卷。 model.addObject("hotPaperList", iPaperService.findHotPaper(paper.getSubjectId(), paper.getCourseId())); + + // 返回包含视图名称和各种相关数据的ModelAndView对象,以便进行视图渲染并展示给用户试卷详情页面内容。 return model; } catch (Exception e) { + // 如果在查询相关数据过程中出现异常,将视图名称设置为"404",通常用于表示请求的资源不存在等错误情况。 model.setViewName("404"); + // 返回包含错误视图名称的ModelAndView对象,可能会渲染出404页面展示给用户。 return model; } - + } - + + /** + * 处理根据地区ID和试卷类型查找试卷的GET请求,直接调用试卷服务的相应方法,根据传入的地区ID和试卷类型查找对应的试卷列表信息,并将查找结果(试卷列表)以JSON格式返回给前端, + * 方便前端根据返回的试卷数据进行展示或者其他相关操作(例如动态加载某个地区下特定类型的试卷列表等功能)。 + * + * @param areaId 地区的唯一标识符,用于定位要查找试卷所属的地区范围。 + * @param type 试卷的类型,用于筛选出符合该类型的试卷,具体类型值根据业务定义,可能是真题、模拟题等分类。 + * @return 返回包含指定地区下符合类型要求的所有试卷实体信息的List列表,若未找到则返回空列表,前端可根据返回结果进行相应展示或处理。 + */ @RequestMapping(value = {"/paper/area/{areaId}-{type}.html"}, method = RequestMethod.GET) @ResponseBody - public List findPaperByAreaId(@PathVariable String areaId ,@PathVariable String type){ - return iPaperService.findPaperByAreaId(areaId , type); + public List findPaperByAreaId(@PathVariable String areaId, @PathVariable String type) { + return iPaperService.findPaperByAreaId(areaId, type); } - -} + +} \ No newline at end of file diff --git a/tamguo/src/main/java/com/tamguo/web/PasswordController.java b/tamguo/src/main/java/com/tamguo/web/PasswordController.java index cae68a2..9779553 100644 --- a/tamguo/src/main/java/com/tamguo/web/PasswordController.java +++ b/tamguo/src/main/java/com/tamguo/web/PasswordController.java @@ -10,66 +10,147 @@ import org.springframework.web.servlet.ModelAndView; import com.tamguo.service.IMemberService; import com.tamguo.util.Result; +// 标识这是一个Spring的控制器类,主要用于处理与用户密码相关的各种操作的Web请求,例如确认账号、安全验证、重置密码以及检查账号是否存在等功能。 @Controller public class PasswordController { - + + // 自动注入IMemberService,用于调用会员相关的业务逻辑方法,比如确认账号、进行安全检查、重置密码以及检查账号有效性等操作,这些操作都依赖于会员服务层提供的具体实现。 @Autowired private IMemberService iMemberService; - + + /** + * 处理密码找回流程中确认账号页面的GET请求,设置视图名称为"password/confirmAccount",意味着会查找并渲染名为"password/confirmAccount"的视图模板(例如JSP、Thymeleaf等模板文件,具体取决于项目配置)返回给客户端展示确认账号页面。 + * 该页面通常用于用户输入账号相关信息(如用户名等)以开始密码找回流程的第一步操作。 + * + * @param model 用于传递数据到视图的ModelAndView对象,此处仅设置了视图名称,暂未添加额外数据,用于后续视图渲染展示确认账号页面。 + * @return 返回包含视图名称的ModelAndView对象,以便Spring的视图解析器根据该名称查找并渲染相应的视图展示给用户。 + */ @RequestMapping(value = "password/find", method = RequestMethod.GET) - public ModelAndView confirmAccount(ModelAndView model){ + public ModelAndView confirmAccount(ModelAndView model) { model.setViewName("password/confirmAccount"); return model; } - + + /** + * 处理密码找回流程中提交确认账号信息的POST请求,接收用户名(username)和验证码(veritycode)参数,调用会员服务的confirmAccount方法进行账号确认相关的业务逻辑处理, + * 根据返回的Result对象中的状态码(code)来决定下一步要展示的页面以及向视图模型中添加的数据内容,比如验证成功则跳转到安全检查页面,失败则返回确认账号页面并展示相应的错误提示信息。 + * + * @param username 用户输入的用户名,用于在会员服务中进行账号确认操作,判断账号是否存在以及验证码是否匹配等情况。 + * @param veritycode 用户输入的验证码,用于与系统生成并发送给用户(可能通过邮箱或短信等方式)的验证码进行比对验证,作为账号确认的一部分依据。 + * @param model 用于传递数据到视图的ModelAndView对象,根据业务逻辑处理结果向其添加相应的数据(如验证结果、账号信息、错误码等),以便在相应的页面展示给用户。 + * @return 返回包含视图名称和相关数据的ModelAndView对象,以便Spring的视图解析器根据视图名称查找并渲染相应的视图展示给用户,展示的页面根据业务逻辑验证结果而定(成功跳转到安全检查页面,失败返回确认账号页面)。 + */ @RequestMapping(value = "password/confirmAccount", method = RequestMethod.POST) - public ModelAndView submitConfirmAccount(String username , String veritycode , ModelAndView model){ + public ModelAndView submitConfirmAccount(String username, String veritycode, ModelAndView model) { + // 调用会员服务的confirmAccount方法,传入用户名和验证码进行账号确认操作,返回包含操作结果信息的Result对象,其中包含状态码、提示信息以及可能的业务数据等内容。 Result result = iMemberService.confirmAccount(username, veritycode); - if(result.getCode() == 200){ + + // 判断返回的Result对象中的状态码是否为200,表示账号确认操作成功,通常意味着账号存在且验证码匹配等情况符合预期。 + if (result.getCode() == 200) { + // 如果账号确认成功,设置视图名称为"password/securityCheck",表示要跳转到密码找回流程中的安全检查页面,用于后续进一步验证用户身份等操作。 model.setViewName("password/securityCheck"); + // 将确认账号操作返回的Result对象添加到视图模型中,可能在安全检查页面会展示一些相关的提示信息或者使用其中的数据(具体根据前端页面需求确定)。 model.addObject("result", result); - model.addObject("isEmail", username.contains("@") ? "1" : "0"); - }else{ + // 根据用户名中是否包含"@"符号来判断是邮箱账号还是手机号等其他形式账号,将判断结果("1"表示邮箱账号,"0"表示非邮箱账号)添加到视图模型中, + // 可能用于安全检查页面根据账号类型进行不同的验证方式展示等逻辑处理(例如邮箱账号可能通过邮件验证,手机号账号可能通过短信验证等,具体根据业务逻辑确定)。 + model.addObject("isEmail", username.contains("@")? "1" : "0"); + } else { + // 如果账号确认失败,设置视图名称为"password/confirmAccount",表示返回确认账号页面,以便用户重新输入正确的信息进行账号确认操作。 model.setViewName("password/confirmAccount"); + // 将用户输入的账号信息添加到视图模型中,可能用于前端页面保留用户之前输入的账号内容,方便用户查看和修改,提升用户体验。 model.addObject("account", username); - model.addObject("username",username); + // 将用户输入的用户名添加到视图模型中,可能同样用于前端页面保留用户名信息,具体根据前端页面展示需求确定。 + model.addObject("username", username); + // 将用户输入的验证码添加到视图模型中,可能用于前端页面展示用户之前输入的验证码内容,方便用户查看是否输入有误等情况。 model.addObject("veritycode", veritycode); + // 将操作失败返回的Result对象中的状态码添加到视图模型中,方便前端页面根据状态码进行不同的错误提示信息展示等逻辑处理,告知用户具体是哪种验证失败的情况。 model.addObject("code", result.getCode()); } + + // 返回包含视图名称和相应数据的ModelAndView对象,以便Spring的视图解析器根据视图名称查找并渲染相应的视图展示给用户。 return model; } - + + /** + * 处理密码找回流程中安全检查的POST请求,接收用户名(username)、是否为邮箱账号标识(isEmail)以及手机验证码(mobileVcode)等参数,调用会员服务的securityCheck方法进行安全检查相关的业务逻辑处理, + * 根据返回的Result对象中的状态码(code)来决定下一步要展示的页面以及向视图模型中添加的数据内容,比如验证成功则跳转到重置密码页面并传递相关数据,失败则返回安全检查页面并展示相应的错误提示信息。 + * + * @param username 用户输入的用户名,用于在会员服务中进一步确认用户身份等相关安全检查操作,确保是合法的用户进行密码重置操作。 + * @param isEmail 表示账号是否为邮箱账号的标识,从前端传递过来,用于在会员服务中根据账号类型进行不同的安全检查逻辑处理(例如邮箱账号可能验证邮件验证码,手机号账号可能验证短信验证码等),值为"1"表示是邮箱账号,"0"表示不是邮箱账号。 + * @param mobileVcode 用户输入的手机验证码(如果是手机号账号相关验证场景),用于与系统发送给用户手机的验证码进行比对验证,作为安全检查的一部分依据;如果是邮箱账号相关验证场景,此处可能根据业务逻辑不一定使用该参数(具体依实际情况而定)。 + * @param model 用于传递数据到视图的ModelAndView对象,根据业务逻辑处理结果向其添加相应的数据(如用户名、重置密码的关键信息、验证结果、错误标识等),以便在相应的页面展示给用户。 + * @return 返回包含视图名称和相关数据的ModelAndView对象,以便Spring的视图解析器根据视图名称查找并渲染相应的视图展示给用户,展示的页面根据业务逻辑验证结果而定(成功跳转到重置密码页面,失败返回安全检查页面)。 + */ @RequestMapping(value = "password/securityCheck", method = RequestMethod.POST) - public ModelAndView securityCheck(String username , String isEmail , String mobileVcode , ModelAndView model){ - Result result = iMemberService.securityCheck(username , isEmail , mobileVcode); - if(result.getCode() == 200){ + public ModelAndView securityCheck(String username, String isEmail, String mobileVcode, ModelAndView model) { + // 调用会员服务的securityCheck方法,传入用户名、是否为邮箱账号标识以及手机验证码等参数进行安全检查操作,返回包含操作结果信息的Result对象,其中包含状态码、提示信息以及可能的业务数据等内容。 + Result result = iMemberService.securityCheck(username, isEmail, mobileVcode); + + // 判断返回的Result对象中的状态码是否为200,表示安全检查操作成功,通常意味着用户身份验证通过等情况符合预期,可以进行下一步的密码重置操作。 + if (result.getCode() == 200) { + // 将用户名添加到视图模型中,可能在重置密码页面会展示用户名等相关信息,告知用户当前正在重置哪个账号的密码(具体根据前端页面需求确定)。 model.addObject("username", username); - model.addObject("resetPasswordKey" , result.getResult()); + // 将安全检查操作成功后返回的Result对象中包含的用于重置密码的关键信息(例如重置密码的令牌等,具体根据业务逻辑确定)添加到视图模型中, + // 后续在重置密码页面会使用该信息来确保是经过安全验证后的合法操作,用于关联和验证密码重置请求。 + model.addObject("resetPasswordKey", result.getResult()); + // 设置视图名称为"password/resetPassword",表示要跳转到密码找回流程中的重置密码页面,用于用户输入新的密码等操作。 model.setViewName("password/resetPassword"); - }else{ + } else { + // 如果安全检查失败,将安全检查操作返回的Result对象添加到视图模型中,可能在安全检查页面会展示一些相关的错误提示信息或者使用其中的数据(具体根据前端页面需求确定)。 model.addObject("result", result); + // 将是否为邮箱账号的标识添加到视图模型中,可能用于安全检查页面根据账号类型进行不同的错误提示信息展示等逻辑处理,告知用户具体是哪种账号类型的验证失败情况。 model.addObject("isEmail", isEmail); + // 添加一个表示验证码错误的标识(值为"1")到视图模型中,方便前端页面根据该标识进行相应的错误提示信息展示,告知用户验证码输入有误等情况(具体根据前端页面展示逻辑确定)。 model.addObject("codeError", "1"); + // 设置视图名称为"password/securityCheck",表示返回安全检查页面,以便用户重新输入正确的信息进行安全检查操作。 model.setViewName("password/securityCheck"); } + + // 返回包含视图名称和相应数据的ModelAndView对象,以便Spring的视图解析器根据视图名称查找并渲染相应的视图展示给用户。 return model; } - + + /** + * 处理密码找回流程中提交重置密码信息的POST请求,接收重置密码的关键信息(resetPasswordKey)、用户名(username)、新密码(password)以及确认新密码(verifypwd)等参数, + * 调用会员服务的resetPassword方法进行密码重置相关的业务逻辑处理,根据返回的Result对象中的状态码(code)来决定下一步要展示的页面(成功则跳转到密码重置成功页面,失败则返回重置密码页面)。 + * + * @param resetPasswordKey 用于验证密码重置操作合法性的关键信息,在前面的安全检查步骤中获取并传递过来,确保当前的密码重置请求是经过合法验证后的操作,防止非法重置密码的情况发生。 + * @param username 用户输入的用户名,用于再次确认是对哪个账号进行密码重置操作,确保操作的准确性和对应性。 + * @param password 用户输入的新密码,用于设置为账号的新密码,在会员服务中会对新密码进行格式等相关验证以及更新到数据库等操作。 + * @param verifypwd 用户输入的确认新密码,用于与新密码进行比对,确保用户两次输入的密码一致,避免因输入错误导致密码不一致的情况。 + * @param model 用于传递数据到视图的ModelAndView对象,根据业务逻辑处理结果向其添加相应的视图名称,以便Spring的视图解析器根据视图名称查找并渲染相应的视图展示给用户,展示的页面根据密码重置操作的验证结果而定(成功跳转到密码重置成功页面,失败返回重置密码页面)。 + * @return 返回包含视图名称的ModelAndView对象,以便Spring的视图解析器根据视图名称查找并渲染相应的视图展示给用户,展示的页面根据密码重置操作的验证结果而定(成功跳转到密码重置成功页面,失败返回重置密码页面)。 + */ @RequestMapping(value = "password/resetPassword", method = RequestMethod.POST) - public ModelAndView resetPassword(String resetPasswordKey , String username , String password , String verifypwd , ModelAndView model){ - Result result = iMemberService.resetPassword(resetPasswordKey , username , password , verifypwd); - if(result.getCode() == 200){ + public ModelAndView resetPassword(String resetPasswordKey, String username, String password, String verifypwd, + ModelAndView model) { + // 调用会员服务的resetPassword方法,传入重置密码关键信息、用户名、新密码以及确认新密码等参数进行密码重置操作,返回包含操作结果信息的Result对象,其中包含状态码、提示信息以及可能的业务数据等内容。 + Result result = iMemberService.resetPassword(resetPasswordKey, username, password, verifypwd); + + // 判断返回的Result对象中的状态码是否为200,表示密码重置操作成功,通常意味着新密码设置成功等情况符合预期。 + if (result.getCode() == 200) { + // 如果密码重置成功,设置视图名称为"password/resetPwSuccess",表示要跳转到密码找回流程中的密码重置成功页面,用于告知用户密码重置操作已顺利完成。 model.setViewName("password/resetPwSuccess"); - }else{ + } else { + // 如果密码重置失败,设置视图名称为"password/resetPassword",表示返回重置密码页面,以便用户重新输入正确的密码信息进行密码重置操作。 model.setViewName("password/resetPassword"); } + + // 返回包含视图名称的ModelAndView对象,以便Spring的视图解析器根据视图名称查找并渲染相应的视图展示给用户。 return model; } - + + /** + * 处理检查账号是否存在的GET请求,接收账号信息(account),调用会员服务的checkAccount方法进行账号检查相关的业务逻辑处理,然后直接将返回的Result对象(包含账号存在与否等相关检查结果信息)返回给前端, + * 前端可根据返回的Result对象进行相应的展示或后续处理,例如根据账号是否存在来决定是否显示某些提示信息或者进行下一步操作等。 + * + * @param account 要检查的账号信息,可能是用户名、邮箱账号或者手机号等形式,具体根据业务逻辑中对账号的定义来确定,用于在会员服务中查询判断该账号是否已经存在于系统中。 + * @return 返回包含账号检查结果信息的Result对象,其中包含状态码、提示信息以及可能的业务数据等内容,前端可根据这些信息进行相应的处理和展示。 + */ @RequestMapping(value = "password/checkAccount", method = RequestMethod.GET) @ResponseBody - public Result checkAccount(String account){ + public Result checkAccount(String account) { return iMemberService.checkAccount(account); } - -} + +} \ No newline at end of file diff --git a/tamguo/src/main/java/com/tamguo/web/QuestionContrller.java b/tamguo/src/main/java/com/tamguo/web/QuestionContrller.java index a62c663..ed16f6c 100644 --- a/tamguo/src/main/java/com/tamguo/web/QuestionContrller.java +++ b/tamguo/src/main/java/com/tamguo/web/QuestionContrller.java @@ -10,6 +10,7 @@ import org.springframework.web.servlet.ModelAndView; import com.baomidou.mybatisplus.plugins.Page; import com.tamguo.model.ChapterEntity; +import com.baomidou.mybatisplus.plugins.Page; import com.tamguo.model.CourseEntity; import com.tamguo.model.QuestionEntity; import com.tamguo.model.SubjectEntity; @@ -20,78 +21,156 @@ import com.tamguo.service.IQuestionService; import com.tamguo.service.ISubjectService; import com.tamguo.util.Result; +// 标识这是一个Spring的控制器类,主要用于处理与试题相关的各种Web请求,例如展示试题列表页面、试题详情页面以及获取特定试题信息等功能, +// 通过调用相应的服务层接口获取数据,并将数据添加到ModelAndView对象中返回给视图进行展示,或者直接以JSON格式返回数据给前端(如通过@ResponseBody注解的方法)。 @Controller public class QuestionContrller { + // 自动注入IQuestionService,用于处理试题相关的业务逻辑,像根据章节ID获取试题列表、查找普通试题、获取推荐试题、按ID选择试题等操作都是通过该服务来完成。 @Autowired private IQuestionService iQuestionService; + + // 自动注入IChapterService,用于处理章节相关的业务逻辑,例如根据章节ID查找章节、查找下一个章节、获取章节所属课程等操作,在获取试题相关信息时可能会涉及章节信息的查询。 @Autowired private IChapterService iChapterService; + + // 自动注入ISubjectService,用于处理学科相关的业务逻辑,例如根据课程所属学科ID查找学科信息等操作,试题通常与特定学科相关,查询试题相关信息时可能需要获取学科相关内容。 @Autowired private ISubjectService iSubjectService; + + // 自动注入ICourseService,用于处理课程相关的业务逻辑,比如根据章节所属课程ID查找课程、获取课程所属学科等操作,在获取试题相关信息时可能会涉及课程信息的查询。 @Autowired private ICourseService iCourseService; - + + /** + * 处理试题列表页面的GET请求,根据传入的章节ID、偏移量(offset,通常用于分页表示起始位置)和每页数量(limit)等路径变量, + * 查询并获取与该章节相关的试题列表及其他相关信息(如章节、课程、学科、父章节、下一个章节等), + * 将这些信息添加到ModelAndView对象中,设置视图名称为"questionList",最后返回该ModelAndView对象用于渲染相应的试题列表页面视图给用户。 + * 如果在查询过程中出现异常,则将视图名称设置为"404",返回相应的ModelAndView对象,表示出现错误情况(可能展示404页面给用户)。 + * + * @param chapterId 章节的唯一标识符,用于筛选特定章节下的试题列表,定位到具体属于哪个章节的试题。 + * @param offset 当前页码对应的偏移量,用于分页查询,确定从哪条记录开始获取试题列表,通常结合每页数量来实现分页功能。 + * @param limit 每页显示的试题数量,用于分页查询,确定每页展示多少条试题信息。 + * @param model 用于传递数据到视图的ModelAndView对象,在这里将各种相关数据添加到该对象中。 + * @return 返回包含视图名称和相关试题及关联数据的ModelAndView对象,用于渲染相应的试题列表页面视图。 + */ @RequestMapping(value = {"questionlist/{chapterId}-{offset}-{limit}"}, method = RequestMethod.GET) - public ModelAndView questionList(@PathVariable String chapterId , @PathVariable Integer offset , - @PathVariable Integer limit , ModelAndView model){ + public ModelAndView questionList(@PathVariable String chapterId, @PathVariable Integer offset, + @PathVariable Integer limit, ModelAndView model) { try { + // 设置视图名称为"questionList",对应相应的试题列表页面模板,后续会根据这个名称去查找并渲染对应的视图展示给用户。 model.setViewName("questionList"); + // 根据传入的章节ID,通过章节服务查询并获取对应的章节实体信息,用于在试题列表页面展示试题所属章节等相关信息。 ChapterEntity chapter = iChapterService.findById(chapterId); + + // 通过章节所属的课程ID,利用课程服务查询并获取对应的课程实体信息,用于在试题列表页面展示试题所属课程等相关信息。 CourseEntity course = iCourseService.find(chapter.getCourseId()); + + // 通过课程所属的学科ID,利用学科服务查询并获取对应的学科实体信息,用于在试题列表页面展示试题所属学科等相关信息。 SubjectEntity subject = iSubjectService.find(course.getSubjectId()); + + // 根据章节的父章节ID,通过章节服务查询并获取对应的父章节实体信息,可能用于在试题列表页面展示章节的层级关系等相关信息(具体根据前端展示需求确定)。 ChapterEntity parentChapter = iChapterService.findById(chapter.getParentId()); - ChapterEntity nextChapter = iChapterService.findNextPoint(chapter.getParentId() , chapter.getOrders()); - + + // 通过章节服务查找给定父章节下,当前章节顺序之后的下一个章节信息,可能用于在试题列表页面提供章节导航等功能(例如查看下一章的试题等情况,具体根据业务逻辑和前端需求确定)。 + ChapterEntity nextChapter = iChapterService.findNextPoint(chapter.getParentId(), chapter.getOrders()); + + // 创建一个用于分页的Page对象,设置当前页码(根据传入的偏移量转换而来,可能需要根据具体分页逻辑进行计算调整)和每页显示的数量。 Page page = new Page<>(); page.setCurrent(offset); page.setSize(limit); - Page questionList = iQuestionService.findByChapterId(chapterId , page); + + // 调用试题服务的findByChapterId方法,根据传入的章节ID和分页对象,获取该章节下符合分页条件的试题列表数据,返回包含试题列表以及分页相关信息的Page对象。 + Page questionList = iQuestionService.findByChapterId(chapterId, page); + + // 将查询到的学科实体信息添加到ModelAndView对象中,以便在视图中展示试题所属学科等相关信息。 model.addObject("subject", subject); + + // 将查询到的课程实体信息添加到ModelAndView对象中,以便在视图中展示试题所属课程等相关信息。 model.addObject("course", course); + + // 将查询到的章节实体信息添加到ModelAndView对象中,以便在视图中展示试题所属章节等相关信息。 model.addObject("chapter", chapter); - model.addObject("parentChapter" , parentChapter); - model.addObject("nextChapter" , nextChapter); + + // 将查询到的父章节实体信息添加到ModelAndView对象中,以便在视图中展示章节的层级关系等相关信息(具体根据前端展示需求确定)。 + model.addObject("parentChapter", parentChapter); + + // 将查询到的下一个章节实体信息添加到ModelAndView对象中,以便在视图中展示章节导航等相关信息(例如查看下一章的试题等情况,具体根据业务逻辑和前端需求确定)。 + model.addObject("nextChapter", nextChapter); + + // 将包含试题列表及分页信息的Page对象添加到ModelAndView对象中,以便在视图中展示具体的试题列表内容。 model.addObject("questionList", questionList); + + // 将课程所属的学科ID添加到ModelAndView对象中,可能用于前端页面的一些交互逻辑或者链接生成等用途(具体根据前端需求确定)。 model.addObject("subjectId", course.getSubjectId()); + + // 将课程的唯一标识符添加到ModelAndView对象中,可能用于前端页面的一些交互逻辑或者链接生成等用途(具体根据前端需求确定)。 model.addObject("courseId", course.getUid()); + + // 返回包含视图名称和各种相关数据的ModelAndView对象,以便进行视图渲染并展示给用户试题列表页面内容。 return model; } catch (Exception e) { + // 如果在查询相关数据过程中出现异常,将视图名称设置为"404",通常用于表示请求的资源不存在等错误情况。 model.setViewName("404"); + // 返回包含错误视图名称的ModelAndView对象,可能会渲染出404页面展示给用户。 return model; } - + } - + /** - * 直接访问题目 - * @param uid - * @param model - * @return + * 处理试题详情页面的GET请求,根据传入的试题唯一标识符(uid),查询并获取与该试题相关的各种信息(如试题本身信息、试题类型描述以及推荐试题列表等), + * 将这些信息添加到ModelAndView对象中,设置视图名称为"question",最后返回该ModelAndView对象用于渲染相应的试题详情页面视图给用户。 + * 如果在查询过程中出现异常,则将视图名称设置为"404",返回相应的ModelAndView对象,表示出现错误情况(可能展示404页面给用户)。 + * + * @param uid 试题的唯一标识符,用于定位要展示详情的具体试题。 + * @param model 用于传递数据到视图的ModelAndView对象,在这里将各种相关数据添加到该对象中。 + * @return 返回包含视图名称和相关试题及关联数据的ModelAndView对象,用于渲染相应的试题详情页面视图。 */ @RequestMapping(value = {"/question/{uid}.html"}, method = RequestMethod.GET) - public ModelAndView question(@PathVariable String uid , ModelAndView model){ + public ModelAndView question(@PathVariable String uid, ModelAndView model) { try { + // 设置视图名称为"question",对应相应的试题详情页面模板,后续会根据这个名称去查找并渲染对应的视图展示给用户。 model.setViewName("question"); + + // 根据传入的试题ID,通过试题服务查询并获取对应的试题实体信息,用于在试题详情页面展示试题的详细内容。 QuestionEntity question = iQuestionService.findNormalQuestion(uid); + + // 根据试题实体中存储的试题类型代码,通过QuestionType枚举类的方法获取对应的试题类型描述信息,并设置到试题实体中, + // 用于在试题详情页面展示更直观的试题类型名称(而不是原始的类型代码,提升用户体验)。 question.setQuestionType(QuestionType.getQuestionType(question.getQuestionType()).getDesc()); + + // 将查询到的试题实体信息(包含更新后的试题类型描述)添加到ModelAndView对象中,以便在视图中展示试题的详细内容,如试题题干、选项等信息。 model.addObject("question", question); - - // 推荐试题 - model.addObject("featuredQuestionList", iQuestionService.featuredQuestion(question.getSubjectId(),question.getCourseId())); + + // 获取推荐试题列表,调用试题服务的featuredQuestion方法,传入当前试题的学科ID和课程ID,获取相关的推荐试题列表, + // 并添加到ModelAndView对象中,用于在试题详情页面展示相关的推荐试题内容,方便用户查看其他类似或相关的试题。 + model.addObject("featuredQuestionList", iQuestionService.featuredQuestion(question.getSubjectId(), question.getCourseId())); + + // 返回包含视图名称和各种相关数据的ModelAndView对象,以便进行视图渲染并展示给用户试题详情页面内容。 return model; } catch (Exception e) { + // 如果在查询相关数据过程中出现异常,将视图名称设置为"404",通常用于表示请求的资源不存在等错误情况。 model.setViewName("404"); + // 返回包含错误视图名称的ModelAndView对象,可能会渲染出404页面展示给用户。 return model; } - + } - + + /** + * 处理获取特定试题信息的GET请求,根据传入的试题唯一标识符(uid),调用试题服务的select方法获取对应的试题信息, + * 将获取到的试题信息包装在表示成功的Result对象中返回给前端,方便前端根据统一的格式进行解析和使用试题数据(例如展示试题详情等操作)。 + * + * @param uid 试题的唯一标识符,用于定位要获取信息的具体试题。 + * @param model 用于传递数据到视图的ModelAndView对象(这里在该方法中未实际使用到,可能是为了与其他类似方法保持参数形式一致而添加的)。 + * @return 返回包含指定试题信息的Result对象,若成功获取则Result对象中的数据字段包含相应的试题信息,若出现问题则可能包含相应的错误提示等信息,前端可根据返回结果进行相应展示或处理。 + */ @RequestMapping(value = {"question/getQuestion/{uid}"}, method = RequestMethod.GET) @ResponseBody - public Result getQuestion(@PathVariable String uid , ModelAndView model){ + public Result getQuestion(@PathVariable String uid, ModelAndView model) { return Result.successResult(iQuestionService.select(uid)); } - -} + +} \ No newline at end of file diff --git a/tamguo/src/main/java/com/tamguo/web/RegisterController.java b/tamguo/src/main/java/com/tamguo/web/RegisterController.java index a8d9ef6..7f519b8 100644 --- a/tamguo/src/main/java/com/tamguo/web/RegisterController.java +++ b/tamguo/src/main/java/com/tamguo/web/RegisterController.java @@ -20,51 +20,97 @@ import com.tamguo.service.IMemberService; import com.tamguo.util.Result; import com.tamguo.util.ShiroUtils; +// 标识这是一个Spring的控制器类,主要用于处理用户注册相关的Web请求,例如展示注册页面、检查用户名和手机号是否可用以及处理实际的注册提交操作等功能。 @Controller public class RegisterController { - + + // 自动注入IMemberService,用于调用会员相关的业务逻辑方法,比如检查用户名是否已存在、检查手机号是否已注册、执行用户注册操作等,这些操作都依赖于会员服务层提供的具体实现。 @Autowired private IMemberService iMemberService; + /** + * 处理用户注册页面的GET请求,设置视图名称为"register",意味着会查找并渲染名为"register"的视图模板(例如JSP、Thymeleaf等模板文件,具体取决于项目配置)返回给客户端展示注册页面, + * 该页面通常包含用户输入注册信息(如用户名、密码、手机号等)的表单等元素,供用户填写信息进行注册操作。 + * + * @param model 用于传递数据到视图的ModelAndView对象,此处仅设置了视图名称,暂未添加额外数据,用于后续视图渲染展示注册页面。 + * @param session 当前的HttpSession对象,虽然在这里暂时未使用,但在后续注册成功登录等相关操作中可能会用到,用于保存用户相关信息等操作(例如保存登录后的用户信息)。 + * @return 返回包含视图名称的ModelAndView对象,以便Spring的视图解析器根据该名称查找并渲染相应的视图展示给用户。 + */ @RequestMapping(value = "/register", method = RequestMethod.GET) - public ModelAndView register(ModelAndView model , HttpSession session) { + public ModelAndView register(ModelAndView model, HttpSession session) { model.setViewName("register"); return model; - } - + } + + /** + * 处理检查用户名是否可用的GET请求,接收用户名(username)参数,调用会员服务的checkUsername方法进行用户名可用性检查的业务逻辑处理, + * 然后直接将返回的Result对象(包含用户名是否已存在等相关检查结果信息)返回给前端,前端可根据返回的Result对象进行相应的展示或后续处理,例如根据用户名是否可用来决定是否显示某些提示信息或者进行下一步操作等。 + * + * @param username 要检查的用户名,用于在会员服务中查询判断该用户名是否已经被其他用户使用,以确保注册时用户名的唯一性。 + * @return 返回包含用户名检查结果信息的Result对象,其中包含状态码、提示信息以及可能的业务数据等内容,前端可根据这些信息进行相应的处理和展示。 + */ @RequestMapping(value = "/checkUsername", method = RequestMethod.GET) @ResponseBody - public Result checkUsername(String username){ + public Result checkUsername(String username) { return iMemberService.checkUsername(username); } - + + /** + * 处理检查手机号是否可用的GET请求,接收手机号(mobile)参数,调用会员服务的checkMobile方法进行手机号可用性检查的业务逻辑处理, + * 然后直接将返回的Result对象(包含手机号是否已注册等相关检查结果信息)返回给前端,前端可根据返回的Result对象进行相应的展示或后续处理,例如根据手机号是否可用来决定是否显示某些提示信息或者进行下一步操作等。 + * + * @param mobile 要检查的手机号,用于在会员服务中查询判断该手机号是否已经被其他用户注册使用,以确保注册时手机号的唯一性(如果业务有此要求)。 + * @return 返回包含手机号检查结果信息的Result对象,其中包含状态码、提示信息以及可能的业务数据等内容,前端可根据这些信息进行相应的处理和展示。 + */ @RequestMapping(value = "/checkMobile", method = RequestMethod.GET) @ResponseBody - public Result checkMobile(String mobile){ + public Result checkMobile(String mobile) { return iMemberService.checkMobile(mobile); } - + + /** + * 处理用户提交注册信息的POST请求,通过@RequestBody注解接收前端传来的JSON格式的MemberEntity对象(包含了用户注册时填写的各种信息,如用户名、密码、手机号等)以及HttpSession对象, + * 先调用会员服务的register方法进行用户注册相关的业务逻辑处理,根据返回的Result对象中的状态码(code)来判断注册是否成功, + * 如果注册成功则尝试使用Shiro框架进行登录操作(将注册成功后的用户信息用于登录凭证),并在登录成功后将用户信息保存到HttpSession中;如果注册失败或者登录过程中出现异常,则返回相应的错误提示信息的Result对象给前端。 + * + * @param member 前端传来的包含用户注册信息的MemberEntity对象,通过JSON格式解析后得到,其中包含了注册所需的各种详细信息,如用户名、密码、手机号等,用于在会员服务中进行用户注册操作。 + * @param session 当前的HttpSession对象,用于在注册成功且登录成功后保存当前登录用户的相关信息,方便在后续的请求处理中获取当前用户信息,例如在其他页面展示用户昵称等操作。 + * @return 返回包含注册结果信息的Result对象,若注册成功且登录成功则返回表示成功的Result对象(状态码为200等相关成功信息),若注册失败或者登录出现异常则返回相应的错误提示信息的Result对象(包含不同的状态码和提示内容),前端可根据返回结果进行相应展示或处理。 + */ @RequestMapping(value = "/subRegister", method = RequestMethod.POST) @ResponseBody - public Result subRegister(@RequestBody MemberEntity member , HttpSession session){ + public Result subRegister(@RequestBody MemberEntity member, HttpSession session) { + // 调用会员服务的register方法,传入包含用户注册信息的MemberEntity对象进行用户注册操作,返回包含操作结果信息的Result对象,其中包含状态码、提示信息以及可能的业务数据等内容。 Result result = iMemberService.register(member); - if(result.getCode() == 200) { + + // 判断返回的Result对象中的状态码是否为200,表示用户注册操作成功,通常意味着用户信息已成功保存到数据库等情况符合预期,此时尝试进行登录操作。 + if (result.getCode() == 200) { + // 获取Shiro的Subject对象,它代表了当前执行的用户主体,用于后续进行登录认证等操作。 Subject subject = ShiroUtils.getSubject(); + // 从注册成功返回的Result对象中获取包含注册后的用户信息的MemberEntity对象,用于构建登录凭证(用户名和密码)。 MemberEntity memberEntity = (MemberEntity) result.getResult(); + // 创建一个UsernamePasswordToken对象,将注册后的用户名和用户注册时填写的密码封装进去,作为登录认证的凭证传递给Shiro框架。 UsernamePasswordToken token = new UsernamePasswordToken(memberEntity.getUsername(), member.getPassword()); try { + // 通过Subject对象发起登录认证操作,Shiro框架会根据配置的认证逻辑(如从数据库查询用户信息并比对密码等)进行验证。 subject.login(token); - + + // 如果登录成功,将登录后的用户信息保存到HttpSession中,方便在后续的请求处理中获取当前登录用户的相关信息,键为"currMember"。 session.setAttribute("currMember", ShiroUtils.getMember()); } catch (UnknownAccountException e) { + // 如果Shiro认证过程中抛出UnknownAccountException异常,表示用户名不存在或者未找到对应的用户信息,返回相应的错误提示信息的Result对象给前端。 return Result.result(201, null, "用户名或密码有误,请重新输入或找回密码"); } catch (IncorrectCredentialsException e) { + // 如果Shiro认证过程中抛出IncorrectCredentialsException异常,表示密码错误,返回相应的错误提示信息的Result对象给前端。 return Result.result(202, null, "用户名或密码有误,请重新输入或找回密码"); } catch (LockedAccountException e) { + // 如果Shiro认证过程中抛出LockedAccountException异常,表示账号被锁定,返回相应的错误提示信息的Result对象给前端。 return Result.result(203, null, "账号被锁定"); - } + } } + + // 返回包含注册结果信息的Result对象,若注册成功且登录成功则返回正常的Result对象(包含注册后的用户等相关成功信息),若注册失败或者登录出现异常则返回相应的错误提示信息的Result对象,前端可根据此结果进行相应处理。 return result; } - -} + +} \ No newline at end of file diff --git a/tamguo/src/main/java/com/tamguo/web/SubjectController.java b/tamguo/src/main/java/com/tamguo/web/SubjectController.java index a3f723f..afd3172 100644 --- a/tamguo/src/main/java/com/tamguo/web/SubjectController.java +++ b/tamguo/src/main/java/com/tamguo/web/SubjectController.java @@ -26,63 +26,121 @@ import com.tamguo.util.Result; /** * Controller - 考试(高考,建造师,医药师) - * - * @author candy.tam + * 该类作为Spring的控制器,主要用于处理与各类考试相关学科(Subject)主题的Web请求,例如展示特定学科的详细页面、获取课程树结构数据以及获取级联选择器形式的课程树数据等功能, + * 通过调用相应的服务层接口获取所需数据,并根据请求类型将数据以合适的方式返回给前端(渲染视图或者以JSON格式返回数据),同时对可能出现的异常进行了相应处理。 * + * @author candy.tam */ @Controller public class SubjectController { - + + // 创建日志记录器,用于记录在处理各类请求过程中出现的关键信息(如正常的业务日志、异常信息等),方便后续查看日志进行调试、排查问题以及了解系统运行情况。 private Logger logger = LoggerFactory.getLogger(getClass()); - + + // 自动注入IChapterService,用于处理章节相关的业务逻辑,例如根据书籍ID查找课程对应的章节列表等操作,在展示学科相关页面时可能需要获取章节信息进行展示。 @Autowired private IChapterService iChapterService; + + // 自动注入IAreaService,用于处理地区相关的业务逻辑,比如查找根地区列表等操作,可能在学科相关页面展示地区筛选信息或者其他与地区相关的功能中会用到该服务获取的数据。 @Autowired private IAreaService iAreaService; + + // 自动注入ISubjectService,用于处理学科相关的业务逻辑,像根据学科ID查找学科实体、获取课程树结构数据、获取级联选择器形式的课程树数据等操作都是通过该服务来完成,是处理学科相关请求的核心服务依赖。 @Autowired private ISubjectService iSubjectService; + + // 自动注入IBookService,用于处理书籍相关的业务逻辑,例如根据课程ID查询相关书籍列表等操作,在获取学科相关信息时可能会涉及书籍信息的查询,进而基于书籍信息获取其他相关数据(如章节信息等)。 @Autowired private IBookService iBookService; + /** + * 处理展示特定学科详细页面的GET请求,根据传入的学科ID(subjectId),查询并获取与该学科相关的各种信息(如学科本身信息、对应的课程、书籍、章节列表以及地区列表等), + * 将这些信息添加到ModelAndView对象中,设置视图名称为"subject",最后返回该ModelAndView对象用于渲染相应的学科详细页面视图给用户。 + * 如果在查询过程中出现异常,则将异常信息记录到日志中,同时将视图名称设置为"500",返回相应的ModelAndView对象,表示出现服务器内部错误情况(可能展示500页面给用户)。 + * + * @param subjectId 学科的唯一标识符,用于定位要展示详细信息的具体学科。 + * @param model 用于传递数据到视图的ModelAndView对象,在这里将各种相关数据添加到该对象中。 + * @return 返回包含视图名称和相关学科及关联数据的ModelAndView对象,用于渲染相应的学科详细页面视图,若出现异常则返回包含错误视图名称的ModelAndView对象。 + */ @SuppressWarnings("unchecked") @RequestMapping(value = {"subject/{subjectId}.html"}, method = RequestMethod.GET) - public ModelAndView indexAction(@PathVariable String subjectId , ModelAndView model) { + public ModelAndView indexAction(@PathVariable String subjectId, ModelAndView model) { try { + // 根据传入的学科ID,通过学科服务查询并获取对应的学科实体信息,用于在学科详细页面展示学科的基本信息、关联课程等相关内容。 SubjectEntity subject = iSubjectService.find(subjectId); - // 获取第一个科目 + + // 获取该学科下的第一个课程实体信息,这里假设取第一个课程作为某种默认展示或者后续处理的基础(具体业务逻辑可能需根据实际情况调整), + // 比如后续基于该课程查找相关书籍、章节等信息,方便在页面上展示与该学科相关的课程、书籍、章节等内容的关联关系。 CourseEntity course = subject.getCourseList().get(0); - // 获取第一本书 + + // 通过条件查询,查找与当前课程相关的所有书籍列表,可能用于展示该课程下有哪些相关书籍等场景,以便在学科详细页面呈现更丰富的关联信息。 List bookList = iBookService.selectList(Condition.create().eq("course_id", course.getUid())); + + // 获取书籍列表中的第一本图书实体信息(这里同样可能是基于某种默认展示或后续处理的考虑,取第一本作为代表,具体根据业务需求确定), + // 后续可能基于该书籍去查找对应的章节信息等内容,用于在学科详细页面展示课程下书籍包含的章节情况。 BookEntity book = bookList.get(0); - + + // 通过章节服务,查询并获取当前书籍对应的章节列表信息,用于在学科详细页面展示该课程下书籍所包含的章节内容等,方便用户查看学科相关的具体知识章节情况。 List chapterList = iChapterService.findCourseChapter(book.getUid()); - model.setViewName("subject"); - model.addObject("subject", subject); - model.addObject("course" , course); - model.addObject("courseList", subject.getCourseList()); - model.addObject("chapterList" , chapterList); - model.addObject("areaList", iAreaService.findRootArea()); - return model; + + // 设置视图名称为"subject",对应相应的学科详细页面模板,后续会根据这个名称去查找并渲染对应的视图展示给用户。 + model.setViewName("subject"); + + // 将查询到的学科实体信息添加到ModelAndView对象中,以便在视图中展示学科的详细信息,如学科名称、学科描述等内容。 + model.addObject("subject", subject); + + // 将获取到的第一个课程实体信息添加到ModelAndView对象中,以便在视图中展示学科下的课程相关信息,可能展示课程名称、课程简介等内容(具体根据前端页面需求确定)。 + model.addObject("course", course); + + // 将学科包含的所有课程列表信息添加到ModelAndView对象中,可能用于在页面上展示该学科下所有可选的课程,方便用户切换查看不同课程对应的书籍、章节等信息(具体根据前端展示需求确定)。 + model.addObject("courseList", subject.getCourseList()); + + // 将查询到的章节列表信息添加到ModelAndView对象中,以便在视图中展示课程下书籍所包含的章节内容,方便用户查看具体的知识章节情况。 + model.addObject("chapterList", chapterList); + + // 通过地区服务查找根地区列表信息,可能用于在学科详细页面提供地区筛选等功能(例如按地区查找该学科相关资源等情况,具体根据业务逻辑和前端展示需求确定),并添加到ModelAndView对象中。 + model.addObject("areaList", iAreaService.findRootArea()); + + // 返回包含视图名称和各种相关数据的ModelAndView对象,以便进行视图渲染并展示给用户学科详细页面内容。 + return model; } catch (Exception e) { - logger.error(e.getMessage() , e); + // 如果在查询相关数据过程中出现异常,将异常信息记录到日志中,方便后续查看问题原因、进行调试等操作,记录的信息包括异常消息以及详细的异常堆栈信息。 + logger.error(e.getMessage(), e); + // 将视图名称设置为"500",通常用于表示服务器内部出现错误的情况,意味着出现了未预期的异常导致无法正常处理请求并返回正确的视图内容。 model.setViewName("500"); + // 返回包含错误视图名称的ModelAndView对象,可能会渲染出500页面展示给用户,告知出现了服务器内部错误。 return model; } - - } - + + } + + /** + * 处理获取课程树结构数据的GET请求,调用学科服务的getCourseTree方法获取课程树结构信息(以JSONArray形式返回,具体结构和内容由服务层实现决定,可能是树形结构的课程层级关系数据等), + * 将获取到的数据包装在表示成功的Result对象中返回给前端,方便前端根据统一的格式进行解析和使用课程树数据(例如用于展示课程的层级结构、进行课程选择等操作)。 + * + * @return 返回包含课程树结构数据的Result对象,若成功获取则Result对象中的数据字段包含相应的JSONArray形式的课程树信息,若出现问题则可能包含相应的错误提示等信息,前端可根据返回结果进行相应展示或处理。 + */ @RequestMapping(value = {"subject/getCourseTree.html"}, method = RequestMethod.GET) @ResponseBody - public Result getCourseTree(){ + public Result getCourseTree() { + // 调用学科服务的getCourseTree方法获取课程树结构数据,返回的数据格式为JSONArray,其中包含了课程之间的层级关系等相关信息(具体结构和内容由服务层实现决定)。 JSONArray list = iSubjectService.getCourseTree(); + // 将获取到的课程树结构数据包装在表示成功的Result对象中返回给前端,方便前端按照统一的格式进行处理,Result对象中的数据字段存放实际的课程树信息(这里就是JSONArray类型的list)。 return Result.successResult(list); } - - // [{"value":"11","label":"北京市","children":[{"value":"1101","label":"市辖区"}]}] + + /** + * 处理获取级联选择器形式的课程树数据的GET请求,调用学科服务的getCourseCascaderTree方法获取相应的数据(以JSONArray形式返回,其结构可能符合级联选择器所需的格式,包含节点的value、label以及子节点children等信息), + * 将获取到的数据包装在表示成功的Result对象中返回给前端,方便前端根据统一的格式进行解析和使用该课程树数据(例如用于在前端页面的级联选择器中展示课程层级关系,方便用户进行多级选择等操作)。 + * + * @return 返回包含级联选择器形式课程树数据的Result对象,若成功获取则Result对象中的数据字段包含相应的JSONArray形式的课程树信息,若出现问题则可能包含相应的错误提示等信息,前端可根据返回结果进行相应展示或处理。 + */ @RequestMapping(value = {"subject/getCourseCascaderTree"}, method = RequestMethod.GET) @ResponseBody public Result getCourseCascaderTree() { + // 调用学科服务的getCourseCascaderTree方法获取级联选择器形式的课程树数据,返回的数据格式为JSONArray,其结构符合级联选择器所需的格式(例如示例中展示的包含节点的value、label以及子节点children等信息)。 JSONArray list = iSubjectService.getCourseCascaderTree(); + // 将获取到的级联选择器形式的课程树数据包装在表示成功的Result对象中返回给前端,方便前端按照统一的格式进行处理,Result对象中的数据字段存放实际的课程树信息(这里就是JSONArray类型的list)。 return Result.successResult(list); } -} +} \ No newline at end of file diff --git a/tamguo/src/main/java/com/tamguo/web/UEditorController.java b/tamguo/src/main/java/com/tamguo/web/UEditorController.java index 5d0f54d..26dea83 100644 --- a/tamguo/src/main/java/com/tamguo/web/UEditorController.java +++ b/tamguo/src/main/java/com/tamguo/web/UEditorController.java @@ -26,87 +26,168 @@ import com.tamguo.dao.redis.CacheService; import com.tamguo.util.DateUtils; import com.tamguo.util.Setting; +// 标识这是一个Spring的控制器类,主要用于处理与UEditor相关的Web请求,例如返回UEditor的配置信息以及处理图片上传等功能, +// 在图片上传过程中涉及文件的存储操作、根据日期生成文件路径、利用缓存生成唯一文件名编号等逻辑,同时对操作过程中的异常进行了相应处理并返回合适的结果给前端。 @Controller public class UEditorController { - + + // 创建日志记录器,用于记录在处理UEditor相关请求过程中出现的关键信息(如正常的业务日志、文件上传成功或失败信息、异常信息等),方便后续查看日志进行调试、排查问题以及了解系统运行情况。 private Logger logger = org.slf4j.LoggerFactory.getLogger(getClass()); + + // 通过@Value注解注入配置文件中配置的文件存储路径属性值,该路径用于指定上传文件在服务器端存储的基础目录位置,后续会根据日期等信息进一步构建具体的文件存储子目录。 @Value("${file.storage.path}") private String fileStoragePath; + + // 自动注入Setting对象,该对象可能包含了系统的一些全局设置信息(例如域名等相关配置),在处理文件上传后的文件访问路径等相关逻辑时可能会用到其中的配置数据。 @Autowired private Setting setting; + + // 自动注入CacheService,用于与缓存系统进行交互,在这里主要是利用缓存来生成UEditor上传文件的唯一编号,以保证文件名的唯一性,便于文件管理和区分不同时间上传的文件。 @Autowired private CacheService cacheService; + + // 定义一个表示UEditor文件编号无格式的常量字符串,用于后续格式化生成唯一编号时作为格式模板,这里"00000"表示生成的编号会按照五位数字进行格式化填充(前面补0),具体用途在生成文件名编号逻辑中体现。 private static final String UEDITOR_NO_FORMAT = "00000"; + + // 定义一个UEditor相关的文件名前缀常量字符串,用于构建上传文件的唯一文件名,方便在文件系统中对UEditor上传的文件进行统一标识和区分,与后面生成的编号等组合形成完整的文件名。 private static final String UEDITOR_PREFIX = "UEDITOR"; - - - @RequestMapping(value="/ueditor") - @ResponseBody - public JSONObject ueditor(HttpServletRequest request) { - return JSONObject.parseObject(UEditorConfig.UEDITOR_CONFIG); - } - - @RequestMapping(value="/imgUpload") - @ResponseBody - public Ueditor imgUpload(MultipartFile upfile) throws IOException { - if (!upfile.isEmpty()) { + + /** + * 处理获取UEditor配置信息的请求,直接将预定义的UEditor配置字符串(以JSON格式存储在UEditorConfig.UEDITOR_CONFIG中)解析为JSONObject对象并返回给前端, + * 前端可以根据接收到的配置信息来初始化UEditor编辑器,使其具备相应的功能和展示效果(例如配置支持的文件类型、上传接口地址等相关设置)。 + * + * @param request HttpServletRequest对象,虽然在这里暂时未使用,但在一些复杂的业务场景下可能会根据请求信息进行不同的配置返回或者其他相关处理(例如根据用户权限返回不同的配置等情况),此处仅按固定配置返回。 + * @return 返回解析后的JSONObject对象,包含了UEditor的配置信息,前端可根据该信息进行相应的处理和展示。 + */ + @RequestMapping(value = "/ueditor") + @ResponseBody + public JSONObject ueditor(HttpServletRequest request) { + return JSONObject.parseObject(UEditorConfig.UEDITOR_CONFIG); + } + + /** + * 处理图片上传的请求,接收前端传来的MultipartFile类型的文件(upfile),进行文件保存到服务器指定位置的操作, + * 如果文件上传成功,返回包含成功状态及文件访问URL等信息的Ueditor对象给前端;如果文件为空或者上传过程中出现异常,则返回相应的表示失败状态及错误提示信息的Ueditor对象给前端。 + * + * @param upfile 前端传来的要上传的文件对象,类型为MultipartFile,包含了文件的原始信息(如文件名、文件内容的输入流等),用于后续将文件保存到服务器端的指定位置。 + * @return 返回包含文件上传结果信息的Ueditor对象,若上传成功则对象中包含成功状态("SUCCESS")以及生成的文件访问URL等信息,若上传失败则包含相应的错误状态("ERROR")和错误提示标题(如"上传失败"、"File is empty"等),前端可根据返回结果进行相应展示或处理。 + * @throws IOException 如果在文件流读取、写入等I/O操作过程中出现异常则抛出该异常,例如读取上传文件的输入流或者向服务器文件写入内容时出现问题时会抛出此异常。 + */ + @RequestMapping(value = "/imgUpload") + @ResponseBody + public Ueditor imgUpload(MultipartFile upfile) throws IOException { + // 判断上传的文件是否为空,如果不为空则进行后续的文件保存等操作,为空则直接返回表示错误状态(文件为空)的Ueditor对象给前端。 + if (!upfile.isEmpty()) { InputStream in = null; OutputStream out = null; - + try { + // 根据当前日期(年、月、日)构建文件存储的子目录路径,将其与配置的文件存储基础路径(fileStoragePath)拼接起来,形成完整的文件存储目录路径, + // 这样可以按照日期对上传的文件进行分类存储,便于管理和查找不同时间上传的文件。 String path = fileStoragePath + DateUtils.format(new Date(), "yyyyMMdd"); File dir = new File(path); + // 判断文件存储目录是否存在,如果不存在则创建该目录,确保后续可以将文件保存到正确的目录位置。 if (!dir.exists()) dir.mkdirs(); + + // 调用getUEditorNo方法生成一个唯一的文件名编号,该编号结合文件原始名称的后缀(通过截取原始文件名中最后一个"."之后的部分获取后缀),构建出完整的服务器端存储文件名, + // 以此保证文件名在整个系统中的唯一性,便于文件管理和区分不同的上传文件。 String fileName = this.getUEditorNo() + upfile.getOriginalFilename().substring(upfile.getOriginalFilename().lastIndexOf(".")); File serverFile = new File(dir + File.separator + fileName); + + // 获取上传文件的输入流,用于读取文件内容,后续将通过该输入流把文件内容写入到服务器端的目标文件中。 in = upfile.getInputStream(); + // 创建一个指向服务器端目标文件的输出流,用于将从上传文件读取到的内容写入到该文件中,实现文件保存到服务器的操作。 out = new FileOutputStream(serverFile); + + // 创建一个字节数组缓冲区,用于批量读取和写入文件内容,每次读取1024字节的数据,提高文件读写效率(避免逐个字节读写的低效率情况)。 byte[] b = new byte[1024]; int len = 0; + // 通过循环,不断从输入流中读取数据到字节数组缓冲区(每次最多读取1024字节),直到读取完整个文件内容(即读取的字节数小于等于0表示已读完), + // 然后将缓冲区中的数据写入到输出流对应的服务器端文件中,实现文件内容的复制保存操作。 while ((len = in.read(b)) > 0) { out.write(b, 0, len); } + + // 关闭输出流,释放相关资源,确保文件写入操作完成后正确关闭资源,避免资源泄漏等问题。 out.close(); + // 关闭输入流,释放相关资源,确保文件读取操作完成后正确关闭资源,避免资源泄漏等问题。 in.close(); + + // 将服务器端保存文件的绝对路径信息记录到日志中,方便后续查看文件实际存储位置以及排查文件相关的问题(如文件是否保存成功等情况)。 logger.info("Server File Location=" + serverFile.getAbsolutePath()); - Ueditor ueditor = new Ueditor(); - ueditor.setState("SUCCESS"); - ueditor.setUrl(setting.domain + "files" + "/" +DateUtils.format(new Date(), "yyyyMMdd") + "/" + fileName); - return ueditor; + // 创建一个Ueditor对象,用于返回文件上传的结果信息给前端。 + Ueditor ueditor = new Ueditor(); + // 设置文件上传状态为"SUCCESS",表示文件上传成功,前端可以根据该状态进行相应的提示信息展示等处理(例如显示上传成功的提示框)。 + ueditor.setState("SUCCESS"); + // 构建文件的访问URL,将系统配置的域名(通过Setting对象获取)、固定的文件访问路径前缀("files")、按照日期生成的文件存储子目录(通过DateUtils.format生成)以及最终的文件名拼接起来, + // 形成完整的文件访问URL,前端可以使用该URL来访问上传后的文件(例如在编辑器中展示上传的图片等场景)。 + ueditor.setUrl(setting.domain + "files" + "/" + DateUtils.format(new Date(), "yyyyMMdd") + "/" + fileName); + + // 返回包含成功状态及文件访问URL等信息的Ueditor对象给前端,告知文件上传成功以及可以通过返回的URL访问该文件。 + return ueditor; } catch (Exception e) { + // 如果在文件上传过程中出现异常(如文件保存失败、流操作异常等情况),创建一个表示错误状态的Ueditor对象,用于返回错误信息给前端。 Ueditor ueditor = new Ueditor(); - ueditor.setState("ERROR"); - ueditor.setTitle("上传失败"); - return ueditor; + // 设置文件上传状态为"ERROR",表示文件上传失败,前端可以根据该状态进行相应的提示信息展示(例如显示上传失败的提示框)。 + ueditor.setState("ERROR"); + // 设置错误提示标题为"上传失败",用于更详细地告知前端用户文件上传出现问题的大致情况,前端可根据此标题进行更具体的错误提示展示(具体展示逻辑由前端决定)。 + ueditor.setTitle("上传失败"); + + // 返回包含错误状态及错误提示标题的Ueditor对象给前端,告知文件上传失败及失败原因(通过标题提示)。 + return ueditor; } finally { - if (out != null) { + // 在finally块中确保输出流资源能够正确关闭,避免因异常等情况导致资源未释放的问题,先判断输出流是否不为空,如果不为空则关闭输出流,并将其置为null,表示资源已释放。 + if (out!= null) { out.close(); out = null; } - if (in != null) { + // 在finally块中确保输入流资源能够正确关闭,避免因异常等情况导致资源未释放的问题,先判断输入流是否不为空,如果不为空则关闭输入流,并将其置为null,表示资源已释放。 + if (in!= null) { in.close(); in = null; } } } else { + // 如果上传的文件为空,创建一个表示错误状态的Ueditor对象,用于返回错误信息给前端。 Ueditor ueditor = new Ueditor(); - ueditor.setState("ERROR"); - ueditor.setTitle("File is empty"); - return ueditor; + // 设置文件上传状态为"ERROR",表示文件上传失败,前端可以根据该状态进行相应的提示信息展示(例如显示上传失败的提示框)。 + ueditor.setState("ERROR"); + // 设置错误提示标题为"File is empty",明确告知前端用户上传的文件为空,前端可根据此标题进行相应的错误提示展示(具体展示逻辑由前端决定)。 + ueditor.setTitle("File is empty"); + + // 返回包含错误状态及错误提示标题的Ueditor对象给前端,告知文件上传失败及失败原因(文件为空)。 + return ueditor; } - } - + } + + /** + * 生成UEditor上传文件的唯一编号,该编号基于当前日期(精确到月)以及缓存中的自增计数来生成,先按照指定格式("yyyyMM")获取当前日期字符串, + * 然后以固定的前缀(UEDITOR_PREFIX)加上日期字符串作为缓存中的键,利用缓存服务(CacheService)对该键对应的值进行自增操作(保证每次生成的编号是顺序递增且唯一的), + * 最后将自增后的值按照固定的格式(UEDITOR_NO_FORMAT,即五位数字,不足五位前面补0)进行格式化,与前缀组合形成完整的唯一文件名编号并返回。 + * + * @return 返回生成的UEditor上传文件的唯一文件名编号,用于构建上传文件在服务器端存储的文件名,确保文件名的唯一性,便于文件管理和区分不同时间上传的文件。 + */ private String getUEditorNo() { + // 创建一个SimpleDateFormat对象,用于按照"yyyyMM"格式对日期进行格式化,即获取当前日期的年和月信息,用于构建与日期相关的唯一编号部分。 SimpleDateFormat sdf = new SimpleDateFormat("yyyyMM"); String format = sdf.format(new Date()); + + // 创建一个DecimalFormat对象,使用定义的UEDITOR_NO_FORMAT常量作为格式化模板,用于将后续生成的自增编号按照五位数字的格式进行格式化(不足五位前面补0),保证编号格式的一致性和唯一性。 DecimalFormat df = new DecimalFormat(UEDITOR_NO_FORMAT); + + // 构建缓存中的键,将固定的UEDITOR_PREFIX前缀与当前日期格式字符串(format)拼接起来,形成一个唯一标识当前月份的UEditor文件编号相关的键,用于在缓存中进行计数操作。 String key = UEDITOR_PREFIX + format; + + // 通过缓存服务(CacheService)对构建的键对应的值进行自增操作,每次调用该方法时,对应键的值会在原来基础上加1,以此生成顺序递增且唯一的编号值,返回自增后的编号值。 Long incr = cacheService.incr(key); + + // 将自增后的编号值按照定义的格式(通过DecimalFormat进行格式化)进行处理,与固定的UEDITOR_PREFIX前缀再次拼接,形成完整的唯一文件名编号,用于后续构建上传文件的文件名,保证文件名的唯一性。 String avatorNo = UEDITOR_PREFIX + df.format(incr); + return avatorNo; } -} +} \ No newline at end of file diff --git a/tamguo/src/main/java/com/tamguo/web/member/AccountController.java b/tamguo/src/main/java/com/tamguo/web/member/AccountController.java index eac0ff7..f5f989c 100644 --- a/tamguo/src/main/java/com/tamguo/web/member/AccountController.java +++ b/tamguo/src/main/java/com/tamguo/web/member/AccountController.java @@ -35,76 +35,133 @@ import com.tamguo.util.ShiroUtils; import com.tamguo.util.Status; import com.tamguo.util.UploaderMessage; +// 标识这是一个Spring的控制器类,用于处理Web请求并返回相应的视图或数据 @Controller public class AccountController { - + + // 自动注入IMemberService,用于处理会员相关的业务逻辑 @Autowired public IMemberService memberService; + + // 通过配置文件注入文件存储路径属性值,方便后续文件保存操作使用 @Value("${file.storage.path}") private String fileStoragePath; + + // 自动注入Setting,可能用于获取一些系统配置相关的设置信息 @Autowired private Setting setting; + + // 自动注入CacheService,用于缓存相关操作,例如在生成头像编号时使用缓存来保证编号的唯一性和递增性 @Autowired private CacheService cacheService; + // 定义头像编号的默认格式化字符串,用于格式化生成的编号,保证编号格式统一 private static final String AVATOR_NO_FORMAT = "00000"; + // 定义头像编号的前缀,方便识别头像文件相关的编号 private static final String AVATOR_PREFIX = "MTX"; - + + // 创建日志记录器,用于记录该类中关键操作的日志信息,方便调试和问题排查 public Logger logger = LoggerFactory.getLogger(getClass()); + /** + * 处理会员账户页面的GET请求,设置视图名称并从会话中获取当前会员信息,再通过会员服务查询更详细的会员信息后添加到模型中返回视图 + * + * @param model 用于传递数据到视图的ModelAndView对象 + * @param session 当前的HttpSession对象,可从中获取会话相关的属性,如当前会员信息 + * @return 返回包含视图名称和会员相关数据的ModelAndView对象,以便渲染相应的视图展示给用户 + */ @RequestMapping(value = "member/account", method = RequestMethod.GET) - public ModelAndView index(ModelAndView model , HttpSession session){ + public ModelAndView index(ModelAndView model, HttpSession session) { model.setViewName("member/account"); MemberEntity member = (MemberEntity) session.getAttribute("currMember"); - model.addObject("member" , memberService.findByUid(member.getUid())); + model.addObject("member", memberService.findByUid(member.getUid())); return model; } - + + /** + * 处理会员信息更新的POST请求,设置会员的用户ID(可能从安全框架获取当前登录用户ID),然后调用会员服务更新会员信息,并返回更新成功的结果 + * + * @param member 包含要更新的会员信息的MemberEntity对象,通过请求体接收 + * @return 返回表示更新操作结果的Result对象,如果更新成功则包含更新后的会员信息 + */ @RequestMapping(value = "member/account/update", method = RequestMethod.POST) @ResponseBody - public Result updateMember(@RequestBody MemberEntity member){ + public Result updateMember(@RequestBody MemberEntity member) { member.setUid(ShiroUtils.getUserId()); memberService.updateMember(member); return Result.successResult(member); } - + + /** + * 处理会员密码页面的GET请求,仅设置视图名称,用于返回相应的密码修改页面视图给用户 + * + * @param model 用于传递数据到视图的ModelAndView对象(此处未添加额外数据) + * @return 返回包含视图名称的ModelAndView对象,以便渲染密码修改页面视图 + */ @RequestMapping(value = "member/password", method = RequestMethod.GET) - public ModelAndView password(ModelAndView model){ + public ModelAndView password(ModelAndView model) { model.setViewName("member/password"); return model; } - + + /** + * 处理会员密码更新的POST请求,调用会员服务来更新会员密码,并返回相应的更新结果 + * + * @param member 包含要更新密码相关信息的MemberEntity对象,通过请求体接收 + * @return 返回表示密码更新操作结果的Result对象 + */ @RequestMapping(value = "member/password/update", method = RequestMethod.POST) @ResponseBody - public Result updatePwd(@RequestBody MemberEntity member){ + public Result updatePwd(@RequestBody MemberEntity member) { return memberService.updatePwd(member); } - + + /** + * 处理会员上传文件的POST请求,接收上传的文件以及HttpServletRequest对象,实现文件上传功能,包括创建文件存储目录、生成文件名、文件流读写等操作,最后返回文件上传的结果信息 + * + * @param file 上传的MultipartFile类型文件对象,通过请求参数接收 + * @param request 当前的HttpServletRequest对象,可获取请求相关的一些信息(此处可能未使用到,但在某些场景下可能会用到) + * @return 返回包含文件上传状态、提示信息、文件路径等相关信息的UploaderMessage对象,告知前端文件上传的结果情况 + * @throws IOException 如果在文件读写等操作过程中出现I/O异常则抛出 + */ @RequestMapping(value = "/member/uploadFile", method = RequestMethod.POST) @ResponseBody - public UploaderMessage uploadFileHandler(@RequestParam("file") MultipartFile file,HttpServletRequest request) throws IOException { + public UploaderMessage uploadFileHandler(@RequestParam("file") MultipartFile file, HttpServletRequest request) throws IOException { + // 判断上传的文件是否为空,如果为空则直接返回文件为空的错误提示信息 if (!file.isEmpty()) { InputStream in = null; OutputStream out = null; - + try { + // 根据当前日期构建文件存储的路径,方便按日期分类存储文件 String path = fileStoragePath + DateUtils.format(new Date(), "yyyyMMdd"); File dir = new File(path); + // 如果目录不存在,则创建相应的目录,确保文件有存储的位置 if (!dir.exists()) dir.mkdirs(); + + // 生成头像文件名,结合缓存获取的编号以及原文件名的后缀组成完整的文件名 String avatorName = this.getAvatorNo() + file.getOriginalFilename().substring(file.getOriginalFilename().lastIndexOf(".")); + // 获取文件输入流,用于读取上传文件的内容 in = file.getInputStream(); + // 创建文件输出流,用于将读取的文件内容写入到服务器指定的存储位置 out = new FileOutputStream(path + "/" + avatorName); + byte[] b = new byte[1024]; int len = 0; + // 通过循环读取输入流中的文件内容,并写入到输出流中,实现文件的复制保存操作 while ((len = in.read(b)) > 0) { out.write(b, 0, len); } + // 关闭输出流,释放资源 out.close(); + // 关闭输入流,释放资源 in.close(); + // 记录文件在服务器上的存储位置信息到日志中,方便后续查看和排查问题 logger.info("Server File Location=" + path + avatorName); + // 创建表示文件上传成功的消息对象,并设置相应的成功状态、提示信息、文件路径以及文件所在的域名等信息 UploaderMessage msg = new UploaderMessage(); msg.setStatus(Status.SUCCESS); msg.setStatusMsg("File upload success"); @@ -112,30 +169,38 @@ public class AccountController { msg.setFileDomain(setting.domain); return msg; } catch (Exception e) { + // 如果在文件上传过程中出现异常,创建表示文件上传失败的消息对象,并设置相应的错误状态和提示信息,然后返回该对象告知前端上传失败 UploaderMessage msg = new UploaderMessage(); msg.setStatus(Status.ERROR); msg.setError("File upload file"); return msg; } finally { - if (out != null) { + // 在最终块中确保输出流资源被正确关闭,避免资源泄露 + if (out!= null) { out.close(); out = null; } - if (in != null) { + // 在最终块中确保输入流资源被正确关闭,避免资源泄露 + if (in!= null) { in.close(); in = null; } } } else { + // 如果上传的文件为空,创建表示文件为空的错误消息对象,并设置相应的错误状态和提示信息,然后返回该对象告知前端文件为空 UploaderMessage msg = new UploaderMessage(); msg.setStatus(Status.ERROR); msg.setError("File is empty"); return msg; } } - + /** + * 生成头像编号的方法,先根据当前日期格式化得到年月格式的字符串作为缓存的键前缀,然后通过缓存服务获取自增的编号,再按照指定格式进行格式化,最后拼接前缀生成完整的头像编号 + * + * @return 返回生成的头像编号字符串 + */ private String getAvatorNo() { SimpleDateFormat sdf = new SimpleDateFormat("yyyyMM"); String format = sdf.format(new Date()); @@ -145,4 +210,4 @@ public class AccountController { String avatorNo = AVATOR_PREFIX + df.format(incr); return avatorNo; } -} +} \ No newline at end of file diff --git a/tamguo/src/main/java/com/tamguo/web/member/MemberPaperController.java b/tamguo/src/main/java/com/tamguo/web/member/MemberPaperController.java index 8405e7e..f649278 100644 --- a/tamguo/src/main/java/com/tamguo/web/member/MemberPaperController.java +++ b/tamguo/src/main/java/com/tamguo/web/member/MemberPaperController.java @@ -19,109 +19,182 @@ import com.tamguo.service.IPaperService; import com.tamguo.util.ExceptionSupport; import com.tamguo.util.Result; +// 标识这是一个Spring的控制器类,用于处理Web请求并返回相应的视图或数据 @Controller public class MemberPaperController { - + + // 自动注入IPaperService,用于处理试卷相关的业务逻辑,比如试卷的查询、添加、修改、删除等操作 @Autowired private IPaperService iPaperService; - + /** + * 处理会员试卷页面的GET请求,设置视图名称为"member/paperList",用于返回相应的试卷列表页面视图给用户 + * + * @param model 用于传递数据到视图的ModelAndView对象(此处未添加额外数据) + * @param session 当前的HttpSession对象,可从中获取会话相关的属性(此处未使用到相关属性) + * @return 返回包含视图名称的ModelAndView对象,以便渲染试卷列表页面视图 + */ @RequestMapping(value = "/member/paper", method = RequestMethod.GET) - public ModelAndView paper(ModelAndView model, HttpSession session){ + public ModelAndView paper(ModelAndView model, HttpSession session) { model.setViewName("member/paperList"); return model; } - + + /** + * 处理查找试卷的GET请求,根据传入的试卷ID调用试卷服务查找对应的试卷信息,然后将查找结果包装在表示成功的Result对象中返回 + * + * @param paperId 要查找的试卷的唯一标识符 + * @return 返回包含查找结果的Result对象,如果查找成功则Result对象中包含对应的试卷信息 + */ @RequestMapping(value = "/member/findPaper", method = RequestMethod.GET) @ResponseBody public Result findPaper(String paperId) { return Result.successResult(iPaperService.find(paperId)); } - - @RequestMapping(value = "member/paper/list" , method = RequestMethod.GET) + + /** + * 处理获取试卷列表的GET请求,从会话中获取当前会员信息,根据传入的查询参数(试卷名称、页码、每页数量)构建分页对象,调用试卷服务获取会员对应的试卷列表信息, + * 最后将列表数据按照特定格式包装在Map中返回,以便前端进行展示(可能用于支持类似表格形式展示试卷列表,符合jqGrid等前端组件的数据格式要求) + * + * @param name 试卷名称,用于筛选查找试卷(可为空,表示不按名称筛选) + * @param page 当前页码,用于分页查询 + * @param limit 每页显示的记录数量,用于分页查询 + * @param session 当前的HttpSession对象,从中获取当前会员信息,以确定是哪个会员的试卷列表 + * @return 返回包含试卷列表数据、总记录数、每页数量、当前页码、总页数等信息的Map对象,符合前端展示要求的格式 + */ + @RequestMapping(value = "member/paper/list", method = RequestMethod.GET) @ResponseBody - public Map paperList(String name , Integer page , Integer limit , HttpSession session){ - MemberEntity member = ((MemberEntity)session.getAttribute("currMember")); + public Map paperList(String name, Integer page, Integer limit, HttpSession session) { + MemberEntity member = ((MemberEntity) session.getAttribute("currMember")); Page p = new Page<>(); p.setCurrent(page); p.setSize(limit); - Page list = iPaperService.memberPaperList(name, member.getUid() , p); + Page list = iPaperService.memberPaperList(name, member.getUid(), p); return Result.jqGridResult(list.getRecords(), list.getTotal(), limit, page, list.getPages()); } - - @RequestMapping(value="member/paperList/addPaperQuestionInfo",method=RequestMethod.POST) + + /** + * 处理添加试卷问题信息的POST请求,从请求体中的JSON数据中解析出相关参数(试卷ID、标题、类型等),根据类型获取对应的描述信息,然后调用试卷服务添加试卷问题信息, + * 如果操作成功则返回表示成功的Result对象,若出现异常则通过异常处理工具类返回相应的异常结果 + * + * @param data 包含试卷问题信息相关参数的JSONObject对象,通过请求体接收 + * @return 返回表示添加操作结果的Result对象,成功时包含相应提示信息,出现异常时返回异常处理后的结果 + */ + @RequestMapping(value = "member/paperList/addPaperQuestionInfo", method = RequestMethod.POST) @ResponseBody - public Result addPaperQuestionInfo(@RequestBody JSONObject data){ + public Result addPaperQuestionInfo(@RequestBody JSONObject data) { try { - String paperId ; String title ; String name ;String type; + String paperId; + String title; + String name; + String type; paperId = data.getString("uid"); title = data.getString("title"); type = data.getString("type"); name = QuestionType.getQuestionType(type).getDesc(); - iPaperService.addPaperQuestionInfo(paperId , title , name , type); + iPaperService.addPaperQuestionInfo(paperId, title, name, type); return Result.result(0, null, "修改成功"); } catch (Exception e) { return ExceptionSupport.resolverResult("添加questionInfo", this.getClass(), e); } } - + + /** + * 处理更新试卷问题信息的POST请求,从请求体中的JSON数据中解析出相关参数(试卷ID、标题、类型、问题信息唯一标识符等),根据类型获取对应的描述信息, + * 然后调用试卷服务更新试卷问题信息,如果操作成功则返回表示成功的Result对象,若出现异常则通过异常处理工具类返回相应的异常结果 + * + * @param data 包含试卷问题信息相关更新参数的JSONObject对象,通过请求体接收 + * @return 返回表示更新操作结果的Result对象,成功时包含相应提示信息,出现异常时返回异常处理后的结果 + */ @RequestMapping("member/paperList/updatePaperQuestionInfo.html") @ResponseBody - public Result updatePaperQuestionInfo(@RequestBody JSONObject data){ + public Result updatePaperQuestionInfo(@RequestBody JSONObject data) { try { - String paperId ; String title ; String name ; String type ; String uid; + String paperId; + String title; + String name; + String type; + String uid; paperId = data.getString("uid"); title = data.getString("title"); type = data.getString("type"); name = QuestionType.getQuestionType(type).getDesc(); uid = data.getString("infoUid"); - iPaperService.updatePaperQuestionInfo(paperId , title , name , type , uid); + iPaperService.updatePaperQuestionInfo(paperId, title, name, type, uid); return Result.result(0, null, "修改成功"); } catch (Exception e) { return ExceptionSupport.resolverResult("修改questionInfo", this.getClass(), e); } } - + + /** + * 处理删除试卷的请求,尝试调用试卷服务删除指定ID的试卷,如果操作成功则返回相应的删除结果(可能是成功的Result对象),若出现异常则通过异常处理工具类返回相应的异常结果 + * + * @param paperId 要删除的试卷的唯一标识符 + * @return 返回表示删除操作结果的Result对象,成功时包含相应提示信息,出现异常时返回异常处理后的结果 + */ @RequestMapping("member/paperList/deletePaper") @ResponseBody - public Result deletePaper(String paperId){ + public Result deletePaper(String paperId) { try { return iPaperService.deletePaper(paperId); } catch (Exception e) { return ExceptionSupport.resolverResult("删除试卷", this.getClass(), e); } } - + + /** + * 处理删除试卷问题信息(可能是试卷下的子项相关信息,从名称推测)的请求,尝试调用试卷服务根据试卷ID和问题信息唯一标识符删除对应的信息, + * 如果操作成功则返回相应的删除结果(可能是成功的Result对象),若出现异常则通过异常处理工具类返回相应的异常结果 + * + * @param paperId 试卷的唯一标识符,用于定位到对应的试卷 + * @param uid 试卷问题信息的唯一标识符,用于确定要删除的具体子项信息 + * @return 返回表示删除操作结果的Result对象,成功时包含相应提示信息,出现异常时返回异常处理后的结果 + */ @RequestMapping("member/paperList/deletePaperQuestionInfoBtn") @ResponseBody - public Result deletePaperQuestionInfoBtn(String paperId , String uid){ + public Result deletePaperQuestionInfoBtn(String paperId, String uid) { try { - return iPaperService.deletePaperQuestionInfoBtn(paperId , uid); + return iPaperService.deletePaperQuestionInfoBtn(paperId, uid); } catch (Exception e) { return ExceptionSupport.resolverResult("删除子卷", this.getClass(), e); } } - - @RequestMapping(value="member/paperList/addPaper.html",method=RequestMethod.POST) + + /** + * 处理添加试卷的POST请求,从会话中获取当前会员信息并设置为试卷的创建者ID,然后调用试卷服务添加试卷,如果操作成功则返回表示成功的Result对象, + * 若出现异常则通过异常处理工具类返回相应的异常结果 + * + * @param paper 包含试卷相关信息的PaperEntity对象,通过请求体接收 + * @param session 当前的HttpSession对象,从中获取当前会员信息,用于设置试卷创建者ID + * @return 返回表示添加操作结果的Result对象,成功时包含添加后的试卷信息及相应提示信息,出现异常时返回异常处理后的结果 + */ + @RequestMapping(value = "member/paperList/addPaper.html", method = RequestMethod.POST) @ResponseBody - public Result addPaper(@RequestBody PaperEntity paper,HttpSession session){ + public Result addPaper(@RequestBody PaperEntity paper, HttpSession session) { try { MemberEntity member = (MemberEntity) session.getAttribute("currMember"); paper.setCreaterId(member.getUid()); iPaperService.addPaper(paper); - return Result.result(Result.SUCCESS_CODE, paper, "添加成功"); + return Result.result(Result.SUCCESS_CODE, paper, "添加成功"); } catch (Exception e) { return ExceptionSupport.resolverResult("添加试卷", this.getClass(), e); } } - - @RequestMapping(value="member/paperList/updatePaper.html",method=RequestMethod.POST) + + /** + * 处理更新试卷的POST请求,尝试调用试卷服务更新传入的试卷信息,如果操作成功则返回相应的更新结果(可能是成功的Result对象),若出现异常则通过异常处理工具类返回相应的异常结果 + * + * @param paper 包含要更新的试卷相关信息的PaperEntity对象,通过请求体接收 + * @return 返回表示更新操作结果的Result对象,成功时包含相应提示信息,出现异常时返回异常处理后的结果 + */ + @RequestMapping(value = "member/paperList/updatePaper.html", method = RequestMethod.POST) @ResponseBody - public Result updatePaper(@RequestBody PaperEntity paper){ + public Result updatePaper(@RequestBody PaperEntity paper) { try { return iPaperService.updatePaper(paper); } catch (Exception e) { return ExceptionSupport.resolverResult("修改试卷", this.getClass(), e); } } -} +} \ No newline at end of file diff --git a/tamguo/src/main/java/com/tamguo/web/member/QuestionController.java b/tamguo/src/main/java/com/tamguo/web/member/QuestionController.java index 07f4237..de04275 100644 --- a/tamguo/src/main/java/com/tamguo/web/member/QuestionController.java +++ b/tamguo/src/main/java/com/tamguo/web/member/QuestionController.java @@ -17,80 +17,146 @@ import com.tamguo.service.IQuestionService; import com.tamguo.util.ExceptionSupport; import com.tamguo.util.Result; -@Controller(value="memberQuestionController") +// 标识这是一个Spring的控制器类,名为"memberQuestionController",用于处理与会员相关的试题操作的Web请求,并返回相应的视图或数据 +@Controller(value = "memberQuestionController") public class QuestionController { - + + // 自动注入IQuestionService,用于处理试题相关的业务逻辑,例如试题的添加、查询、更新、删除等操作 @Autowired private IQuestionService iQuestionService; + + // 自动注入IPaperService,用于获取试卷相关信息,可能在试题与试卷关联等操作中会用到 @Autowired private IPaperService iPaperService; - + + /** + * 处理添加试题页面的GET请求,根据传入的试卷ID,通过试卷服务获取对应的试卷信息,设置视图名称为"member/addQuestion", + * 并将试卷信息添加到模型中,最后返回包含视图名称和试卷信息的ModelAndView对象,以便渲染相应的添加试题页面视图给用户 + * + * @param paperId 要添加试题所属的试卷的唯一标识符,用于查找对应的试卷信息 + * @param model 用于传递数据到视图的ModelAndView对象,在这里将试卷信息添加到该对象中 + * @return 返回包含视图名称和试卷信息的ModelAndView对象,用于渲染添加试题页面视图 + */ @RequestMapping(value = "/member/addQuestion", method = RequestMethod.GET) - public ModelAndView index(String paperId , ModelAndView model){ + public ModelAndView index(String paperId, ModelAndView model) { model.setViewName("member/addQuestion"); model.addObject("paper", iPaperService.find(paperId)); return model; } - + + /** + * 处理提交试题的POST请求,尝试调用试题服务添加传入的试题信息,如果操作成功则返回相应的添加结果(可能是成功的Result对象), + * 若出现异常则通过异常处理工具类返回相应的异常结果,告知前端添加试题操作出现问题 + * + * @param question 包含要添加的试题相关信息的QuestionEntity对象,通过请求参数接收 + * @return 返回表示添加试题操作结果的Result对象,成功时包含相应提示信息,出现异常时返回异常处理后的结果 + */ @RequestMapping(value = "/member/submitQuestion", method = RequestMethod.POST) @ResponseBody - public Result submitQuestion(QuestionEntity question){ + public Result submitQuestion(QuestionEntity question) { try { return iQuestionService.addQuestion(question); } catch (Exception e) { return ExceptionSupport.resolverResult("添加试题", this.getClass(), e); } } - + + /** + * 处理试题列表页面的GET请求,根据传入的试卷ID,通过试卷服务获取对应的试卷信息,将试卷信息添加到模型中,设置视图名称为"member/questionList", + * 最后返回包含视图名称和试卷信息的ModelAndView对象,以便渲染相应的试题列表页面视图给用户 + * + * @param paperId 要展示其试题列表的试卷的唯一标识符,用于查找对应的试卷信息 + * @param model 用于传递数据到视图的ModelAndView对象,在这里将试卷信息添加到该对象中 + * @return 返回包含视图名称和试卷信息的ModelAndView对象,用于渲染试题列表页面视图 + */ @RequestMapping(value = "/member/questionList", method = RequestMethod.GET) - public ModelAndView questionList(String paperId , ModelAndView model){ + public ModelAndView questionList(String paperId, ModelAndView model) { model.addObject("paper", iPaperService.find(paperId)); model.setViewName("member/questionList"); return model; } - - @RequestMapping(value = "/member/queryQuestionList" , method=RequestMethod.POST) + + /** + * 处理查询试题列表的POST请求,从请求体中的JSON数据中解析出查询相关的参数(试题类型、唯一标识符、内容、所属试卷ID、页码、每页数量等), + * 根据这些参数构建分页对象并调用试题服务查询符合条件的试题列表信息,最后将列表数据按照特定格式包装在Map中返回,以便前端进行展示(可能用于支持类似表格形式展示试题列表,符合jqGrid等前端组件的数据格式要求) + * + * @param data 包含试题查询相关参数的JSONObject对象,通过请求体接收 + * @return 返回包含试题列表数据、总记录数、每页数量、当前页码、总页数等信息的Map对象,符合前端展示要求的格式 + */ + @RequestMapping(value = "/member/queryQuestionList", method = RequestMethod.POST) @ResponseBody - public Map queryQuestionList(@RequestBody JSONObject data){ - String questionType ; String uid ; String content ; String paperId ; - Integer page ; Integer limit; + public Map queryQuestionList(@RequestBody JSONObject data) { + String questionType; + String uid; + String content; + String paperId; + Integer page; + Integer limit; questionType = data.getString("questionType"); uid = data.getString("uid"); - content = data.getString("content"); + content = data.getString("questionContent"); paperId = data.getString("paperId"); page = data.getInteger("page"); limit = data.getInteger("limit"); Page p = new Page<>(); p.setCurrent(page); p.setSize(limit); - Page list = iQuestionService.queryQuestionList(questionType , uid , content , paperId , p); + Page list = iQuestionService.queryQuestionList(questionType, uid, content, paperId, p); return Result.jqGridResult(list.getRecords(), list.getTotal(), limit, page, list.getPages()); } - + + /** + * 处理编辑试题页面的GET请求,根据传入的试卷ID和试题ID,通过试卷服务获取对应的试卷信息,设置视图名称为"member/editQuestion", + * 并将试卷信息以及试题ID添加到模型中,最后返回包含视图名称、试卷信息和试题ID的ModelAndView对象,以便渲染相应的编辑试题页面视图给用户 + * + * @param paperId 要编辑的试题所属的试卷的唯一标识符,用于查找对应的试卷信息 + * @param questionId 要编辑的试题的唯一标识符,用于在编辑页面定位到具体的试题 + * @param model 用于传递数据到视图的ModelAndView对象,在这里将试卷信息和试题ID添加到该对象中 + * @return 返回包含视图名称、试卷信息和试题ID的ModelAndView对象,用于渲染编辑试题页面视图 + */ @RequestMapping(value = "/member/editQuestion", method = RequestMethod.GET) - public ModelAndView editQuestion(String paperId , String questionId , ModelAndView model){ + public ModelAndView editQuestion(String paperId, String questionId, ModelAndView model) { model.setViewName("member/editQuestion"); model.addObject("paper", iPaperService.find(paperId)); - model.addObject("questionId" , questionId); + model.addObject("questionId", questionId); return model; } - + + /** + * 处理获取试题信息的GET请求,根据传入的试题ID调用试题服务查找对应的试题信息,然后将查找结果包装在表示成功的Result对象中返回 + * + * @param questionId 要获取信息的试题的唯一标识符 + * @return 返回包含查找结果的Result对象,如果查找成功则Result对象中包含对应的试题信息 + */ @RequestMapping(value = "/member/getQuestion", method = RequestMethod.GET) @ResponseBody public Result getQuestion(String questionId) { return Result.successResult(iQuestionService.select(questionId)); } - + + /** + * 处理更新试题的POST请求,尝试调用试题服务更新传入的试题信息,如果操作成功则返回相应的更新结果(可能是成功的Result对象), + * 若出现异常则通过异常处理工具类返回相应的异常结果,告知前端更新试题操作出现问题 + * + * @param question 包含要更新的试题相关信息的QuestionEntity对象,通过请求参数接收 + * @return 返回表示更新试题操作结果的Result对象,成功时包含相应提示信息,出现异常时返回异常处理后的结果 + */ @RequestMapping(value = "/member/updateQuestion", method = RequestMethod.POST) @ResponseBody public Result updateQuestion(QuestionEntity question) { return iQuestionService.updateQuestion(question); } - - + + /** + * 处理删除试题的GET请求,尝试调用试题服务根据传入的试题唯一标识符删除对应的试题,如果操作成功则返回相应的删除结果(可能是成功的Result对象), + * 若出现异常则通过异常处理工具类返回相应的异常结果,告知前端删除试题操作出现问题 + * + * @param uid 要删除的试题的唯一标识符,用于定位到具体要删除的试题 + * @return 返回表示删除试题操作结果的Result对象,成功时包含相应提示信息,出现异常时返回异常处理后的结果 + */ @RequestMapping(value = "/member/deleteQuestion", method = RequestMethod.GET) @ResponseBody public Result deleteQuestion(@RequestBody String uid) { return iQuestionService.delete(uid); } -} +} \ No newline at end of file diff --git a/tamguo/src/main/java/com/tamguo/web/teacher/JoinusController.java b/tamguo/src/main/java/com/tamguo/web/teacher/JoinusController.java index 37ca3ea..23abbf9 100644 --- a/tamguo/src/main/java/com/tamguo/web/teacher/JoinusController.java +++ b/tamguo/src/main/java/com/tamguo/web/teacher/JoinusController.java @@ -15,18 +15,34 @@ import com.tamguo.model.TeacherEntity; import com.tamguo.service.ITeacherService; import com.tamguo.util.Result; +// 标识这是一个Spring的控制器类,用于处理与教师相关的Web请求,并返回相应的视图或数据 @Controller public class JoinusController { - + + // 自动注入ITeacherService,用于处理教师相关的业务逻辑,比如根据手机号获取教师信息、教师注册(加入)等操作 @Autowired private ITeacherService iTeacherService; + /** + * 处理教师注册(加入)页面的GET请求,设置视图名称为"teacher/joinus",用于返回相应的教师注册页面视图给用户,此方法主要是进行视图的初始化设置,暂未涉及更多业务逻辑处理 + * + * @param model 用于传递数据到视图的ModelAndView对象(此处目前未添加额外数据) + * @param session 当前的HttpSession对象,可从中获取会话相关的属性(此处暂时未使用到相关属性) + * @return 返回包含视图名称的ModelAndView对象,以便渲染教师注册页面视图 + */ @RequestMapping(value = "teacher/joinus", method = RequestMethod.GET) - public ModelAndView register(ModelAndView model , HttpSession session) { + public ModelAndView register(ModelAndView model, HttpSession session) { model.setViewName("teacher/joinus"); return model; - } - + } + + /** + * 处理获取教师信息的POST请求,从请求体传入的Map参数中获取手机号(mobile)和验证码(verifyCode)信息, + * 然后调用教师服务根据手机号和验证码获取对应的教师信息,并返回相应的查询结果(通过Result对象包装,可能包含教师信息或者错误提示等情况) + * + * @param param 包含查询教师信息所需参数(手机号和验证码)的Map对象,通过请求体接收 + * @return 返回表示查询教师信息操作结果的Result对象,其中可能包含查找到的教师信息或者表示查询失败等情况的相关提示信息 + */ @RequestMapping(value = "teacher/info", method = RequestMethod.POST) @ResponseBody public Result getTeacher(@RequestBody Map param) { @@ -35,11 +51,18 @@ public class JoinusController { Result result = iTeacherService.getTeacherByMobile(mobile, verifyCode); return result; } - + + /** + * 处理教师注册(加入)的POST请求,接收包含教师相关信息的TeacherEntity对象,调用教师服务的joinus方法进行教师注册(加入)操作, + * 注册成功后返回表示成功的Result对象(此处成功返回的Result对象中未携带额外数据,仅表示操作成功) + * + * @param teacher 包含要注册(加入)的教师相关信息的TeacherEntity对象,通过请求体接收 + * @return 返回表示教师注册(加入)操作结果的Result对象,成功时表示操作成功,若服务层出现异常等情况可能需要进一步处理来返回相应的错误提示信息 + */ @RequestMapping(value = "teacher/joinus", method = RequestMethod.POST) @ResponseBody public Result teacherJoinus(@RequestBody TeacherEntity teacher) { iTeacherService.joinus(teacher); return Result.successResult(null); } -} +} \ No newline at end of file