2024.10月7~10日 进一步完善《电信资费管理系统》

 一、新增的模块:

在原项目基础上,新增加了以下功能:

1、增加AspectJ 框架的AOP 异常记录和事务管理模块。

2、增加SpringMVC的拦截器,实现登录 控制页面访问权限。

3、增加 Logback日志框架,记录日志。

4、增加动态验证码(CAPTCHA)并验证功能。

5、增加所有页面的表单验证功能。

二、完善项目结构:

前端项目结构无需改变。

更新后端结构:

aop包:aop包下是ExceptionLogAspect.java切面类,实现了 异常记录和记录日志、事务管理等功能

interceptor包:包含RoleInterceptor.java拦截器,实现了根据登录身份进行权限检查从而实现控制访问不同登录角色的界面

  

三、新增模块实现:

1、AspectJ 框架AOP 事务管理 和  记录日志

(1)pom.xml中添加依赖坐标:(AspectJ和Logback)

<!-- AspectJ -->
<dependency>
  <groupId>org.aspectj</groupId>
  <artifactId>aspectjweaver</artifactId>
  <version>1.9.19</version>
</dependency>

<!-- 日志框架(SLF4J 和 Logback) -->
<dependency>
  <groupId>org.slf4j</groupId>
  <artifactId>slf4j-api</artifactId>
  <version>1.7.36</version>
</dependency>
<dependency>
  <groupId>ch.qos.logback</groupId>
  <artifactId>logback-classic</artifactId>
  <version>1.2.11</version>
</dependency>

(2)日志配置文件:logback.xml

<?xml version="1.0" encoding="UTF-8"?>
<!--CONSOLE:将日志输出到控制台。-->

<!--FILE:将日志输出到文件 logs/application.log,并按天滚动生成新的日志文件。-->

<!--root:设置根日志级别为 INFO,并绑定到 CONSOLE 和 FILE Appender。-->

<!--logger:为 ExceptionLogAspect 类单独设置日志级别为 ERROR,仅记录错误日志。-->
<configuration>

    <!-- Console Appender -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss} %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <!-- 文件Appender -->
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>logs/application.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- 每天生成一个新的日志文件 -->
            <fileNamePattern>logs/application.%d{yyyy-MM-dd}.log</fileNamePattern>
            <!-- 保留30天的历史日志 -->
            <maxHistory>30</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss} %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <!-- 日志级别和Appender绑定 -->
    <root level="INFO">
        <appender-ref ref="CONSOLE" />
        <appender-ref ref="FILE" />
    </root>

    <!-- 为AOP切面单独设置日志级别-->
    <logger name="com.ssm.netctoss.aop.ExceptionLogAspect" level="ERROR" additivity="false">
        <appender-ref ref="FILE" />
    </logger>

</configuration>

(3)在spring-ioc.xml配置文件中,新增配置SpringMVC拦截器:

http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/tx
https://www.springframework.org/schema/tx/spring-tx.xsd
<!-- 启用AOP -->
<aop:aspectj-autoproxy/>

<!-- 启用事务管理 -->
<tx:annotation-driven transaction-manager="transactionManager"/>

<!-- 配置事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"/>
</bean>

 完整的spring-ioc配置代码如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="
           http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans.xsd
           http://www.springframework.org/schema/context
           https://www.springframework.org/schema/context/spring-context.xsd
           http://www.springframework.org/schema/aop
           https://www.springframework.org/schema/aop/spring-aop.xsd
           http://www.springframework.org/schema/tx
           https://www.springframework.org/schema/tx/spring-tx.xsd">

    <!-- 开启注解扫描,比如扫描Dao, Service, AOP切面 -->
    <context:component-scan base-package="com.ssm.netctoss"/>

    <!-- 配置加载jdbc.properties文件 -->
    <context:property-placeholder location="classpath:jdbc.properties"/>

    <!-- 配置druid的连接池 -->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="${jdbc.driver}"></property>
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>

    <!-- 配置SqlSessionFactory的bean -->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <!-- 绑定数据源 -->
        <property name="dataSource" ref="dataSource"></property>
        <!-- 加载mybatis的核心配置文件 -->
        <property name="configLocation" value="classpath:mybatis-config.xml"></property>
        <!-- 配置加载各个pojo对应的XXXXMapper.xml -->
        <property name="mapperLocations"  value="classpath:com/ssm/netctoss/mapper/*.xml"/>

        <property name="typeAliasesPackage" value="com.ssm.netctoss.pojo"/>
    </bean>

    <!-- 配置可以扫描mapper/dao接口的类型 -->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"></property>
        <property name="basePackage" value="com.ssm.netctoss.mapper"></property>
    </bean>

    <!-- 启用AOP -->
    <aop:aspectj-autoproxy/>

    <!-- 启用事务管理 -->
    <tx:annotation-driven transaction-manager="transactionManager"/>

    <!-- 配置事务管理器 -->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>

