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

news2024/10/11 0:34:18

 一、新增的模块:

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

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>

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

代码随想录算法训练营第四十六天 | 647. 回文子串,516.最长回文子序列

四十六天打卡&#xff0c;今天用动态规划解决回文问题&#xff0c;回文问题需要用二维dp解决 647.回文子串 题目链接 解题思路 没做出来&#xff0c;布尔类型的dp[i][j]&#xff1a;表示区间范围[i,j] &#xff08;注意是左闭右闭&#xff09;的子串是否是回文子串&#xff0…

【JavaEE】——回显服务器的实现

阿华代码&#xff0c;不是逆风&#xff0c;就是我疯 你们的点赞收藏是我前进最大的动力&#xff01;&#xff01; 希望本文内容能够帮助到你&#xff01;&#xff01; 目录 一&#xff1a;引入 1&#xff1a;基本概念 二&#xff1a;UDP socket API使用 1&#xff1a;socke…

zookeeper ——watcher

这应该不是目录 1.watcher定义和说明1.1定义1.2特性1.3watcher的通知状态Watcher.Event.KeeperState1.4watcher的事件类型Event.EventType1.5基本流程 1.watcher定义和说明 1.1定义 在ZooKeeper中&#xff0c;Watcher是一种机制&#xff0c;用于在节点&#xff08;znode&#…

2024百度云智大会|百度大模型内容安全合规探索与实践

9月25日&#xff0c;2024百度云智大会在北京举办。会上&#xff0c;百度智能云分别针对算力、模型、AI 应用&#xff0c;全面升级百舸 AI 异构计算平台 4.0、千帆大模型平台 3.0 两大 AI 基础设施&#xff0c;并升级代码助手、智能客服、数字人三大 AI 原生应用产品。 在大模型…

网络编程(17)——asio多线程模型IOThreadPool

十七、day17 之前我们介绍了IOServicePool的方式&#xff0c;一个IOServicePool开启n个线程和n个iocontext&#xff0c;每个线程内独立运行iocontext, 各个iocontext监听各自绑定的socket是否就绪&#xff0c;如果就绪就在各自线程里触发回调函数。为避免线程安全问题&#xf…

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

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

【笔记学习篇】一篇文章搞定Mybatis-快速回顾

概述 5.1.1 Mybatis简介 Mybatis是一款优秀的持久层框架&#xff0c;它以sql为中心&#xff0c;支持定制化sql、存储过程以及高级映射。 使用Mybatis框架&#xff0c;可以无需手动编写基础的JDBC代码、无需手动设置参数和转换结果集到对象。 Mybatis可以使用简单的xml或注解来…

App测试时常用的adb命令

adb 全称为 Android Debug Bridge&#xff08;Android 调试桥&#xff09;&#xff0c;是 Android SDK 中提供的用于管理 Android 模拟器或真机的工具。 adb 是一种功能强大的命令行工具&#xff0c;可让 PC 端与 Android 设备进行通信。adb 命令可执行各种设备操作&#xff0…

银发产业资讯丨蚂蚁集团、金城药业、百联集团、京东健康布局业务

银发经济『新趋势大数据』 AgeNews 每日银发产业大事件速览 2024-10-8 星期二 AgeClub整理 金融监管总局&#xff1a;鼓励险企提供更多养老保障服务 蚂蚁集团等签署合作协议&#xff0c;聚焦智慧医疗领域 金城医药等合作聚焦年长女性健康科技领域 京东健康助力四川发放…

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

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

日语学习零基础生活日语口语柯桥外语学校|股票用日语怎么说?

在日语中&#xff0c;“股票”可以说&#xff1a; • 株&#xff08;かぶ&#xff09; 这是最常用的表达方式&#xff0c;直接表示“股票”。 例如&#xff1a; 株を買う - 买股票 株を売る - 卖股票 • 株式&#xff08;かぶしき&#xff09; 这个词也是“股票”的意…

学习文档(二)

异常 这是Java 异常类层次结构图概览&#xff1a; Exception 和 Error 有什么区别? 一、概念与本质 Exception&#xff08;异常&#xff09;&#xff1a;异常是在程序运行过程中出现的可预料的、可恢复的不正常情况。例如&#xff0c;试图打开一个不存在的文件时&#xff0…

【数据结构-栈】【位运算优化】力扣3170. 删除星号以后字典序最小的字符串

给你一个字符串 s 。它可能包含任意数量的 ‘’ 字符。你的任务是删除所有的 ’ 字符。 当字符串还存在至少一个 ‘*’ 字符时&#xff0c;你可以执行以下操作&#xff1a; 删除最左边的 ‘’ 字符&#xff0c;同时删除该星号字符左边一个字典序 最小 的字符。如果有多个字典…

【C++篇】虚境探微:多态的流动诗篇,解锁动态的艺术密码

文章目录 C 多态详解&#xff08;进阶篇&#xff09;前言第一章&#xff1a;多态的原理1.1 虚函数表的概念1.1.1 虚函数表的生成过程 1.2 虚表的存储位置 第二章&#xff1a;动态绑定与静态绑定2.1 静态绑定2.1.1 静态绑定的实现机制&#xff1a;2.1.2 示例代码&#xff1a; 2.…

从0到1:小区业主决策投票小程序开发笔记

可研 小区业主决策投票小程序&#xff1a; 便于业主参与社区事务的决策&#xff0c;通过网络投票的形式&#xff0c;大大节省了业委会和业主时间&#xff0c;也提高了投票率。其主要功能&#xff1a;通过身份证、业主证或其他方式确认用户身份&#xff1b;小区管理人员或业委会…

YOLO5的修改

在传统的yolov5网络中并不存在注意力机制&#xff0c;但是源代码中存在相关简略的代码&#xff1a; def __init__(self, c, num_heads):"""Initializes a transformer layer, sans LayerNorm for performance, with multihead attention and linear layers.See …

prometheus client_java实现进程的CPU、内存、IO、流量的可观测

文章目录 1、获取进程信息的方法1.1、通过读取/proc目录获取进程相关信息1.2、通过Linux命令获取进程信息1.2.1、top&#xff08;CPU/内存&#xff09;命令1.2.2、iotop&#xff08;磁盘IO&#xff09;命令1.2.3、nethogs&#xff08;流量&#xff09;命令 2、使用prometheus c…

tableau除了图表好看,在业务中真有用吗?

tableau之前的市值接近150亿美金&#xff0c;被saleforce以157亿美金收购&#xff0c;这个市值和现在的蔚来汽车差不多。 如果tableau仅仅是个show的可视化工具&#xff0c;必然不会有这么高的市值&#xff0c;资本市场的眼睛是雪亮的。 很多人觉得tableau做图表好看&#xff…

分布式常见面试题总结

文章目录 1 什么是 UUID 算法&#xff1f;2 什么是雪花算法&#xff1f;&#x1f525;3 说说什么是幂等性&#xff1f;&#x1f525;4 怎么保证接口幂等性&#xff1f;&#x1f525;5 paxos算法6 Raft 算法7 CAP理论和 BASE 理论7.1 CAP 理论&#x1f525;7.2 为什么无法同时保…

Echarts合集更更更之树图

实现效果 写在最后&#x1f352; 源码&#xff0c;关注&#x1f365;苏苏的bug&#xff0c;&#x1f361;苏苏的github&#xff0c;&#x1f36a;苏苏的码云