国税

一:纳税服务系统实战day01

1.1:SSH整合

1.1.1:引入jar包,并创建配置文件

  • 引入三个框架的jar包,然后针对每个框架创建对应的配置文件

注:除了hibernate不需要指定配置文件,因为它已经把数据源,sessionFactory等交给spring容器来管理了,所 以没必要在创建一个配置文件

1.1.1.1:针对struts创建配置文件

  • 在web.xml中配置过滤器
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://xmlns.jcp.org/xml/ns/javaee" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" id="WebApp_ID" version="3.1">
  <filter>
  	<filter-name>struts2</filter-name>
  	<filter-class>org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter</filter-class>
  </filter>
  <filter-mapping>
  	<filter-name>struts2</filter-name>
  	<!--只拦截.action -->
  	<url-pattern>*.action</url-pattern>
  </filter-mapping>
</web-app>
  • 在src创建一个名为struts.xml的配置文件
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE struts PUBLIC
	"-//Apache Software Foundation//DTD Struts Configuration 2.3//EN"
	"http://struts.apache.org/dtds/struts-2.3.dtd">
	
<struts>
	
</struts>

1.1.1.2:针对spring创建配置文件

  • 在web.xml中配置监听器(注意放在过滤器的前面)

注:监听应用服务器的启动,启动后要去实例化IOC容器!

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns="http://xmlns.jcp.org/xml/ns/javaee"
	xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
	id="WebApp_ID" version="3.1">

	<!--监听器先放在过滤器的前面 -->
	<listener>
		<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
	</listener>
	<context-param>
		<param-name>contextConfigLocation</param-name>
		<param-value>classpath:applicationContext.xml</param-value>
	</context-param>

	<filter>
		<filter-name>struts2</filter-name>
		<filter-class>org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter</filter-class>
	</filter>
	<filter-mapping>
		<filter-name>struts2</filter-name>
		<!--只拦截.action -->
		<url-pattern>*.action</url-pattern>
	</filter-mapping>
	
</web-app>
  • 配置applicationContext.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:p="http://www.springframework.org/schema/p"
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
	xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd 
	http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd 
	http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
	http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd">
    
</beans>

1.1.2:SSH整合测试

  • 大概思路

先看spring有没有问题,没有问题就先spring和struts整合一下,还是没有问题就spring和hibernate整合一下,然后三者一起结合跑一下看有没有问题,没有问题说明SSH整合成功

1.1.2.1:spring的自我测试

在applicationContext.xml注册一个bean,然后加载配置文件获取看有没有问题?
注:把bean注册到spring容器中要么xml,要么注解@Compoent等,然后使用扫描

@Service
public class TestUserService {
	
}
<!--在spring核心配置中进行扫描  -->
<context:component-scan base-package="cn.itcast.test"></context:component-scan>
@Test
	public void testSpring() {
		ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
		TestUserService bean = (TestUserService)context.getBean("testUserService");
		System.out.println(bean);
	}

1.1.2.2:spring和struts的测试

页面访问一个Action,在Action中调用spring中注册的bean输出,然后正确的响应了一个页面给浏览器,
则说明这两个结合使用没有问题!

  • 配置struts的里面的Action
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE struts PUBLIC
	"-//Apache Software Foundation//DTD Struts Configuration 2.3//EN"
	"http://struts.apache.org/dtds/struts-2.3.dtd">
	
<struts>
	<package name="testPackage" extends="struts-default" namespace="/">
		<action name="test_*" class="cn.itcast.test.TestUserAction" method="{1}">
			<result name="success">/WEB-INF/index.jsp</result>
		</action>
	</package>
</struts>
public class TestUserAction extends ActionSupport{
	
    // 注:这个Action类没有使用@component,也就是没有被容器管理,居然可以使用@Resource来注入
    //    原因是:struts2-spring-plugin-2.3.20.jar 帮忙做的处理!!!
	@Resource //根据属性名去spring容器中找
	TestUserService testUserService;
	
	public String hello()throws Exception {
		System.out.println(testUserService);
		return Action.SUCCESS;
	}
}
  • 然后在浏览器地址栏上输入:
http://localhost:8080/项目名/test_hello.action
  • 执行过程:
struts中配置的过滤器会拦截 test_hello.action, 然后去struts.xml中去匹配action标签的name属性为test_hello的,这里使用的是通配符的方式,匹配上了,然后去TestUserAction类中,执行hello()方法,返回success,然后转发到WEB-INF下面index.jsp
  • 遇到的异常:在浏览器上用的是https, 而不是http所导致的!!!
java.lang.IllegalArgumentException: Invalid character found in method name. HTTP method names must be tokens

1.1.2.3:spring和hibernate的测试

  • 由于hibernate中的连接,事务都交给spring来进行管理,所以在spring配置中需要配置连接数据库必须有的四大参数,频繁的打开关闭连接也非常耗费系统资源,所以还需要配置连接池(c3p0), 还有hibernate操作数据库是用sessionFactory,所以需要在spring中注册一个sessionFactory的bean
  • 在applicationContext.xml配置数据库连接池&sessionFactory&数据库其他配置
<?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:p="http://www.springframework.org/schema/p"
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:aop="http://www.springframework.org/schema/aop"
	xmlns:tx="http://www.springframework.org/schema/tx"
	xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd 
	http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd 
	http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
	http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd">

	<import resource="classpath:cn/itcast/test/conf/*-spring.xml" />

	<!-- 导入外部的properties配置文件 -->
	<context:property-placeholder
		location="classpath:db.properties" />

	<!--之前在hibernate配置的数据源信息,由spring来管理 -->
	<!-- 配置c3p0数据源 -->
	<bean id="dataSource"
		class="com.mchange.v2.c3p0.ComboPooledDataSource"
		destroy-method="close">
		<property name="jdbcUrl" value="${jdbcUrl}"></property>
		<property name="driverClass" value="${driverClass}"></property>
		<property name="user" value="${user}"></property>
		<property name="password" value="${password}"></property>
		<!--初始化时获取三个连接,取值应在minPoolSize与maxPoolSize之间。Default: 3 -->
		<property name="initialPoolSize" value="${initialPoolSize}"></property>
		<!--连接池中保留的最小连接数。Default: 3 -->
		<property name="minPoolSize" value="3"></property>
		<!--连接池中保留的最大连接数。Default: 15 -->
		<property name="maxPoolSize" value="${maxPoolSize}"></property>
		<!--当连接池中的连接耗尽的时候c3p0一次同时获取的连接数。Default: 3 -->
		<property name="acquireIncrement" value="3"></property>
		<!--最大空闲时间,1800秒内未使用则连接被丢弃,若为0则永不丢弃。Default: 0 -->
	</bean>

	<!--3.配置sessionFactory  -->
	<bean id="sessionFactory" class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
		<property name="dataSource" ref="dataSource"></property>
		<property name="hibernateProperties">
			<props>
				<!--配置数据库方言  -->
				<prop key="hibernate.dialect">org.hibernate.dialect.MySQL5Dialect</prop>
				<prop key="hibernate.show_sql">true</prop>
				<prop key="hibernate.hbm2ddl.auto">update</prop>
				<prop key="javax.persistence.validation.mode">none</prop>
			</props>
		</property>
		<!--hibernate实体类的映射文件  -->
		<property name="mappingLocations">
			<list>
				<value>classpath:cn/itcast/entity/Person.hbm.xml</value>
			</list>
		</property>
	</bean>
</beans>
  • 在src下创建一个db.properties
jdbcUrl=jdbc:mysql://127.0.0.1:3306/itcastTax?serverTimezone=UTC&useUnicode=true&charaterEncoding=utf-8&useSSL=false
driverClass=com.mysql.jdbc.Driver
user=root
password=123456
initialPoolSize=10
maxPoolSize=30
  • 注意到了里面的映射文件吗?,所以我们需要在cn.itcast.entity包下创建一个名为Person.hbm.xml映射文件
public class Person {

	//使用uuid,怕有时候两个系统,然后主键都是自增长!
	private String id; 
	private String name;
    
	//省了getter/setter
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-mapping PUBLIC 
    "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
    "http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
	<class name="cn.itcast.entity.Person" table="person">
		<id name="id" type="java.lang.String">
			<column name="id" length="32"></column>
			<generator class="uuid.hex"></generator>
		</id>
		
		<property name="name" type="java.lang.String">
			<!--指定表中列的映射  -->
			<column name="name" length="20" not-null="true"></column>
		</property>
	</class>
</hibernate-mapping>
  • 测试代码
@Test
public void testHibernate() {
    //2.加载spring容器
    ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");

    //2.从spring容器中获取SessionFactory
    SessionFactory sessionFactory = (SessionFactory) context.getBean("sessionFactory");

    //3.打开session
    Session session = sessionFactory.openSession();

    //4.开启事务
    Transaction tx = session.beginTransaction();

    //创建一个Person瞬时态的对象
    Person person = new Person("wzj");
    session.save(person);

    tx.commit();

    session.close();
    sessionFactory.close();
}
1.1.2.3.1:测试Service层和Dao层
  • 创建Dao层和Service层的接口和实现类

(1):Dao层的接口和实现类

//接口不需要注册到容器中
public interface TestDao {
    