</beans>

(4)举例:用service层进行切面:

建立aop包,包中建立 ExceptionLogAspect.java切面类:

/**
 * 异常记录切面
 * @Aspect:标记该类为切面。
 *
 * @Component:将切面类注册为 Spring Bean。
 *
 * @AfterThrowing:在目标方法抛出异常后执行通知。
 *
 * pointcut:匹配 com.ssm.netctoss.service 包及其子包中的所有方法。
 *
 * logAfterThrowing:记录异常信息,包括方法名和异常详情。
 */
@Aspect
@Component
public class ExceptionLogAspect {

    private static final Logger logger = LoggerFactory.getLogger(ExceptionLogAspect.class);

    /**
     * 定义切点:匹配 service 包及其子包中的所有方法
     */
    @AfterThrowing(pointcut = "execution(* com.ssm.netctoss.service..*(..))", throwing = "ex")
    public void logAfterThrowing(JoinPoint joinPoint, Throwable ex) {
        // 获取方法签名
        String methodName = joinPoint.getSignature().toShortString();
        // 获取异常信息
        String errorMessage = ex.getMessage();
        // 记录日志
        logger.error("Exception in {}: {}", methodName, errorMessage, ex);
    }
}

解释:

     使用Spring AOP实现的异常日志记录切面。它定义了一个切点,匹配com.ssm.netctoss.service包及其子包中的所有方法。这些方法抛出异常时,切面会记录异常日志。

     切点定义使用了@AfterThrowing注解,它表示在方法抛出异常后执行。pointcut属性指定了切点,用于匹配需要记录异常日志的方法。throwing属性指定了接收异常的参数名,这里命名为ex

  logAfterThrowing方法是切面的通知方法,它接收两个参数:JoinPoint对象和异常对象。JoinPoint对象包含了方法签名等信息,可以通过调用它的getSignature()方法获取方法签名。异常对象可以通过ex参数获取。

logAfterThrowing方法中,首先获取方法签名,然后获取异常信息,并将其记录到日志中。这里使用了LoggerFactory获取一个Logger对象,用于记录日志。logger.error方法用于记录错误日志,它接收三个参数:日志前缀、日志信息和异常对象。

    注意,切面类需要使用@Aspect@Component注解进行标注,以便Spring容器能够扫描到并将其注册为Bean。

2、SpringMVC拦截器,实现登录控制页面访问权限 。

(1)spring-mvc.xml添加 springmvc拦截器配置:

<!-- 配置拦截器 -->
<mvc:interceptors>
    <mvc:interceptor>
        <!-- 指定需要拦截的URL路径 -->
        <mvc:mapping path="/role/**"/>
        <mvc:mapping path="/admin/**"/>
        <!-- 定义拦截器的Bean -->
        <bean class="com.ssm.netctoss.interceptor.RoleInterceptor"/>
    </mvc:interceptor>
</mvc:interceptors>

 完整的spring-mvc.xml文件如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
       https://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/mvc
       https://www.springframework.org/schema/mvc/spring-mvc.xsd">

    <!--开启SpringMVC的注解: 简化处理器映射器和处理器适配器的bean注册,以及JSON数据的传递-->
    <mvc:annotation-driven/>

    <!--静态资源的访问通过配置-->
    <mvc:default-servlet-handler/>

    <!--视图解析器的配置-->
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/pages/"/>
        <property name="suffix" value=".jsp" />
    </bean>

    <!-- 配置拦截器 -->
    <mvc:interceptors>
        <mvc:interceptor>
            <!-- 指定需要拦截的URL路径 -->
            <mvc:mapping path="/role/**"/>
            <mvc:mapping path="/admin/**"/>
            <!-- 定义拦截器的Bean -->
            <bean class="com.ssm.netctoss.interceptor.RoleInterceptor"/>
        </mvc:interceptor>
    </mvc:interceptors>

</beans>

(2)前端nav.jsp设置分权限页面:

<div id="navi">
    <ul id="menu">
        <li><a href="${pageContext.request.contextPath}/index" class="index_off"></a></li>

        <!-- 仅管理员可见的导航项 -->
        <c:if test="${sessionScope.loggedInUser.adminCode == 'admin'}">
            <li><a href="${pageContext.request.contextPath}/role/list" class="role_off"></a></li>
            <li><a href="${pageContext.request.contextPath}/admin/list" class="admin_off"></a></li>
        </c:if>

        <!-- 其他所有用户可见的导航项 -->
        <li><a href="${pageContext.request.contextPath}/fee/list" class="fee_off"></a></li>
        <li><a href="${pageContext.request.contextPath}/account/list" class="account_off"></a></li>
        <li><a href="${pageContext.request.contextPath}/service/searchService" class="service_off"></a></li>
        <li><a href="${pageContext.request.contextPath}/bill/list" class="bill_off"></a></li>
        <li><a href="${pageContext.request.contextPath}/report/list" class="report_off"></a></li>

        <!-- 信息和密码修改仅管理员可见 -->
        <c:if test="${sessionScope.loggedInUser.adminCode == 'admin'}">
            <li>
                <a href="${pageContext.request.contextPath}/user/info?adminId=${sessionScope.loggedInUser.adminId}" class="information_off"></a>
            </li>
        </c:if>
        <li><a href="${pageContext.request.contextPath}/user/modipwd" class="password_off"></a></li>
    </ul>
</div>

(3)创建拦截器实现角色权限访问:

建立interceptor包,包下创建RoleInterceptor.java拦截器,实现了根据登录身份进行权限检查从而实现控制访问不同登录角色的界面。仅举例权限验证代码:

package com.ssm.netctoss.interceptor;