	void save(Person person);
	
	//为了通用性,使用Serializable
	Person findById(Serializable id);
}
//注:TestDaoImpl不需要使用@Repository来登记到容器中,它使用了xml的方式
public class TestDaoImpl extends HibernateDaoSupport implements TestDao{

	@Override
	public void save(Person person) {
		getHibernateTemplate().save(person);
	}

	@Override
	public Person findById(Serializable id) {
		return getHibernateTemplate().get(Person.class, id);
	}

}

注:这里需要思考,为什么继承HibernateDaoSupport就可以对数据库增删改查了?
它都没有要数据库四大参数,它怎么知道我数据库在哪?所以需要在配置文件中配置SessionFactory,然后让HibernateDaoSupport的子类引用,这样HibernateDaoSupport就知道了!!
为了applicationContext.xml 中配置文件结构清晰和日后好维护,这里采用引入的方式

  • test-spring.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:p="http://www.springframework.org/schema/p"
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
	xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd 
	http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd 
	http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
	http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd">
    
    <bean id="testDao" class="cn.itcast.dao.impl.TestDaoImpl">
    	<property name="sessionFactory" ref="sessionFactory"></property>
    </bean>
</beans>
  • 引入到applicationContext.xml中
<!--导入外部的配置文件 -->
<import resource="classpath:cn/itcast/conf/*-spring.xml" />

(2):Service层的接口和实现类

public interface TestService {

	void save(Person person);
	
	Person findById(Serializable id);
}
@Service
public class TestServiceImpl implements TestService {
	
	@Resource
	TestDao testdao;

	@Override
	public void save(Person person) {
		testdao.save(person);
	}

	@Override
	public Person findById(Serializable id) {
		return testdao.findById(id);
	}
}
  • 测试代码
	@Test
	public void testServiceAndDao() {
		ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
		TestService testService = (TestService)context.getBean("testServiceImpl");
//		testService.save(new Person("sbt"));
		System.out.println(testService.findById("402881e77195bb82017195bb82f40000").getName());
	}
1.1.2.3.2:配置并测试spring中的事务
  • 在appliactionContext.xml中加上事务的配置,事务分为编程式事务(注解)和声明式事务(xml)
<!--事务管理  -->
	<bean id="txManager" class=" org.springframework.orm.hibernate3.HibernateTransactionManager">
		<property name="sessionFactory" ref="sessionFactory"></property>
	</bean>
	
	<!--事务通知  -->
	<tx:advice id="txAdvice" transaction-manager="txManager">
		<tx:attributes>
			<!--只读事务,我这里配置就是以find,get,list开头的方法,里面的操作是只读,不能对其修改!  -->
			<tx:method name="find*" read-only="true"/>
			<tx:method name="get*" read-only="true"/>
			<tx:method name="list*" read-only="true"/>
			<!--回滚事务:除了上面配置的几个以find,get,list开头的方法,其他方法一旦内部发生Throwable的异常,就会回滚!  -->
			<tx:method name="*" rollback-for="Throwable"/>
		</tx:attributes>
	</tx:advice>
	<!--aop配置:被事务控制的类  -->
	<aop:config>
        <!-- <aop:pointcut id="serviceOperation" expression="bean(*ServiceImpl)"/> --> <!--扫描以ServiceImpl结尾的bean -->
		<aop:pointcut id="serviceOperation" expression="execution(* cn.itcast..service.impl.*.*(..))"/>
		<aop:advisor advice-ref="txAdvice" pointcut-ref="serviceOperation"/>
	</aop:config>

(1):测试只读事务

你上面不是配置在execution(* cn.itcast…service.impl..(…)) 下中的方法,如果是以find开头的方法,里面对数据库的操作就只能是只读操作(查询)吗?,所以我试试在findById()中加上一段插入的方法会怎么样!!

@Override
public Person findById(Serializable id) {
    testdao.save(new Person("zhangsan")); //为了测试只读事务是否生效
    return testdao.findById(id);
}
@Test
	public void testTransactionReadOnly() {
		ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
		TestService testService = (TestService)context.getBean("testServiceImpl");
		System.out.println(testService.findById("402881e77195bb82017195bb82f40000").getName());;
	}
  • 异常
org.springframework.dao.InvalidDataAccessApiUsageException: Write operations are not allowed in read-only mode (FlushMode.MANUAL): Turn your Session into FlushMode.COMMIT/AUTO or remove 'readOnly' marker from transaction definition.

注:执行测试方法,发现出现了异常,说明我们配置的只读事务生效了

(2):测试回滚事务

@Override
public void save(Person person) {
    testdao.save(person);
    int i = 10/0; //故意搞一个异常,测试一下回滚事务
}
@Test
public void testTransactionRollback() {
    ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
    TestService testService = (TestService)context.getBean("testServiceImpl");
    testService.save(new Person("ls"));
}
  • 异常
java.lang.ArithmeticException: / by zero

注:查看了一下数据库,数据也并没有插入成功,说明我们配置的回滚事务生效了!

只读事务:如果在只读事务中出现更新操作则回滚
回滚事务:如果操作中出现有任何异常就回滚

1.2:资源文件分类&导入log4j&BaseDao的抽取

1.2.1:资源文件分类

1.2.2:导入log4j

  • 为了不让那些红色警告出来,我们需要导入log4j和slf4j-log4j12-1.6.1.jar,
    然后在src下放一个log4j.properties的配置文件
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{ABSOLUTE} %5p [%t] %c{1}:%L - %m%n

## 默认是警告级别,stdou:就是会在后台输出警告及其以上的级别,R就是输出在指定的文件中
log4j.rootLogger=warn, stdout, R
## 在cn.itcast包下是输出debug及其以上级别
log4j.logger.cn.itcast=debug 

## 这个是每天会新创建一个文件来记录日志,并且文件名中有就是当天的日期
log4j.appender.R=org.apache.log4j.DailyRollingFileAppender
## 日志会输出到这个文件中
log4j.appender.R.File=D:/itcast/itcast.log 
log4j.appender.R.layout=org.apache.log4j.PatternLayout
log4j.appender.R.layout.ConversionPattern=%d [%t] %5p  %c - %m%n
@Test
public void testLog4j() {
    Log log = LogFactory.getLog(getClass());
    //		log.debug("debug级别");
    //		log.info("info级别");
    //		log.warn("warn级别");
    //		log.error("error 级别");
    //		log.fatal("fatal 级别");
    try {
        int i = 10/0;
    } catch (Exception e) {
        log.error(e.getMessage());
    }
}

1.2.3:BaseDao的抽取

  • 作为一个系统肯定有很多子模块,这些子模块基本上都会涉及到基本的增删改查,所以为了减少代码量
    我们应该抽取一个公共的BaseDao接口,里面涵盖基本的crud,让其他Dao接口继承

注:现在我怎么感觉有共有的属性,可以抽象成一个类,有共同的方法可以抽象成一个接口!!

public interface BaseDao<T> {

	//增
	void save(T entity);
	
	//删:根据id删
	void delete(Serializable id);
	
	//更改
	void update(T entity);
	
	//查询单个
	T findObjectById(Serializable id);
	
	//查询所有
	List<T> findObjects();
}
public class BaseDaoImpl<T> extends HibernateDaoSupport implements BaseDao<T> {

	Class<T> clazz;
	
	public BaseDaoImpl() {
		//根据反射获取子类的泛型类型
		clazz = (Class)((ParameterizedType)this.getClass().getGenericSuperclass()).getActualTypeArguments()[0];
	}

	@Override
	public void save(T entity) {
		getHibernateTemplate().save(entity);
	}

	@Override
	public void delete(Serializable id) {
		getHibernateTemplate().delete(findObjectById(id));
	}

	@Override
	public void update(T entity) {
		getHibernateTemplate().update(entity);
	}

	@Override
	public T findObjectById(Serializable id) {
		return getHibernateTemplate().get(clazz, id);
	}

	@Override
	public List<T> findObjects() {
		//使用HQL语句来查询所有
		Query query = getSession().createQuery("FROM "+clazz.getSimpleName());
		return query.list();
	}

}
  • 一些注意事项
1.简单的增删改查的需求可以从后往前开发:entity-->Dao-->Service-->Action
  注:条件比较复杂点的,可以从前往后开发

2.Action
列表页面
跳转到新增页面
新增
跳转到编辑页面
更新
删除
批量删除

3.写一个专门注入SessionFactory的bean让其他需要sessionFactory的bean来引用
4.修改了配置文件需要重启!
5.struts2项目访问页面404,我基本上都检查过了,按道理是没有问题,但结果总是404因为不能在struts.xml中   开启<constant name="struts.devMode" value="true" />,把它注释掉就没了

1.4:用户模块

1.4.1:简单增删改查

  • 开发流程:实体(映射文件)–>Dao–>Service–>Action

注:其实一般是先使用PowerDesigner数据库建模:从概念模型–>物理模型–>生成表—>生成实体类和映射文件

  • 实体:根据添加页面的原型图,我们可以知道实体类中有哪些的属性
public class User implements Serializable{

	private String id;
	private String dept;
	private String account;
	private String name;
	private String password;
	