import com.ssm.netctoss.pojo.User;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class RoleInterceptor implements HandlerInterceptor {

    /**
     * 在请求处理之前进行调用(Controller 方法调用之前)
     * @return true 表示继续流程(如调用下一个拦截器或处理器)
     *         false 表示中断流程(如重定向)
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 获取请求的URL
        String uri = request.getRequestURI();
        String contextPath = request.getContextPath();

        // 定义需要管理员权限的URL路径
        if (uri.startsWith(contextPath + "/role/") || uri.startsWith(contextPath + "/admin/")) {
            // 获取用户信息
            User user = (User) request.getSession().getAttribute("loggedInUser");
            if (user == null) {
                // 用户未登录,重定向到 nopower.jsp
                response.sendRedirect(contextPath + "/nopower");
                return false;
            }
            if (!"admin".equals(user.getAdminCode())) {
                // 无管理员权限,重定向到 nopower.jsp
                response.sendRedirect(contextPath + "/nopower");
                return false;
            }
        }

        // 其他请求或有权限,继续处理请求
        return true;
    }

    /**
     * 在 Controller 方法调用之后,视图渲染之前调用
     */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
                           ModelAndView modelAndView) throws Exception {
        // 添加日志记录
    }

    /**
     * 在整个请求结束之后调用(即视图已经渲染完成)
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
                                Object handler, Exception ex) throws Exception {
        // 资源清理
    }
}

解释:
  1. preHandle方法是HandlerInterceptor接口中的一个方法,在请求处理之前进行调用(Controller 方法调用之前)。该方法的返回值决定了请求是否继续执行。如果返回true,则表示继续流程(如调用下一个拦截器或处理器);如果返回false,则表示中断流程(如重定向)。

  2. 首先,通过request.getRequestURI()request.getContextPath()获取请求的URL和项目路径。然后,定义需要管理员权限的URL路径,如以/role//admin/开头的路径。

  3. 通过request.getSession().getAttribute("loggedInUser")获取用户信息。如果用户未登录,则重定向到“nopower.jsp”页面。如果用户登录了,但不是管理员,则同样重定向到“nopower.jsp”页面。

  4. 如果用户具有管理员权限,或者请求的URL不需要管理员权限,则继续处理请求。

(4)创建权限访问类:CommonController,确保 nopower.jsp 的映射

package com.ssm.netctoss.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class CommonController {

    @RequestMapping("/nopower")
    public String noPower() {
        return "nopower"; // 确保与 nopower.jsp 的视图名匹配
    }
}

(5) LoginController中,储存登录信息 sesson

// 登录成功,获取用户信息

User user = userService.getUserByAdminCode(adminCode);

// 将用户信息存储在会话中

session.setAttribute("loggedInUser", user);

(6)RoleController.java添加权限判断:

package com.ssm.netctoss.controller;

import com.ssm.netctoss.pojo.User;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpSession;

@Controller
@RequestMapping("/role")
public class RoleController {

    @RequestMapping("/list")
    public ModelAndView listRoles(HttpSession session) {
        User user = (User) session.getAttribute("loggedInUser");
        ModelAndView mav = new ModelAndView();
        
        if (user == null || !"admin".equals(user.getAdminCode())) {
            mav.setViewName("redirect:/nopower");
            return mav;
        }
        
        // 添加获取角色列表的逻辑
        // List<Role> roles = roleService.getAllRoles();
        // mav.addObject("roles", roles);
        mav.setViewName("roleList"); // 确保存在 roleList.jsp
        return mav;
    }
}

(7)AdminController.java添加权限判断:

package com.ssm.netctoss.controller;

import com.ssm.netctoss.pojo.User;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpSession;

@Controller
@RequestMapping("/admin")
public class AdminController {

    @RequestMapping("/list")
    public ModelAndView listAdmins(HttpSession session) {
        User user = (User) session.getAttribute("loggedInUser");
        ModelAndView mav = new ModelAndView();
        
        if (user == null || !"admin".equals(user.getAdminCode())) {
            mav.setViewName("redirect:/nopower");
            return mav;
        }
        
        // 添加获取管理员列表的逻辑
        // List<Admin> admins = adminService.getAllAdmins();
        // mav.addObject("admins", admins);
        mav.setViewName("adminList"); // 确保存在 adminList.jsp
        return mav;
    }
}

3、增加动态验证码(CAPTCHA)并验证功能。

(1)修改 LoginController 控制器添加验证码逻辑:

添加验证码生成方法,并验证。完整LoginController.java代码如下:

package com.ssm.netctoss.controller;

import com.ssm.netctoss.pojo.User;
import com.ssm.netctoss.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;

// 导入必要的类
import javax.imageio.ImageIO;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.OutputStream;
import java.util.Random;

@Controller
public class LoginController {

    @Autowired
    private UserService userService;

    // 映射到/toLogin的GET请求,显示登录页面并生成验证码
    @RequestMapping("/toLogin")
    public String toLogin() {
        return "login"; // 确保返回的视图名与您的视图解析器配置匹配
    }

    // 映射到/captcha的GET请求,生成并返回验证码图片
    @RequestMapping("/captcha")
    public void getCaptcha(HttpSession session, HttpServletResponse response) throws Exception {
        // 创建验证码图片
        int width = 80;
        int height = 30;
        BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        Graphics g = image.getGraphics();
        Random random = new Random();

        // 填充背景色
        g.setColor(new Color(random.nextInt(256), random.nextInt(256), random.nextInt(256)));
        g.fillRect(0, 0, width, height);

        // 生成验证码字符串
        String captcha = getRandomNumber(4);

        // 绘制验证码字符串
        g.setColor(new Color(random.nextInt(256), random.nextInt(256), random.nextInt(256)));
        g.setFont(new Font("Times New Roman", Font.BOLD, 24));
        g.drawString(captcha, 15, 22);

        // 保存验证码到 session 中
        session.setAttribute("captcha", captcha);

        // 生成干扰线
        for (int i = 0; i < 4; i++) {
            g.setColor(new Color(random.nextInt(256), random.nextInt(256), random.nextInt(256)));
            g.drawLine(random.nextInt(width), random.nextInt(height), random.nextInt(width), random.nextInt(height));
        }

        g.dispose();

        // 设置响应的类型为图片
        response.setContentType("image/jpeg");
        OutputStream os = response.getOutputStream();
        ImageIO.write(image, "jpeg", os);
        os.close();
    }

    // 获取随机验证码
    private String getRandomNumber(int num) {
        char[] chars = {
                'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
                'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
                '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'
        };
        StringBuilder sb = new StringBuilder();
        Random random = new Random();
        for (int i = 0; i < num; i++) {
            int index = random.nextInt(chars.length);
            sb.append(chars[index]);
        }
        return sb.toString();
    }

    // 映射到/loginner的POST请求,处理登录表单提交
    @RequestMapping("/loginner")
    public ModelAndView login(
            ModelAndView modelAndView,
            @RequestParam String adminCode,
            @RequestParam String password,
            @RequestParam String captcha, // 获取用户输入的验证码
            HttpSession session) {

        // 从 session 中获取生成的验证码
        String sessionCaptcha = (String) session.getAttribute("captcha");

        // 验证验证码
        if (sessionCaptcha == null || !sessionCaptcha.equalsIgnoreCase(captcha)) {
            // 验证码错误
            modelAndView.addObject("errorLogin", "验证码错误,请重新输入");
            modelAndView.setViewName("login");
            return modelAndView;
        }

        // 验证用户名和密码
        if (userService.validateUser(adminCode, password)) {

            // 登录成功,获取用户信息
            User user = userService.getUserByAdminCode(adminCode);

            // 将用户信息存储在会话中(可选)
            session.setAttribute("loggedInUser", user);

            // 登录成功,重定向到主页面
            modelAndView.setViewName("redirect:index");
        } else {
            // 登录失败,返回登录页面并显示错误信息
            modelAndView.addObject("errorLogin", "用户名或密码错误,请重试");
            modelAndView.setViewName("login");
        }

        return modelAndView;
    }
}

(2)更新 login.jsp 前端页面:

更新 login.jsp 以显示动态生成的验证码图片,并确保用户可以刷新验证码。

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>电信资费管理系统</title>
    <link type="text/css" rel="stylesheet" media="all" href="statics/styles/global.css" />
    <link type="text/css" rel="stylesheet" media="all" href="statics/styles/global_color.css" />
    <style>
        /* 添加验证码图片样式 */
        .captcha-img {
            cursor: pointer;
        }
    </style>
    <script>
        // JavaScript函数,用于点击验证码图片时刷新
        function refreshCaptcha() {
            var captchaImg = document.getElementById("captchaImage");
            // 添加一个随机参数以防止浏览器缓存
            captchaImg.src = "captcha?" + Math.random();
        }
    </script>
</head>
<body class="index">
<div class="login_box">
    <form action="loginner" method="post">
        <table>
            <tr>
                <td class="login_info">账号:</td>
                <td colspan="2"><input name="adminCode" type="text" class="width150" required /></td>
                <td class="login_error_info"><span class="required">30长度的字母、数字和下划线</span></td>
            </tr>
            <tr>
                <td class="login_info">密码:</td>
                <td colspan="2"><input name="password" type="password" class="width150" required /></td>
                <td><span class="required">30长度的字母、数字和下划线</span></td>
            </tr>
            <tr>
                <td class="login_info">验证码:</td>
                <td class="width70"><input name="captcha" type="text" class="width70" required /></td>
                <td>
                    <!-- 显示动态生成的验证码图片,并添加点击事件以刷新 -->
                    <img src="captcha" alt="验证码" id="captchaImage" class="captcha-img" οnclick="refreshCaptcha()" title="点击更换" />
                </td>
            </tr>
            <tr>
                <td></td>
                <td class="login_button" colspan="2">
                    <input type="submit" value="登录" class="btn_save" style="border: 1px solid orange"/>
                </td>
                <td>
                    <!-- 显示登录错误信息 -->
                    <span class="required">
                        <c:if test="${not empty errorLogin}">
                            ${errorLogin}
                        </c:if>
                    </span>
                </td>
            </tr>
        </table>
    </form>
</div>
</body>
</html>

4、增加页面表单验证功能。

(1)资费修改页面举例: fee_modi.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>电信资费管理系统</title>
    <link type="text/css" rel="stylesheet" media="all" href="../statics/styles/global.css" />
    <link type="text/css" rel="stylesheet" media="all" href="../statics/styles/global_color.css" />
    <script language="javascript" type="text/javascript">
        //保存结果的提示
        function showResult() {
            showResultDiv(true);
            window.setTimeout(function() {
                showResultDiv(false);
            }, 3000);
        }

        function showResultDiv(flag) {
            var divResult = document.getElementById("save_result_info");
            if (flag)
                divResult.style.display = "block";
            else
                divResult.style.display = "none";
        }

        //切换资费类型
        function feeTypeChange(type) {
            var inputArray = document.getElementById("main").getElementsByTagName("input");
            if (type == 1) {
                inputArray[4].readOnly = true;
                inputArray[4].value = "";
                inputArray[4].className += " readonly";
                inputArray[5].readOnly = false;
                inputArray[5].className = "width100";
                inputArray[6].readOnly = true;
                inputArray[6].className += " readonly";
                inputArray[6].value = "";
            }
            else if (type == 2) {
                inputArray[4].readOnly = false;
                inputArray[4].className = "width100";
                inputArray[5].readOnly = false;
                inputArray[5].className = "width100";
                inputArray[6].readOnly = false;
                inputArray[6].className = "width100";
            }
            else if (type == 3) {
                inputArray[4].readOnly = true;
                inputArray[4].value = "";
                inputArray[4].className += " readonly";
                inputArray[5].readOnly = true;
                inputArray[5].value = "";
                inputArray[5].className += " readonly";
                inputArray[6].readOnly = false;
                inputArray[6].className = "width100";
            }
        }

        //表单验证
        function validateForm(formId) {
            const form = document.getElementById(formId);
            const name = form.querySelector('input[name="name"]');
            const baseDuration = form.querySelector('input[name="baseDuration"]');
            const baseCost = form.querySelector('input[name="baseCost"]');
            const unitCost = form.querySelector('input[name="unitCost"]');
            const descr = form.querySelector('textarea[name="descr"]');
            const privileges = form.querySelectorAll('input[name="privileges"]:checked');

            let isValid = true;
            let errorMessage = '';

            // 验证资费名称
            if (name && (name.value.length === 0 || name.value.length > 50)) {
                errorMessage += '资费名称不能为空且必须在50字符以内。\n';
                isValid = false;
            }

            // 验证基本时长
            if (baseDuration && (baseDuration.value < 1 || baseDuration.value > 600)) {
                errorMessage += '基本时长必须在1到600之间。\n';
                isValid = false;
            }

            // 验证基本费用和单位费用
            if (baseCost && (baseCost.value < 0 || baseCost.value > 99999.99)) {
                errorMessage += '基本费用必须在0到99999.99之间。\n';
                isValid = false;
            }
            if (unitCost && (unitCost.value < 0 || unitCost.value > 99999.99)) {
                errorMessage += '单位费用必须在0到99999.99之间。\n';
                isValid = false;
            }

            // 验证描述
            if (descr && descr.value.length > 100) {
                errorMessage += '资费说明长度不能超过100个字符。\n';
                isValid = false;
            }

            // 验证权限(仅在角色管理页面需要)
            if (privileges && privileges.length === 0) {
                errorMessage += '请至少选择一个权限。\n';
                isValid = false;
            }

            // 如果有错误,显示错误信息并阻止提交
            if (!isValid) {
                alert(errorMessage);
            }

            return isValid;
        }
    </script>