	private String headImg;
	private boolean gender;
	private String state;
	private String mobile;
	private String email;
	private Date birthday;
	private String memo;
	//用户状态
	public static String USER_STATE_VALID = "1";//有效
	public static String USER_STATE_INVALID = "0";//无效
    
    //省了getter和setter方法
}
  • UserDao
public interface UserDao<User> extends BaseDao<User>{

}
// 注: 这样UserDao就继承了简单的Crud的抽象方法
  • UserDaoImpl
public class UserDaoImpl extends BaseDaoImpl<User> implements UserDao<User>{
    
}
// 注:这样UserDaoImpl继承了BaseDaoImpl了方法,就相当于重写了UserDao里面的抽象方法
  • UserService
public interface UserServie {
	//增
	void save(T entity);
	
	//删:根据id删
	void delete(Serializable id);
	
	//更改
	void update(T entity);
	
	//查询单个
	T findObjectById(Serializable id);
	
	//查询所有
	List<T> findObjects();
}
// 注:暂时还没抽取成BaseService
  • UserServiceImpl
@Service("userService")
public class UserServiceImpl implements UserServie {
	
	//Service层依赖dao层
	@Resource
	UserDao<User> userDao;

	@Override
	public void save(User user) {
		userDao.save(user);
	}

	@Override
	public void delete(Serializable id) {
		userDao.delete(id);
		//删除用户对应的所有权限
		userDao.deleteUserRoleByUserId(id);
	}

	@Override
	public void update(User user) {
		userDao.update(user);
	}

	@Override
	public User findObjectById(Serializable id) {
		return userDao.findObjectById(id);
	}

	@Override
	public List<User> findObjects() {
		return userDao.findObjects();
	}
}

  • UserAction
用户模块页面分析:
访问首页后,页面上有一个添加按钮和编辑按钮还有删除按钮和批量删除(一般就是勾选多选框然后删除)
1.访问首页:是不是要从数据库中查询所有,然后显示在页面上,所以要发送一个请求: 所以ListUI()出现了
2.添加按钮:你一点击是不是要跳到添加用户的页面,我这里不是直接超链接链过去的,我还是发送一个请求,然后让
          Action给我调度,返回一个添加页面,所以UserAction中出现了addUI(),在添加页面你填完用户信           息之后,要保存了,所以一点击发送了一个请求,所以UserAction中出现了add()
3.编辑按钮:与添加按钮的情况类似,点击一下到了编辑页面,所以UserAction中出现了editUI(),然后在编辑页           面编辑好之后要保存了,然后发送了一个请求,所以UserAction中出现了edit()
4.删除/批量删除:都不需要额外的页面,所以直接发送一个请求给后台处理就行,所以UserAction中出现了delete、deleteBySelected

注:最终保存成功和修改成功后,都需要回到列表页面!!
public class UserAction extends BaseAction{
	
	//表现层依赖Service层
	@Resource
	private UserServie userService;
	private User user;
	
    // 列表页面
	public String listUI() {
		return "listUI";
	}
	
   // 跳转到新增页面
	public String addUI() {
		return "addUI";
	}
	
    // 新增
	public String add() {
		return "list";
	}
	
    // 跳转到编辑页面
	public String editUI() {

		return "editUI";
	}
	
   // 编辑
	public String edit() {
		
		return "list";
	}
	
    // 删除
	public String delete() {
		return "list";
	}
	
    // 批量删除
	public String deleteBySelected() {
		return "list";
	}
    
	//省了getter/setter
}

1.4.2:头像上传&js日期组件

1.4.2.1:struts2上传头像

(1):页面中

  • 页面的表单属性需要有enctype=“multipart/form-data”,还得写个输入框来上传文件
<input type="file" name="headImg"/>

(2):Action中

  • struts2需要定义三个属性来接收
private File headImg; //头像,是一个图片,所以也就是一个文件
private String headImgFileName;//上传头像的文件名
private String headImgContentType; //文件类型
//生成对应的getter/setter
  • Action类中处理上传头像
if(headImg != null) {//1.获取用户头像
    //2.保存头像文件
    String filePath = ServletActionContext.getServletContext().getRealPath("upload/user");
    //为了防止文件名重复,使用uuid
    String fileName = UUID.randomUUID().toString().replace("-", "")+headImgFileName.substring(headImgFileName.lastIndexOf("."));
    FileUtils.copyFile(headImg, new File(filePath,fileName));//保存到指定的目录下了
    //3.设置用户头像的路径
    user.setHeadImg("user/"+fileName);
}
userService.save(user);

注:不会将图片本身保存到数据库中,只是保存一个图片路径就行,图片文件本身会存放到项目下,到时候页面要显示图片的话,在页面中拼接保存在数据库的图片路径来显示图片就行了

扩展:后期自己可以做一个在线图片预览的功能

注意事项:

上面我们虽然用代码将它保存在/test2/upload/user/6800773c5af74aa193e34ffd3c53f7c1.png"下,但是
我发现当我去upload/user/下看的时候,发现却没有图片,但是页面上头像却显示出来了,是不是感觉很奇怪,其实图片文件存到了:D:\tomcat\apache-tomcat-8.5.39\wtpwebapps\test2\upload\user下
但是一旦重新启动(其实是发布:publish)tomcat服务器,那些图片就会丢失,导致一重启然后访问页面,头像就无法显示了

1.4.3:使用poi

  • 先导入jar包
poi-3.11-20141221.jar,poi-ooxml-3.11-20141221.jar,poi-ooxml-schemas-3.11-20141221.jar,xmlbeans-2.6.0.jar
  • 概念
工作簿: workbook
工作表: sheet
行: row
单元格: cell
注:工作薄包含工作表,工作表包含行,行包含单元格

步骤:
1.操作(创建,读取)工作簿
2.操作工作表
3.操作行
4.操作单元格
注:使用poi操作excel,无论有多复杂,都是基于这几步扩展出来的!
  • 2003:.xls 和 2007:.xlsx的读取和写入
public class TestPOI {

	@Test
	public void wirte03Excel() throws Exception {
		//需求:在第三行,第三列,写一个hello
		//1.创建工作簿
		HSSFWorkbook hssfWorkbook = new HSSFWorkbook();
		//2.创建工作表
		HSSFSheet sheet = hssfWorkbook.createSheet("sheet1");
		//3.创建行,第三行,行列都是从索引为0开始的
		HSSFRow row = sheet.createRow(2);
		//4.创建单元格,第三列
		HSSFCell cell = row.createCell(2);
		cell.setCellValue("hello");
		
		FileOutputStream fileOutputStream = new FileOutputStream(new File("D:\\test.xls"));
		hssfWorkbook.write(fileOutputStream);
		
		//先关excel的流,再关字节流的资源
		hssfWorkbook.close();
		fileOutputStream.close();
	}
	
	
	@Test
	public void read03Excel() throws Exception {
		//需求:读取在第三行,第三列的内容
		//1.读取工作簿
		FileInputStream fileInputStream = new FileInputStream(new File("D:\\test.xls"));
		HSSFWorkbook hssfWorkbook = new HSSFWorkbook(fileInputStream);
		//2.读取工作表, 根据索引读取第一个sheet
		HSSFSheet sheet = hssfWorkbook.getSheetAt(0);
		//3.读取行,第三行,行列都是从索引为0开始的
		HSSFRow row = sheet.getRow(2);
		//4.读取单元格,第三列
		HSSFCell cell = row.getCell(2);
		String content = cell.getStringCellValue();
		System.out.println(content);
		
		//先关excel的流,再关字节流的资源
		hssfWorkbook.close();
		fileInputStream.close();
	}
	
	@Test
	public void wirte07Excel() throws Exception {
		//需求:在第三行,第三列,写一个hello
		//1.创建工作簿
		XSSFWorkbook XSSFWorkbook = new XSSFWorkbook();
		//2.创建工作表
		XSSFSheet sheet = XSSFWorkbook.createSheet("sheet1");
		//3.创建行,第三行,行列都是从索引为0开始的
		XSSFRow row = sheet.createRow(2);
		//4.创建单元格,第三列
		XSSFCell cell = row.createCell(2);
		cell.setCellValue("hello");
		
		FileOutputStream fileOutputStream = new FileOutputStream(new File("D:\\test.xlsx"));
		XSSFWorkbook.write(fileOutputStream);
		
		//先关excel的流,再关字节流的资源
		XSSFWorkbook.close();
		fileOutputStream.close();
	}
	
	
	@Test
	public void read07Excel() throws Exception {
		//需求:读取在第三行,第三列的内容
		//1.读取工作簿
		FileInputStream fileInputStream = new FileInputStream(new File("D:\\test.xlsx"));
		XSSFWorkbook XSSFWorkbook = new XSSFWorkbook(fileInputStream);
		//2.读取工作表, 根据索引读取第一个sheet
		XSSFSheet sheet = XSSFWorkbook.getSheetAt(0);
		//3.读取行,第三行,行列都是从索引为0开始的
		XSSFRow row = sheet.getRow(2);
		//4.读取单元格,第三列
		XSSFCell cell = row.getCell(2);
		String content = cell.getStringCellValue();
		System.out.println(content);
		
		//先关excel的流,再关字节流的资源
		XSSFWorkbook.close();
		fileInputStream.close();
	}
	