</head>
<body>
<!--Logo区域开始-->
<div id="header">
    <img src="../statics/images/logo.png" alt="logo" class="left"/>
    <a href="${pageContext.request.contextPath}/toLogin">[退出]</a>
</div>
<!--Logo区域结束-->
<!-- 导航区域开始 -->
<jsp:include page="../nav.jsp" />
<!-- 导航区域结束 -->
<!--主要区域开始-->
<div id="main">

    <form action="saveModify" method="POST" class="main_form" οnsubmit="return validateForm('formId');">
        <input type="hidden" name="costId" value="${cost.costId}" />
        <div class="text_info clearfix"><span>资费名称:</span></div>
        <div class="input_info">
            <input type="text" name="name" class="width300" value="${cost.name}" required />
            <span class="required">*</span>
            <div class="validate_msg_short">50长度的字母、数字、汉字和下划线的组合</div>
        </div>
        <div class="text_info clearfix"><span>资费类型:</span></div>
        <div class="input_info fee_type">
            <input type="radio" name="costType" value="1" id="monthly" οnclick="feeTypeChange(1);" <c:if test="${cost.costType == '1'}">checked</c:if> />
            <label for="monthly">包月</label>
            <input type="radio" name="costType" value="2" id="package" οnclick="feeTypeChange(2);" <c:if test="${cost.costType == '2'}">checked</c:if> />
            <label for="package">套餐</label>
            <input type="radio" name="costType" value="3" id="timeBased" οnclick="feeTypeChange(3);" <c:if test="${cost.costType == '3'}">checked</c:if> />
            <label for="timeBased">计时</label>
        </div>
        <div class="text_info clearfix"><span>基本时长:</span></div>
        <div class="input_info">
            <input type="number" name="baseDuration" min="1" max="600" class="width100" value="${cost.baseDuration}" required />
            <span class="info">小时</span>
            <span class="required">*</span>
            <div class="validate_msg_long">1-600之间的整数</div>
        </div>
        <div class="text_info clearfix"><span>基本费用:</span></div>
        <div class="input_info">
            <input type="number" step="0.01" name="baseCost" class="width100" value="${cost.baseCost}" required />
            <span class="info">元</span>
            <span class="required">*</span>
            <div class="validate_msg_long error_msg">0-99999.99之间的数值</div>
        </div>
        <div class="text_info clearfix"><span>单位费用:</span></div>
        <div class="input_info">
            <input type="number" step="0.01" name="unitCost" class="width100" value="${cost.unitCost}" required />
            <span class="info">元/小时</span>
            <span class="required">*</span>
            <div class="validate_msg_long error_msg">0-99999.99之间的数值</div>
        </div>
        <div class="text_info clearfix"><span>资费说明:</span></div>
        <div class="input_info_high">
            <textarea name="descr" class="width300 height70">${cost.descr}</textarea>
            <div class="validate_msg_short error_msg">100长度的字母、数字、汉字和下划线的组合</div>
        </div>
        <div class="button_info clearfix">
            <input type="submit" value="保存" class="btn_save" οnclick="showResult();" />
            <input type="button" value="取消" class="btn_save" οnclick="window.location.href='list';" />
        </div>
        <!-- 保存结果的提示 -->
        <div id="save_result_info" class="save_fail" style="display:none;">保存成功!</div>
    </form>
</div>
<!--主要区域结束-->
<div id="footer">
    <span>[Copyright © 1996-2024 NET Corporation, All Rights Reserved]</span>
    <br />
    <span>版权所有(C)  Company  NET</span>
</div>
</body>
</html>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/889263.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

ssm基于Javaee的影视创作论坛的设计与实现

系统包含&#xff1a;源码论文 所用技术&#xff1a;SpringBootVueSSMMybatisMysql 免费提供给大家参考或者学习&#xff0c;获取源码请私聊我 需要定制请私聊 目 录 目 录 III 第1章 绪论 1 1.1选题动因 1 1.2目的和意义 1 1.3论文结构安排 2 第2章 开发环境与技术 …

Auto-Animate:是一款零配置、即插即用的动画工具,可以为您的 Web 应用添加流畅的过渡效果

嗨&#xff0c;大家好&#xff0c;我是小华同学&#xff0c;关注我们获得“最新、最全、最优质”开源项目和高效工作学习方法 用户体验成为了检验产品成功与否的关键因素。而动画效果&#xff0c;作为提升用户体验的重要手段&#xff0c;在网页和应用开发中扮演着举足轻重的角色…

机器学习、深度学习评价指标汇总:TP、TN、FP、FN、AP、mAP、IoU、mAP@3、Prec@10、 Acc@10

系列文章目录 文章目录 系列文章目录一、真正例&#xff08;True Positive&#xff09;、假正例&#xff08;False Positive&#xff09;、真负例&#xff08;True Negative&#xff09;和假负例&#xff08;False Negative&#xff09;是评估分类模型性能的重要概念。1. 定义2…

攻防世界(CTF)~Reverse-easyRE1

题目介绍 下载附件后一个32位一个64位 64位的放到ExeinfoPE查看一下有无壳子&#xff08;无壳&#xff09; 放IDA看一下伪代码&#xff0c;习惯性看一下main函数&#xff0c;直接发现了flag flag{db2f62a36a018bce28e46d976e3f9864}

2024互联网下载神器IDM6.42你值得拥有

&#x1f525; 互联网下载神器大揭秘&#xff01;IDM6.42你值得拥有 &#x1f680; Hey&#xff0c;各位小伙伴们&#xff0c;今天我要给你们安利一款我超爱的软件——Internet Download Manager 6.42&#xff08;简称IDM&#xff09;&#xff0c;这款下载器简直就是下载界的“…

MinIO分片上传超大文件(纯服务端)

目录 一、MinIO快速搭建1.1、拉取docker镜像1.2、启动docker容器 二、分片上传大文件到MinIO2.1、添加依赖2.2、实现MinioClient2.3、实现分片上传2.3.0、初始化MinioClient2.3.1、准备分片上传2.3.2、分片并上传2.3.2.1、设置分片大小2.3.2.2、分片 2.3.3、分片合并 三、测试3…

Java基础-泛型机制

文章目录 为什么引入泛型泛型的基本使用泛型类泛型接口泛型方法泛型数组正确的数组声明使用场景如何理解Java中的泛型是伪泛型&#xff1f;泛型中类型擦除 泛型数组&#xff1a;如何正确的初始化泛型数组实例&#xff1f; 为什么引入泛型 引入泛型的意义在于&#xff1a; 适用…

谷歌AI大模型Gemini API快速入门及LangChain调用视频教程

1. 谷歌Gemini API KEY获取及AI Studio使用 要使用谷歌Gemini API&#xff0c;首先需要获取API密钥。以下是获取API密钥的步骤&#xff1a; 访问Google AI Studio&#xff1a; 打开浏览器&#xff0c;访问Google AI Studio。使用Google账号登录&#xff0c;若没有账号&#xf…

SpringBoot中间件Docker

Docker&#xff08;属于C/S架构软件&#xff09; 简介与概述 1.Docker 是一个开源的应用容器引擎&#xff0c;基于 Go 语言 并遵从 Apache2.0 协议开源。 Docker 可以让开发者打包他们的应用以及依赖包到一个轻量级、可移植的容器中&#xff0c;然后发布到任何流行的 Linux …