	// 03是.xls结尾, 07是.xlsx, 03是HSSF开头, 07是XSSF
}

注:小技巧,可以选中的代码块中的代码,然后ctrl+f将HSSF全部替换成XSSF, 它只会替换你选中的区域,很实用!

  • Excel中的样式
合并居中和样式,都在导航栏所以你可以认为他是属于工作簿的,然后运用就是使用的地方在哪!!
1.合并居中它是属于工作簿(特殊:但是它是独立创建的),运用在工作表,不能运用单元格,(特殊)一个单元格你怎么合并?
2.样式也是属于工作簿,运用在单元格
3.字体也是属于工作簿,加载在样式中,通过样式运用在单元格,
(因为它相当于一个属性<div font-size="16px"></div>//不能这样写,要放在样式中
 <div style="font-size:16px"></div>
如果运用在sheet,就说明sheet中的字体都是一样,但是可以不一样,所以是在单元格中!
  • 代码演示

需求:将第三行,第三列写入内容,然后合并单元格,并且水平垂直居中,设置背景颜色,字体大小,字体加粗

//HSSF 是Horrible SpreadSheet Format的缩写,也即“讨厌的电子表格格式”
	@Test
	public void write03Style() throws Exception {

		//1.创建工作簿
		HSSFWorkbook hssfWorkbook = new HSSFWorkbook();
		//1.1 创建合并单元格对象, 合并3行3列,和3行5列, 比较特殊虽然是属于工作簿,但是是由自己创建
		CellRangeAddress cellRangeAddress = new CellRangeAddress(2, 2, 2, 4);
		//1.2 创建单元格样式
		HSSFCellStyle style = hssfWorkbook.createCellStyle();
		style.setAlignment(HSSFCellStyle.ALIGN_CENTER); //水平对齐方式是居中
		style.setVerticalAlignment(HSSFCellStyle.VERTICAL_CENTER);//垂直对齐方式是居中
		//1.3 创建字体
		HSSFFont font = hssfWorkbook.createFont();
		font.setBoldweight(HSSFFont.BOLDWEIGHT_BOLD);//设置加粗
		font.setFontHeightInPoints((short)16);//设置字体大小
		//加载字体
		style.setFont(font);
		
		//单元格背景
		//设置背景填充模式
		style.setFillPattern(HSSFCellStyle.SOLID_FOREGROUND);
		//设置填充背景色
		style.setFillBackgroundColor(HSSFColor.YELLOW.index);
		//设置填充前景色
		style.setFillForegroundColor(HSSFColor.RED.index);
		
		//2.创建工作表
		HSSFSheet sheet = hssfWorkbook.createSheet("sheet01");
		//2.1:加载合并单元格
		sheet.addMergedRegion(cellRangeAddress);
		//3.创建行,第三行
		HSSFRow row = sheet.createRow(2);
		//4.创建单元格, 第三列
		HSSFCell cell = row.createCell(2);
		//加载样式
		cell.setCellStyle(style);
		cell.setCellValue("hello");
		
		FileOutputStream fileOutputStream = new FileOutputStream(new File("D://hello.xls"));
		hssfWorkbook.write(fileOutputStream);
		fileOutputStream.close();
		hssfWorkbook.close();
	}
	//注:import org.apache.poi.ss.util.CellRangeAddress;

1.4.4:导出用户列表

流程:导出就是将从数据库中查询的数据,存放到一个excel文件中,供用户下载(数据库–>excel–>用户)

(1):页面点击导出按钮
(2):Action中的处理

//导出excel这是业务,所以操作excel的时候得放在service层, controller只是起调度的作用!
	public String exportExcel() {
		try {
			//查找用户列表
			userList = userService.findObjects();
			//获取输出流
			HttpServletResponse response = ServletActionContext.getResponse();
			response.setContentType("application/x-excel");
			response.setHeader("Content-Disposition", "attachment;filename="+new String("用户列表.xls".getBytes(), "ISO-8859-1"));
			ServletOutputStream outputStream = response.getOutputStream();
			userService.exportExcel(userList,outputStream);
			if(outputStream != null) {
				outputStream.close();
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
		return Action.NONE;
	}

(3):Service中的处理

@Override
public void exportExcel(List<User> userList, ServletOutputStream outputStream) {
    try {
        //因为代码太多,所以抽取成了一个方法
        ExcelUtils.exportExcel(userList, outputStream);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

(4):ExcelUtils

public static void exportExcel(List<User> userList, ServletOutputStream outputStream) {
    try {
        //1.创建工作簿
        HSSFWorkbook workbook = new HSSFWorkbook();
        //1.1合并单元格对象
        CellRangeAddress cellRangeAddress = new CellRangeAddress(0, 0, 0, 4); //起始行号,结束行号,起始列号,结束列号
        //1.2、头标题样式
        HSSFCellStyle style1 = createCellStyle(workbook, (short)16);
        //1.3、列标题样式
        HSSFCellStyle style2 = createCellStyle(workbook, (short)13);

        //2.创建工作表
        HSSFSheet sheet = workbook.createSheet("用户列表");
        //2.1、加载合并单元格对象
        sheet.addMergedRegion(cellRangeAddress);
        //设置默认列宽
        sheet.setDefaultColumnWidth(25);

        //3.创建行, 第一行
        //3.1、创建头标题行;并且设置头标题
        HSSFRow row1 = sheet.createRow(0);
        HSSFCell cell1 = row1.createCell(0);
        //加载单元格样式
        cell1.setCellStyle(style1);
        cell1.setCellValue("用户列表");

        //3.2创建列标题行,并设置列标题
        HSSFRow row2 = sheet.createRow(1);
        String[] titles = {"用户名","账号", "所属部门", "性别", "电子邮箱"};
        for(int i=0; i<titles.length; i++) {
            HSSFCell cell2 = row2.createCell(i);
            //加载单元格样式
            cell2.setCellStyle(style2);
            cell2.setCellValue(titles[i]);
        }

        if(userList != null) {
            for(int x=0; x<userList.size(); x++) {
                HSSFRow row = sheet.createRow(x+2);//创建第三行,不能覆盖之前的行
                HSSFCell cell11 = row.createCell(0);
                cell11.setCellValue(userList.get(x).getName());
                HSSFCell cell12 = row.createCell(1);
                cell12.setCellValue(userList.get(x).getAccount());
                HSSFCell cell13 = row.createCell(2);
                cell13.setCellValue(userList.get(x).getDept());
                HSSFCell cell14 = row.createCell(3);
                cell14.setCellValue(userList.get(x).isGender()?"男":"女");
                HSSFCell cell15 = row.createCell(4);
                cell15.setCellValue(userList.get(x).getEmail());
            }
        }

        //5.输出
        workbook.write(outputStream);
        workbook.close();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

1.4.5:导入用户列表

流程:导入就是读取用户上传的excel 数据,存入到数据库(用户–>excel–>数据库)
(1):页面点击导出按钮

(2):Action中的处理

//导入Excel(就相当于导入到数据库,这里返回list,就是导入到数据库,再从数据库全部查出来,重定向显示到页面上!)
public String importExcel() {
    //获取Excel文件
    if(userExcel != null) {
        //是否是excel文件,^$:以什么开头,结尾,\\. 是为.转义, (?i):对大小写不敏感
        if(userExcelFileName.matches("^.+\\.(?i)((xls)|(xlsx))$")) {
            userService.importExcel(userExcel);
        }
    }
    return "list";
}

(3):Service中的处理

public void importExcel(File userExcel) {
    try {
        FileInputStream fileInputStream = new FileInputStream(userExcel);
        //1.读取工作簿
        //			HSSFWorkbook hssfWorkbook = new HSSFWorkbook(fileInputStream);
        Workbook workbook = WorkbookFactory.create(userExcel);//Workbook可以接受HSSFWorkbook(03)和XSSFWorkbook(07)

        //2.读取工作表
        Sheet sheet = workbook.getSheetAt(0);

        //3.读取行,数据大于2行才去读取,前两行不需要的数据
        if(sheet.getPhysicalNumberOfRows() > 2) {
            //4.读取单元格
            User user = null;
            Row row = null;
            for(int i=2; i<sheet.getPhysicalNumberOfRows(); i++) {
                user = new User();
                row = sheet.getRow(i);
                String name = row.getCell(0).getStringCellValue();
                user.setName(name);
                String account = row.getCell(1).getStringCellValue();
                user.setAccount(account);
                String dept = row.getCell(2).getStringCellValue();
                user.setDept(dept);
                String gender = row.getCell(3).getStringCellValue();
                user.setGender("男".equals(gender));

                String mobile;
                try {
                    mobile = row.getCell(4).getStringCellValue(); //手机需要特殊处理
                } catch (Exception e) {
                    //科学计数方式的数值
                    double dMobile = row.getCell(4).getNumericCellValue();
                    //BigDecimal将科学计数方式的值,转为一个正常的数值并转为字符串
                    mobile = BigDecimal.valueOf(dMobile).toString();
                }

                user.setMobile(mobile);
                String email = row.getCell(5).getStringCellValue();
                user.setEmail(email);
                Date birthday = row.getCell(6).getDateCellValue();//日期类型的单元格值
                user.setBirthday(birthday);
                //设置默认状态为有效
                user.setState(User.USER_STATE_VALID);
                //设置默认密码为123456
                user.setPassword("123456");
                //保存到数据库
                save(user);
            }
        }

        workbook.close();
        fileInputStream.close();

    } catch (Exception e) {
        e.printStackTrace();
    }
}

1.4.6:账号唯一性校验

分析:账号肯定不能存在唯一的啊,你见过你的QQ号和别人一样的吗?
所以这里在进行保存操作的时候需要对账号进行一个校验判断!
如果数据库中已经存在,就弹出一个提示框说此账号已经存在!否则不做处理

但问题来了? 你没有想过假如用户输入了一个数据库中已经存在的账号,那你提示它的时机应该是在哪呢?
①是在点击sumbit提交按钮时才提示,
②还是当鼠标在填写账号的输入框中失去焦点(focus)的时候提示呢?,答案都不是

第一种情况:提示的太晚了,我都填了一大堆资料准备提交然后你给我来个账号已存在,早干嘛去了。
​ 会导致用户体验感贼差

第二种情况:看似挺不错,实则也是存在弊端的,假如我填了一个数据库中已经存在的账号,这时你对这个账号的
​ 输入框失焦,然后它提示你**“你的账号已经存在”**,然后我不小心要把光标放到了账号的输入框,然 后没做改变的离开了(失焦),你要提示我“你的账号已经存在”,用户就感觉很烦,我之前都知道这个账号已经存在了, 我要你提示二遍?,而且每次提示(都相当于去拿到账号去数据库查询,看没有没有对应的账号!)

最后:经过上面的分析,最终使用change事件来实现!就不会出现onblur失焦事件的那种情况,只有当值改变了然后失焦才会触发change事件

实现效果的思路:

1.为账号输入框注册一个change事件
2.用jq获取账号,使用ajax将请求发送给后台
3.根据(账号)参数,查询数据库,如果有对应的记录,使用字节输出流,返回ajax一个标识
4.ajax中根据标识,来判断该账号是否已经存在!

但如上操作又不能防止我提交,只是提示了我一下,我该保存我照样保存啊
所以我们在表单提交之前应该先做判断,如果符合,就让用户提交。

问题来了,那我们怎么在表单提交之前做判断呢?我判断的代码写在哪里呢??

<input type="submit" value="保存"> 

将上面代码,改成使用按钮提交的方式,在处理点击事件的函数中做处理!!

 <input type="button" value="保存" onclick="doSubmit()"/>
  • 上面分析的是在添加用户上面的逻辑,那适用于编辑页面吗?,我们知道编辑 = 修改+保存,那既然保存也要对账号进行唯一性检验!初次一看感觉跟保存操作是一模一样的不需要在处理什么**,但是我们忽略了一点,就是我编辑一个用户,这个用户本来就是从数据库中取出来的**,那你现在保存的时候,要拿这个账号去数据库中去查,那肯定是存在的啊,然后你跟我说用户账号已重复这不是有毛病吗!

    那应该怎么办?我数据库还是根据用户的账号去查,但是多了一个条件id不能是当前用户的id,所以你得在编辑页面把账号和id传给后台

    select * from xxx where account=? and id!=?;
    

    总结:其实添加和编辑页面对账号唯一性的处理差不多,如果是编辑页面就多传一个id,用来否定当前用户的账号,所以后台用一个方法来处理这两种情况!

  • 将思路转为代码演示

(1)注册change事件

<s:textfield id="account" name="user.account" onchange="doVerify()"/>

(2)将账号传递给后台

var vResult = false;
    	function doVerify(){
    		var $account = $("#account");
    		if($account.val() != ''){
    			/*在js中可以使用el表达式,但是不能使用标签  */
            	$.ajax({
            		url:'${basePath}nsfw/user_verifyAccount.action',
            		type:'GET',
            		async: false,//false是非异步
            		data:{'user.account':$account.val()},
            		success:function(msg){
            			if(msg == 'true'){
            				//已经存在
            				alert("账号已存在");
            				$account.focus();//定焦
            				vResult = false;
            				return false;
            			}else{
            				vResult = true;
            				return true;
            			}
            		}
            	});
    		}
    	}
    	
 //注:我知道它上面为什么不直接返回true/flase,还定义一个vResult来接收一个标识,因为在ajax中如果直接返回true或者fasle,被其他方法调用的时候返回的是一个undefined

(3)Dao层的处理很灵活,
如果只有account传过来了,说明是添加页面,我就只select * from user where account = ?;
如果有account 和 id传过来了,说明是编辑页面,我就select * from user where account = ? and id !=?;

@Override
	public List<User> findUserByAccountAndId(String account, String id) {
		//使用HQL语言
		String sql = "FROM User WHERE account = ?"; //如果只有account说明是添加页面过来的
		if(StringUtils.isNotBlank(id)) {
			sql += "AND id != ?"; // 如果带了id说明是编辑页面
		}
		Query query = getSession().createQuery(sql);
		query.setParameter(0, account);
		if(StringUtils.isNotBlank(id)) {
			query.setParameter(1, id);
		}
		return query.list(); //因为我这里不能确定是增加页面,还是编辑页面(要带id),反正只有带id的才能确定唯一性
	}

(4)用按钮的方式提交

<!-- <input type="text" value=""保存> -->
<input type="button" class="btnB2" value="保存" onclick="doSubmit()"/>
//将提交type改为button来提交然后事件改为onclick,这样就可以在提交之前做判断
    	function doSubmit() {
			//用户名,密码不能为空
			var $nameVal = $("#name");
			if($nameVal.val() == ""){
				alert("名字不能为空!");
				$nameVal.focus(); //定焦,就是光标定位到用户名这个框!!
				return false; //不让代码向下执行了,也相当于退出doSubmit();
			}
			
			var $pwdVal = $("#pwd");
			if($pwdVal.val() == ""){
				alert("密码不能为空!");
				//$pwdVal.focus();
				return false; 
			}
			//账号检查 Ajax
			doVerify();
			if(vResult){
				//手动提交
				document.forms[0].submit();
			}
		}

1.4.7:抽取baseAction

这里对Action进行一个抽取,就像抽取BaseDao一样,一个系统肯定有很多的模块,如用户模块,角色模块,
而这些模块,都有基本的增删改查的方法,所以就把它横向抽取出来,让其他UserDao,RoleDao来继承就行了
BaseAction思想也是如此,因为大部分的Action应该都有批量删除的方法,并且逻辑是一模一样的,所以将公共的属性部分抽取出来放到BaseAction中,让Action继承BaseAction, 让BaseAction来继承ActionSupport类就行了

注:这里主要掌握这种思想就行了

二:纳税服务系统实战day02

2.1:系统异常处理

2.2:角色和权限

角色和权限之间的关系是多对多的关系: 一个角色拥有多个权限,一个权限属于多个角色
按照上面的分析我们应该建立角色类和权限类,然后在映射文件中两边都使用many-to-many标签,到时候由框架自动生成中间表,但梦想很丰满,现实很骨感,因为这里的权限被我们粗粒度的划分为了5个权限,不太需要特意建一个表来维护,只需要定义5个常量由集合进行装载就行了。这时就不会有权限表,所以上面many-to-many生成中间表的梦想已经破灭了,虽然不能使用权限表了,但是毕竟角色和权限还是一个多对多的关系,所以还是需要一个中间表来维护它们之间的关系,既然不用自动生成中间表了,那么我们自己手动创建一个类似中间表的类!
那该如何做呢?请看以下步骤!

角色和权限的E-R图:
下面的代码,其实就是参考该图来完成的,把多对多的关系,拆分成双向一对多来完成
(1)在角色的映射文件中,体现了1对多
(2)在角色_权限的映射文件中,在联合主键中的标签体现了多对1


①创建角色实体类及映射文件

public class Role implements Serializable {

    private String roleId;
    private String name;
    private String state;
    private Set<RolePrivilege> rolePrivileges;

    //角色状态
    public static String ROLE_STATE_VALID = "1";//有效
    public static String ROLE_STATE_INVALID = "0";//无效
    //省了getter/setter方法
}
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">

<hibernate-mapping>
	<class name="cn.itcast.nsfw.role.entity.Role" table="role">
		<id name="roleId" type="java.lang.String">
			<column name="role_id" length="32" />
			<generator class="uuid.hex" />
		</id>
		<property name="name" type="java.lang.String">
			<column name="name" length="20" not-null="true" />
		</property>
		<property name="state" type="java.lang.String">
			<column name="state" length="1" />
		</property>
		<set name="rolePrivileges" inverse="true" lazy="false" cascade="save-update,delete">
			<key>
				<column name="role_id"></column>
			</key>
			<one-to-many class="cn.itcast.nsfw.role.entity.RolePrivilege"/>
		</set>
	</class>
</hibernate-mapping>

②创建角色_权限实体类及映射文件(相当于第三张表) 由其他两张表的主键构成一个联合主键,所以这个联合主键
不可能是一个基本类型,因为它要存储两个属性:角色类中主键,和权限类中的主键
③联合主键必须要实现Seriabliable接口,和重写hashCode() 和 equals()

public class RolePrivilege implements Serializable {
	private RolePrivilegeId id; //使用RolePrivilegeId作为联合主键
    //省了getter/setter
}
public class RolePrivilegeId implements Serializable {

	private Role role;
	private String code;
    //省了getter/setter, 和重写hashcode(),equals()
}
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">

<hibernate-mapping>
	<class name="cn.itcast.nsfw.role.entity.RolePrivilege" table="role_privilege">
		<composite-id name="id" class="cn.itcast.nsfw.role.entity.RolePrivilegeId">
			<!--
				属性名为role的,类型是Role的类型
				属性名为role_id, 类型是String类型
				name="role",class="cn.itcast.nsfw.role.entity.Role"
				name="code",type="java.lang.String"
				这里为什么是many-to-one标签,等下看看E-R图就清楚了,
				它是将多对多,拆分成了两个一对多
			  -->
			<key-many-to-one name="role" lazy="false" class="cn.itcast.nsfw.role.entity.Role">
				<column name="role_id" ></column>
			</key-many-to-one>
			<key-property name="code" type="java.lang.String">
				<column name="code" length="20"></column>
			</key-property>
		</composite-id>
	</class>
</hibernate-mapping>

疑问:为什么RolePrivilegeId中的role不是用private String id来作为主键!
**回答:**我查看角色拥有所有权限的时候,想要得到角色的名称。这里仅仅查出来的是角色id,还要通过角色id得到角色的名称,这样就有点麻烦了。于是我们写成Role对象。到时候就能直接获取了。

2.3:角色的CRUD

  • 与用户的crud几乎差不多

①:编写实体类、映射文件:Role、Role.hbm.xml
②:编写Dao接口和实现类:RoleDao、RoleDaoImpl
③:编写Service接口和实现类:RoleService、RoleServiceImpl
④:编写Action、和对应的Action、spring配置文件:RoleAction、role-struts.xml、role-spring.xml
⑤:将role-struts.xml、role-spring.xml分别引入总配置文件中,struts.xml 和 applicationContext.xml中
⑥:引入对应的jsp页面

2.4:改造用户模块

2.4.1:不包含角色的用户表,单表增删改查

先看用户的增删改查: 不包含角色(单表)
增加: insert into user(x,x..) values(x,x..);
删除: delete from user where id = ?;   前台传id
批量删除: 调用多个 delete from user where id = ?  
修改: update user set name=? and pwd = ? where id = ?  前台传id,先把用户查询出来,然后修改后,再传id进行修改
查询: select * from user;

总结:其实追根溯源就是对数据库进行操作,只不过参数是动态的由前台传递进来!!然后数据库更新数据后,把 数据要展示给前台页面显示,就是这种来回的交互

2.4.2:包含角色的用户表,多表增删改查

1.增加: 这时不单单只保存到用户表了,还得保存在中间表(你要传递用户id,和角色: 也只需传递id)想一想中间表的外键!!!
2.修改:这时回显的时候,多选框没有自动勾选: 所以我得根据用户id去中间表中把角色id给查询出来,然后设置在        一个类型为字符串数组的属性里,返回给前台!这样就可以实现用户的角色多选框回显的问题了!

3.编辑角色的时候,为了不保留用户之前的角色
3.1:先把用户对应的角色在用户角色表中给删除了
3.2:然后更新用户
3.3:然后在把角色id,保存到用户角色表中去!!

4.删除:现在我删除用户,按道理也应该将用户对应的角色id,在用户角色表中删除,但是由于user映射文件的原因
       不能进行级联删除,所以我应该手动的先删除用户,再去用户角色表中删除该用户对应的角色id

三:纳税服务系统实战day03

3.1:引入系统&子系统首页

  • 引入系统&子系统首页就是配置一下Action,然后转发到对应的jsp页面

3.1.1:访问项目就直接进入首页

http://localhost:8080/项目名/sys/home.action
http://localhost:8080/项目名/  只想访问这段url就直接进入系统首页
  • 先在web.xml配置一个默认的访问页面
<welcome-file-list>
		<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
  • 然后重定向到首页

注意:不能直接在页面重定向到home.jsp,因为它存放在WEB-INF下你也访问不到,
​ 所以你得间接的访问,就是去请求一个action,让它帮你去转发到home.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%
	String path = request.getContextPath();
	System.out.print(path);
	response.sendRedirect(path+"/sys/home.action");
%>

3.2:系统登录&注销

场景:没有登录就直接访问系统首页,我不能让你想进来首页就进首页啊,更别说直接进入子系统的首页了,
你必须登录了,才能访问我的系统首页,必须有访问纳税服务子系统的权限,才能访问我的纳税服务子系统的首页

//登录
	public String login() {
		//这种思想:就是把一些影响流程的问题先排除掉
		if(user == null) {
			loginResult = "请输入帐号和密码!";
			//打回登录页面
			return toLoginUI();
		}
		
		String account = user.getAccount();
		String password = user.getPassword();
		if(StringUtils.isBlank(account) && StringUtils.isBlank(password)) {
			loginResult = "用户名或者密码不能为空";
			//打回登录页面
			return toLoginUI();
		}
		
		//1.根据账号,密码查询数据库
		List<User> userList = userServie.findUserByAccountAndPassword(account,password);
		if(userList != null && userList.size()>0) {
			//2. 说明登录成功
			user = userList.get(0);
			//根据用户id,将用户所对应的用户角色设置到用户中
			user.setUserRoles(userServie.getUserRolesByUserId(user.getId()));
			//3. 将用户信息存到session域
			ServletActionContext.getRequest().getSession().setAttribute(Constant.USER, user);
			//4. 记录日志
			Log log = LogFactory.getLog(getClass());
			log.info("用户"+user.getName()+"登录了系统!");
			//5 重定向到系统首页
			return "home";
		}else {
			loginResult = "用户名或者密码错误";
			//打回登录页面
			return toLoginUI();
		}
	}
//注销/退出
	public String logout() {
		ServletActionContext.getRequest().getSession().removeAttribute(Constant.USER);
		return toLoginUI();
	}

3.3:登录过滤器&权限鉴定

实现3.2中的登录和注销还不行,这时用户还是想直接访问首页就直接访问首页没有限制,所以必须配合过滤器来使用

  • 在web.xml中配置一个登录过滤器

注:需要放在监听器的后面

<filter>
    <filter-name>loginFilter</filter-name>
    <filter-class>cn.itcast.core.filter.LoginFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>loginFilter</filter-name>
    <url-pattern>*.action</url-pattern>
</filter-mapping>
  • LoginFilter中的doFilter方法
@Override
	public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain)
			throws IOException, ServletException {
		//强转成子接口,因为子接口方法更多,更强大
		HttpServletRequest req = (HttpServletRequest)servletRequest;
		HttpServletResponse resp = (HttpServletResponse)servletResponse;
		
		//获取路径: urI是短的, url是长的, 包含http://localhost:8080
		String uri = req.getRequestURI();
		if(uri.contains("sys/login")) {
			//说明是登录请求
			chain.doFilter(req, resp); //登录请求直接放行
		}else {
			//不是登录请求,直接从地址栏上想访问系统首页的
			User user = (User) req.getSession().getAttribute(Constant.USER);
			if(user != null) {//说明已经登录过了
				if(uri.contains("nsfw")) {
					//webapplicationContextUtils获取的是随着应用服务器实例化的ioc容器
					WebApplicationContext context = WebApplicationContextUtils.getWebApplicationContext(req.getSession().getServletContext());
					PermissionCheck pc = (PermissionCheck) context.getBean("permissionCheckImpl"); 
					if(pc.isAccessible(user, "nsfw")) {
						//说明该用户的角色有访问nsfw的权限,放行
						chain.doFilter(req, resp);
					}else {
						//没有权限,就跳转到没有权限提示页面
						resp.sendRedirect(req.getContextPath()+"/sys/login_toNoPermissionUI.action");
					}
				}else {
					//非纳税服务这里就直接放行
					chain.doFilter(req, resp);//放行
				}
			}else {
				//回到登录页面
				resp.sendRedirect(req.getContextPath()+"/sys/login_toLoginUI.action");
			}
		}
		
	}
  • 权限鉴定
public class PermissionCheckImpl implements PermissionCheck{
	
	/**
	 * 你这里凭什么直接@resouece注入啊,
	 * 一般注入可以两种情况
	 * 1是在这里类被@Component注解修饰,说明该类被容器管理,所以里面的属性可以用@Resource注入
	 * 2是有一种像Action类虽然没用用类似@Component注解修饰,但它的属性还是可以用@Resource注入,是因为有一个spring-struts插件的jar
	 * 
	 * 而PermissionCheckImpl,两种情况都不符合,所以要手动配置在容器中
	 */
	@Resource
	private UserServie userServie;

	        
	@Override
	public boolean isAccessible(User user, String code) {
		//查询这个用户有没有这个权限code,如果有就返回true,否则就返回false
		//用户和权限之间,没有直接的关系,所以先根据用户id去用户角色表,查询对应的角色id,
		/**
		 * UserRole表
		 * userId  	roleId
		 * 1		2
		 * 1		3
		  *  一个用户可以对应多个角色,所以用 select * from userRole where userId = 1;
		  *  查询出来就是两行记录啊,一行记录就代表一个UserRole实体类对象,
		  *  有多行就是多个UserRole实体类对象,多个就用一个集合装起来
		 */
		//这段操作导致查询数据库太频繁了,因为是过滤器中的内容,所以每个请求都会拦截去数据库查询,导致性能下降
		//因为为每一个用户只查询一次,
		List<UserRole> userRoles = user.getUserRoles();
		
		if(userRoles == null) {
			userRoles = userServie.getUserRolesByUserId(user.getId());
		}
		
		if(userRoles != null && userRoles.size() > 0) {
			Role role = null;
			for (UserRole userRole : userRoles) {
				//1.获取用户角色表中的角色
				role = userRole.getId().getRole();
				Set<RolePrivilege> rolePrivileges = role.getRolePrivileges();
				if(rolePrivileges != null && rolePrivileges.size() > 0) {
					for(RolePrivilege rolePrivilege : rolePrivileges) {
						if(rolePrivilege.getId().getCode().equals(code)) {
							return true;
						}
					}
				}else {
					return false;
				}
			}
		}
		return false;
		
	}
}

3.4:富文本编辑器(ueditor)

富文本编辑器(百度):复制ueditor文件夹到放到js下,其中jsp/lib/下的jar包,引入到我们WEB-INF/lib的文件夹下

<!--1.在页面上引入js  -->
<script type="text/javascript" charset="utf-8" src="ueditor.config.js"></script>
<script type="text/javascript" charset="utf-8" src="ueditor.all.min.js"> </script>
<script type="text/javascript" charset="utf-8" src="lang/zh-cn/zh-cn.js"></script>
//1. 建议指明ueditor文件夹的相对路径
 window.UEDITOR_HOME_URL = "${basePath }js/ueditor/";
//实例化编辑器
 var ue = UE.getEditor('editor'); //为你这里的textarea组件上加一个id为editor,因为你是在上面应用富文本编辑器

如果你不需要用到工具栏上的所以功能,你可以很轻松的去掉
在ueditor文件夹下ueditor.config.js配置即可,在toolbars里面不需要哪个功能删除对应的英文单词就ok,确实很强大,还有不需要显示元素路径也是在这里面配置,搜索“元素路径”,你就会看到这句代码

//是否启用元素路径,默认是显示
//,elementPathEnabled : true  //只需将注释打开然后值为false就配置好了

在工具栏上有一个上传图片的功能,但是上传的不是我们想要的位置,所以我们要指定一下上传的文件放在哪里
在ueditor/jsp/config.json

 "imageUrlPrefix": "http://localhost:8080/itcastTax", /* 图片访问路径前缀 */
 "imagePathFormat": "/upload/ueditor/image/{yyyy}{mm}{dd}/{time}{rand:6}", /* 上传保存路径,可以自定义保存路径和文件名格式 */

上传后的图片在项目的部署路径(Deploy path)下才能看到,我的是在

F:\xxx\apache-tomcat-8.5.39\wtpwebapps

3.5:抽取BaseService

抽取baseService:因为每个XXXService接口都有基础的Crud方法,所以把它抽取出来,一抽取完Service和ServiceImpl感觉代码都少了很多确实很nice

①:先抽取一个BaseService接口,让UserService,RoleService等接口继承它
②:再抽取一个BaseServiceImpl实现BaseService接口中的所有方法,然后让UserServiceImpl,RoleServiceImpl等继承,这样的话UserServiceImpl即已经实现了UserService接口中的抽象方法,又让类
中看起来很整洁!

//我把断点打到set方法这里,容器一启动就会跑这里来
//奇怪的是我并没有往容器中注入InfoDao接口的实现类,这里却为InfoDao赋值了,好像是jdk动态代理
private InfoDao infoDao;
@Resource
public void setInfoDao(InfoDao infoDao) {
    super.setBaseDao(infoDao);
    this.infoDao = infoDao;
}

3.6:封装QueryHelper对象

public QueryHelper(String entity, String alias){}
//刚开始想如上做法,让调用者传一个类名,Info 到时候我就可以from Info
//但是因为是字符串,假如调用者传的是info就错了,为了将错误扼杀在源头,可以设计成Class类型的
public QueryHelper(Class entity, String alias){} 这样你如果传错了类的类型就会报错

分析他为什么写了一个QueryHelper,要学会他这种分析能力

public class QueryHelper {
	
	private String fromClause = ""; // from子句
	private String whereClause = ""; // where子句
	private String orderByClause = ""; // order by子句
	private List<Object> parameters; // 装载查询条件值,因为是有顺序的,到时候也很好对应?来填充值!
	
	public static final String ORDER_BY_DESC = "desc";
	public static final String ORDER_BY_ASC = "asc";
	/**
	 *    构造from子句
	 *    因为from子句子句是必须的,所以放在构造函数中赋值!
	 * @param clazz 实体类
	 * @param alias 实体类对应的别名
	 */
	public QueryHelper(Class clazz, String alias){
		fromClause = "FROM "+clazz.getSimpleName()+" "+alias;
	}
	
	/**
	 *   构造where子句
	 * @param condition 查询条件语句: 例如 i.title like ?
	 * @param params    查询条件语句中?对应的值,例如 %标题%
	 */
	public void addCondition(String condition, Object...params) {
		if(whereClause.length() > 0) {
			//非第一个查询条件
			whereClause += " AND " + condition;
		}else {
			// 第一个查询条件
			whereClause = " WHERE " + condition;
		}
		
		//设置查询条件到查询条件集合中
		if(parameters == null) {
			parameters = new ArrayList<Object>();
		}
		
		if(params != null) {
			for (Object param : params) {
				parameters.add(param);
			}
		}
	}
	
	/**
	 *    构造orderBy子句
	 * @param property 排序属性:如: i.createTime
	 * @param order    排序顺序:如:DESC 或者 ASC
	 */
	public void addOrderByProperty(String property, String order) {
		if(orderByClause.length() > 0) {
			//非第一个排序属性
			orderByClause += ", " + property + " "+order;
		}else {
			// 第一个排序属性
			orderByClause = " ORDER BY " + property + " "+order;
		}
	}
	
	
	// hql查询语句
	public String getQueryListHql() {
		return fromClause + whereClause + orderByClause;
	}
	
	// hql查询语句中?对应的查询条件值集合
	public List<Object> getParameters(){
		return parameters;
	}
}

因为查询所有,和按条件查询都是查询,条件查询无非后面拼接了条件吗,所以放在一个方法中完成,没有必要分成两个方法
问题一:

当我点击删除链接的时候,之前输入框中输入的条件没了!!

原因是因为我点击删除链接是会发送一个delete.action的请求,然后controller做完处理后,会重定向到ListUI.jsp页面,相当于刷新了一次,导致输入框的条件没了

想法1:用转发啊,但是我们之前就是不想用转发才用的重定向,现在遇到问题了又想用回转发,这不是拆东墙补西墙吗
想法2: 可以重定向,那你重定向的时候,必须给我带个info.title的参数,这样的话到时候我才能回显到页面上

在struts.xml中配置重定向带参数

//...info_listUI.action?info.title = ${info.title}中的值
<result name="list" type="redirectAction">
    <param name="actionName">info_listUI</param>
    <param name="info.title">${info.title}</param>
</result>

问题二
如果用中文作为查询条件的话,到时候回显的时候,浏览器带的参数就会乱码
注:我这里却没有乱码,但是url中显示中文不太好,我们还是应该编码一下传输比较好

所以应该在删除后重定向的时候将参数先编码!,再转发到listUI.jsp后再进行解码!!
<param name="encode">true</param>
URLDecoder.decode(info.getTitle(), "utf-8")

问题三
我现在在编辑页面编辑完信息标题后,返回到列表页面,居然发现连搜索框的信息标题也变成了我编辑后的信息标题

这是因为从数据库查询出来的info,把之前的info给覆盖了,所以在回显的时候显示的是从数据库查出来的info.title

注意:要记住解题的思路(套路),代码其实不要怎么记住,因为这个框架是这个写法,到时候新出一个框架就是另一种写法了,不经常用很快就会忘记的!!

根据页面的分页属性,抽取一个分页的类

js居然加上

由于每个页面都需要分页,所以我们可以抽取出来,用一个<jsp:include page="/common/pageNavigator.jsp"/>标签将,前端分页的那块代码包含起来像一段代码一样,假如很多地方都需要使用,我就可以抽取一个方法,到时候用的时候直接调用方法就行

分页:select * from user limit ?,?
这两个?,?我们分析一下怎么得到,其中第一个?代表页码需要前台用户输入,第二个?代表每页记录数,
注:其实每页记录数,不需要由前台来传,是由甲方指定的

当信息发布模块中的分页查询做好之后,我们就可以去轻而易举的将用户模块和角色模块中的
分页和查询功能也完善一下,并抽取成分页的公共部分,比如前台我们已经抽取了一个pageNavigator.jsp
同意的Action也有相同的分页属性,我们可以抽取到BaseAction中

struts中做的最好的就是这个自动回显的功能,是很赞

看完需求之后,应该画一个流程图(有时间这个要学习一下),然后设计概念模型(就是设计出实体),然后根据需求分析实体和实体之间的关系(是一对一,一对多,多对一),根据原型图设计出实体的字段,字段和表,确认没有问题再进行代码开发,数据库层方面尽量少写点约束,可以让前台,后台来判断

<s:iterator>会把遍历的集合放到栈顶,在取出的时候就不用加#什么的 <s:property value= “xxx”>,#是取域对象中的

由于set是无序的,要想有序在中加上order-by属性= “xxx asc/desc”

不用框架完成二级联动:
被投诉人部门列表:
被投诉人列表:
需求是:在投诉部门中选择对应部门,那么投诉人列表,就得显示该部门中的人员!,这就是所谓的二级联动
其实很好实现,就是一个数据库查询的操作,就是根据部门名称,去用户表中查出对应员工的意思,
select
	u.name
from 
	user u
where
	u.dept = ?
既然需要条件,那么这里应该是从前台传给我们的,

前台:
1.获取?对应的值,也就是部门名称
2.发送ajax给后台处理
4.得到数据解析后,获取到用户的名称,添加到被投诉人的位置!!
后台:
3.根据部门名称去用户表中查询出对应的员工转成json格式返回给ajax
用struts返回json,完成二级联动,跟springmvc相比其实麻烦太多了,真的是越高级的框架,屏蔽的东西越多,那么我们懂的原理就越少!
1.先加入:struts2-json-plugin-2.3.20.jar
2.定义一个变量来装,但必须提供get()方法,
3.
public String getUserJson2() {
		try {
			String toCompDept = ServletActionContext.getRequest().getParameter("toCompDept");
			if(StringUtils.isNotBlank(toCompDept)) {
				QueryHelper helper = new QueryHelper(User.class, "u"); // 要明确最终去哪个表去查!,所以需要userService
				helper.addCondition("u.dept = ?", toCompDept);
				List<User> userList = userService.findObjects(helper);
				
				return_map = new HashMap<String, Object>();
				return_map.put("msg", "success");
				return_map.put("userList", userList);
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
		return SUCCESS;
	}
// 还需要继承json-default,返回类型设置为json,返回变量是return_map, 参数定义为root
<package name="sysHomeJson-action" namespace="/sys" extends="json-default">
		<action name="home_getUserJson2" class="cn.itcast.home.action.HomeAction" method="getUserJson2">
			<result type="json">
				<param name="root">return_map</param>
			</result>
		</action>
	</package>

使用 quartz:
1.导入jar包:org.springframework.context.support-3.0.2.RELEASE,quartz-1.8.6

2.简单触发器其实跟TimerTask差不多,Java.util.TimerTask其实也可以配置在spring中,按道理来说任何java代
码都可以配置applicationContext.xml中

quartz的任务调度(xml配置)就一句话
哪个类的哪个方法(任务),在什么时机,被scheduler工厂去执行!!!
引入这种组件,应该去官网看看使用的步骤,官网一般都说的特别详细!

投诉统计图:
需求:获取某年的每个月的投诉数
我前台只传了一个参数“年”,怎么根据参数“年”通过后台去数据库中将这一年的每个月的投诉数查询出来,然后返回给前台渲染

根据上面这一句你应该写出一条sql:

字段: 月份, 每月统计数
表: 投诉表
条件: 某年

select
	month(comp_time),count(*),group_concat(month(comp_time))
from 
	complain c 
where 
	year(comp_time) = 2020
group by
	month(comp_time);

如上图所示:把2020年有投诉数的月份和个数都统计统计出来了,感觉挺好,但是我要的是每一个月投诉的个数,就是这个月投诉数是0,你也给我显示出来,这么分析的话,你可能会说我先拿到这个月份的值,没有的我去后台判断再补充,那样就太麻烦了,有没有就是无论这个月投诉数是多少,你都得给我显示出来,所以我们想到了新建一张月份表,里面1-12份都有,然后左连接(把月份表放左表),这样到时候不管满不满足条件,这12个月始终会显示出来的!

-- 让投诉表和月份表联合查询,怎么去除笛卡尔积?, 投诉时间可以得到月份让它等于月份表中的月份就行了
select 
	m.imonth,count(c.comp_id)
from 
	t_month m left join complain c
on 
	m.imonth = month(c.comp_time) and year(c.comp_time) = 2020
group by
	m.imonth
order by 
	m.imonth asc
注意:year(c.comp_time) = 2020 这里必须使用and才有效果,我使用where居然还没反应!左表都不显示12个月

就得到了我们想要的效果!

虽然达到了效果,但是你想想,要是有10w条投诉,和month表连接查询就会产生120w条垃圾数据,然后再从其中筛选条件,sql语句的效率比较低下,所以我们可以优化一下sql

问题在于连接时数据过于庞大,我们可以先写一个子查询,将10w投诉数筛选一下,然后在去和month进行左连接

-- 把它当成一个临时表去和month表进行左外连接, 临时表筛选后最多剩12个记录,和month表关联产生的笛卡尔积最多144条,比起之前的120w条少太多了
select month(c.comp_time),count(*) from complain c where year(c.comp_time) = 2020 group by month(c.comp_time);

select
	m.*,t.cnt
from 
	t_month m 
left join 
	(select month(comp_time) t1,count(*) cnt from complain where year(comp_time) = 2020 group by month(comp_time)) t
on 
	m.imonth = t1
order by
	m.imonth asc;
注:左连接时必须加上on条件不然还会报错,不知道为什么!,如下语句会报错
select
	m.*,t.cnt
from 
	t_month m 
left join 
	(select month(comp_time) t1,count(*) cnt from complain where year(comp_time) = 2020 group by month(comp_time)) t
	
You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '' at line 6
注:说第六行有语法错误
后台的接口调试:
http://localhost:8080/test2/nsfw/complain_annualStatistic.action?year=2020

后来发现:struts把我其他拥有get方法的属性给格式话成json返回到前台了,后来发现没在配置文件中指定
        root的参数!(也就是你需要将它格式化json的属性!)
<result name="annualStatisticData" type="json">
	<param name="root"></param>
</result>

给前台渲染

根据上面这一句你应该写出一条sql:

字段: 月份, 每月统计数
表: 投诉表
条件: 某年

select
	month(comp_time),count(*),group_concat(month(comp_time))
from 
	complain c 
where 
	year(comp_time) = 2020
group by
	month(comp_time);

[外链图片转存中…(img-5HrK8cUN-1590203268281)]

如上图所示:把2020年有投诉数的月份和个数都统计统计出来了,感觉挺好,但是我要的是每一个月投诉的个数,就是这个月投诉数是0,你也给我显示出来,这么分析的话,你可能会说我先拿到这个月份的值,没有的我去后台判断再补充,那样就太麻烦了,有没有就是无论这个月投诉数是多少,你都得给我显示出来,所以我们想到了新建一张月份表,里面1-12份都有,然后左连接(把月份表放左表),这样到时候不管满不满足条件,这12个月始终会显示出来的!

-- 让投诉表和月份表联合查询,怎么去除笛卡尔积?, 投诉时间可以得到月份让它等于月份表中的月份就行了
select 
	m.imonth,count(c.comp_id)
from 
	t_month m left join complain c
on 
	m.imonth = month(c.comp_time) and year(c.comp_time) = 2020
group by
	m.imonth
order by 
	m.imonth asc
注意:year(c.comp_time) = 2020 这里必须使用and才有效果,我使用where居然还没反应!左表都不显示12个月

[外链图片转存中…(img-8uQhouQb-1590203268282)]

就得到了我们想要的效果!

虽然达到了效果,但是你想想,要是有10w条投诉,和month表连接查询就会产生120w条垃圾数据,然后再从其中筛选条件,sql语句的效率比较低下,所以我们可以优化一下sql

问题在于连接时数据过于庞大,我们可以先写一个子查询,将10w投诉数筛选一下,然后在去和month进行左连接

-- 把它当成一个临时表去和month表进行左外连接, 临时表筛选后最多剩12个记录,和month表关联产生的笛卡尔积最多144条,比起之前的120w条少太多了
select month(c.comp_time),count(*) from complain c where year(c.comp_time) = 2020 group by month(c.comp_time);

[外链图片转存中…(img-ao3MXkFJ-1590203268283)]

select
	m.*,t.cnt
from 
	t_month m 
left join 
	(select month(comp_time) t1,count(*) cnt from complain where year(comp_time) = 2020 group by month(comp_time)) t
on 
	m.imonth = t1
order by
	m.imonth asc;
注:左连接时必须加上on条件不然还会报错,不知道为什么!,如下语句会报错
select
	m.*,t.cnt
from 
	t_month m 
left join 
	(select month(comp_time) t1,count(*) cnt from complain where year(comp_time) = 2020 group by month(comp_time)) t
	
You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '' at line 6
注:说第六行有语法错误
后台的接口调试:
http://localhost:8080/test2/nsfw/complain_annualStatistic.action?year=2020

后来发现:struts把我其他拥有get方法的属性给格式话成json返回到前台了,后来发现没在配置文件中指定
        root的参数!(也就是你需要将它格式化json的属性!)
<result name="annualStatisticData" type="json">
	<param name="root"></param>
</result>
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 技术工厂 设计师: CSDN官方博客
应支付0元
点击重新获取
扫码支付

支付成功即可阅读