2款.NET开源且免费的Git可视化管理工具

Git是什么&#xff1f; Git是一种分布式版本控制系统&#xff0c;它可以记录文件的修改历史和版本变化&#xff0c;并可以支持多人协同开发。Git最初是由Linux开发者Linus Torvalds创建的&#xff0c;它具有高效、灵活、稳定等优点&#xff0c;如今已成为软件开发领域中最流行…

python34_可变字符串

可变字符串 说明 在 Python 中&#xff0c;字符串属于不可变对象&#xff0c;不支持原地修改&#xff0c;如果需要修改其中的值&#xff0c;智能创建新的字符串对象。 但是&#xff0c;经常我们确实需要原地修改字符串&#xff0c;可以使用 io.StringIO对象或 array 模块impo…

Redis:string类型

Redis&#xff1a;string类型 string命令设置与读取SETGETMSETMGET 数字操作INCRINCRBYDECRDECRBYINCRBYFLOAT 字符串操作APPENDSTRLENGETRANGESETRANGE 内部编码intembstrraw 在Redis中&#xff0c;字符串string存储的是二进制&#xff0c;以byte为单位&#xff0c;输入的二进…

电影选票选座系统|影院购票|电影院订票选座小程序|基于微信小程序的电影院购票系统设计与实现(源码+数据库+文档)

电影院订票选座小程序 目录 基于微信小程序的电影院购票系统设计与实现 一、前言 二、系统功能设计 三、系统实现 1、用户功能实现 2、管理员功能实现 &#xff08;1&#xff09;影院信息管理 &#xff08;2&#xff09;电影信息管理 &#xff08;3&#xff09;已完成…

C语言 | Leetcode C语言题解之第464题我能赢吗

题目&#xff1a; 题解&#xff1a; typedef struct HashItem {int key;bool val;UT_hash_handle hh; } HashItem;bool dfs(int maxChoosableInteger, int usedNumbers, int desiredTotal, int currentTotal, HashItem **memo) {HashItem *pEntry NULL;HASH_FIND_INT(*memo, …

深度解析:从浏览器输入链接到页面展现的奇幻历程

〇、前言 当我们在浏览器中输入一个网址&#xff0c;例如&#xff1a;example.com&#xff0c;按下回车键后&#xff0c;会发生什么呢&#xff1f; 主要会发生以下这些过程&#xff1a;域名解析、建立HTTP连接、发送HTTP请求、数据传输、渲染网页、断开HTTP连接。 一、域名解…

dfs +剪枝sudoku———poj2676

目录 前言 lowbit函数 数独 suduku 问题描述 输入 输出 问题分析 子网格位置 优化搜索顺序剪枝1 优化搜索顺序剪枝2 可行性剪枝 代码 前言 lowbit函数 这是一个利用二进制位运算取出二进制数最后一位’1‘的函数 数独 数独大家肯定都玩过&#xff0c;…

Vue/组件的生命周期

这篇文章借鉴了coderwhy大佬的Vue生命周期 在Vue实例化或者创建组件的过程中 内部涉及到一系列复杂的阶段 每一个阶段的前后时机都可能对应一个钩子函数 以下是我根据coderwhy大佬文章对于每一个阶段的一些看法 1.过程一 首先实例化Vue或者组件 在实例化之前 会对应一个钩子函…

Internet Download Manager6.42免费版下载神器新体验

&#x1f680; 开篇就燃&#xff01;你的下载速度被“TA”承包了 #### &#x1f31f; 初识IDM 6.42&#xff0c;下载界的“超跑”驾到 各位追求效率的小伙伴们&#xff0c;今天小红要来揭秘一款让我彻底告别“龟速”下载的神器——Internet Download Manager (简称IDM) 6.42版&…

threejs-基础材质设置

一、介绍 主要内容&#xff1a;基础材质(贴图、高光、透明、环境、光照、环境遮蔽贴图) 主要属性&#xff1a; side: three.DoubleSide, //设置双面 color: 0xffffff, //颜色 map: texture, //纹理 transparent: true, // 透明度 aoMap: aoTexture, //ao贴图 aoMapIntensity: 1…

商标恶意维权形式及应对策略

在商业领域&#xff0c;商标恶意维权的现象时有出现&#xff0c;给正常的市场秩序和企业经营带来了不良影响。以下将介绍其常见形式及应对方法。 一、商标恶意维权的形式1、囤积商标后恶意诉讼。一些人或企业大量注册与知名品牌相似或具有一定通用性的商标&#xff0c;并非用于…