实施说明:
阶段一:登录模块模块的开发
没有结合拦截器进行开发,仅仅使用的是Struts2以下围绕登录模块进行:
技术实现:java+struts2+jdbc+jsp+jquery/ajax
表示层:jsp+jquery/ajax
业务层:action充当业务层
控制层:Struts2
数据层:jdbc + dao
基础工作:
(1)导入相关的Struts2中使用到的jar包。特别注意不能导错。
json导入到lib下;jQuery导入到新建一个js文件,然后粘贴进去;
其他都要正常的导入。
(2)配置前端控制器:filter充当,在web.xml中配置
(3)导入样式.css文件,在JSP页面中引入。
配置文件:
(4)配置struts.xml文件,本项目需要引入的方式:
struts.xml:主要起到引入全部配置文件的作用。例如:
<struts>
<!-- 引入登录模块配置信息 -->
<include file="struts-login.xml"></include>
</struts>
比较用户名:
(5)创建Admin实体类,与DB数据库字段相呼应。
(6)编写登录的接口、及实现类(ILoginDAO、LoginDAOImpl)
(7)编写登录的Action类(LoginAction),具体的调用DAO实现类
(通过工厂的方法去调用),比较然后得到数据,判断。
(8)配置Struts.xml文件.登录成功则进首页,失败则停留在登录页
输出验证码:
(补充:只要是数据为空,即admin==null,return "fail"
其他抛异常的时候return "error",配置文件的result name="fail")
(9)编写生成验证码的工具类,ImageUtil类(普通类),生成后保存到map里面return map;。
(10)编写输出验证码的属性类,ImageCodeAction类(Action类),输出到JSP页面。
(11)编写登录页面的action类,直接在LoginAction(Action类)添加比较验证码的方法,跟
比较用户名密码都是在同一个execute()方法里。
补充总结:
第一:若一个类的方法使用的是纯对象类型的方法,
即public AdminInfo findByCodeAndPwd(String adminCode,String passWord)
也就是项目中MyNetCtoss_Struts2中的LoginAction,若账号密码不正确,那么
数据是查询不到的,这样输出下面的admin.getTelePhone()的话就会引发500报错!!
//System.out.println(admin.getTelePhone());
但是这样输出:System.out.println("admin的数据是: "+admin);
账号密码不正确会输出null,不会报错500.
第二:若一个类的方法使用的是不是纯对象类型的方法,即public List<AdminInfo>
findByCodeAndPwd(String adminCode,String passWord),如果账号密码不正确,那
调用时:List<AdminInfo> admin = dao.findByCodeAndPwd(adminCode, passWord);
输出:System.out.println(admin);//即使账户密码不正确它永远也不会输出null,
因为它是集合的类型形式,即使查询不到数据,输出的是“[]”,我们知道null与任何数据
比较都是false,永远不成立的。所以下面的判断:永远不可能return "fail";下面的
比较判断永远都是return "success";
if(admin==null){
return "fail";
}else{
return "success";
}
第三:总之使用一个方法时一定要弄明白它返回的数据类型是什么,是返回我们需要的
数据还是返回null,还是空,null不等于空,空也不等于null,二者比较永远不成立。
第一阶段后期任务:
1、分别使用ajax/jquery作登录检查
2、使用拦截器作登录首页
实施说明:
阶段二:资费界面模块的开发
以下围绕资费界面进行:
技术实现:java+struts2+jdbc+jsp+jquery/ajax
表示层:jsp+jquery/ajax
业务层:action充当业务层
控制层:Struts2
数据层:jdbc + dao
二、资费模块开发(分页、增加、启用、修改、删除)
分页显示:
(12)创建资费表cost表的实体类
(13)创建资费模块分页显示的接口并编写实现类,同时需要测试方法
能否返回所需要的数据。使用工厂的方法。
注意dao.properties文件的配置name:接口名 value:实现类的具体包名类名
ICostDAO dao = (ICostDAO)Factory.getInstance("ICostDAO");
是接口,不是实现类。
(14)创建资费模块的Action类---FindCostPageAction(含有execute方法)
注意:cost = dao.findByPage(page, pageSize);//这个数据查询到之后是要在findCost.jsp里显示的
totalPage = dao.totalPage(pageSize);
//cost前面无需List<Cost>,因为变量定义的部分已经声明了。
(15)注意JSP页面的接收数据:
<s:iterator value="cost">要与上面的变量名一致,方能接收到数据。
(16)配置struts.xml文件,本项目需要引入的方式:
struts.xml:主要起到引入全部配置文件的作用。例如:
<struts>
<!-- 引入登录模块配置信息 -->
<include file="struts-login.xml"></include>
<!--引入资费模块配置信息 -->
<include file="struts-cost.xml"></include>
</struts>
(17)登录首页后,为了能成功的链接到各个模块。配置文件必须按下面的配置来描述:
第一种解决办法:
只需要一步即可:在package的namespace="/相同的包名空间",即指定相同的包名空间。
其他均不变,注意包名一定不能相同。这样就可以解决!!
第二种解决办法:
在struts-main.xml这个公共包package中定义了<package name="netctoss"
namespace="netInter" extends="json-default">,然其让其他的配置文件的package包
extends="netctoss"继承了netctoss(其他包必须省略namespace,因为继承了netctoss中的
namespace),目的为了能够使登录成功进入首页后,能与每个模块页面之间相互链接。前提
就是保证namespace相同。所以继承是一个很好的解决办法。
(18)FindCostPageAction使用的拦截器详析:
①目前只有FindCostPageAction引入了拦截器栈,当登录成功后,即session上绑定有了admin
的数据,资费的分页FindCostPageAction才会被调用(调用Action之前首先会调用拦截器的方法
如果拦截器的方法不被调用,那对应的action,即分页的action不会被调用,这就是拦截器的
优势所在)
②本项目的登录暂时与拦截器无关,条件符合即可登录首页,即账号密码验证码正确即可登录。
③因为FindCostPageAction中调用了拦截器,在这个分页的action被调用之前,首先会调用拦
截器中的方法。拦截器方法中进行判断,若admin==null,(admin数据绑定在session上,绑定的
类是登录的action--LoginAction。在LoginInterceptor拦截器中若获取不到数据,证明登录不
成功那么拦截器的方法中将不会调用FindCostPageAction,而是return "login",将返回到name
为login的result,即状态还是停留在登录的页面。这就体现了拦截器的原理:动态的调用action
在action方法被调用之前或之后,在拦截器的方法中加入某些操作,本项目是在之前调用)
④调用拦截器的action文件配置Struts.xml中的<iterator-ref name=""></iterator-ref>下的
result,这个result的name=“success”是来自FindCostPageAction中的success,并不是来自拦截器
的。拦截器中的return可以返回任何字符串,不是success都可以。
⑤在LoginAction中只要把下面的session注释掉,那session上就没有admin的数据了。虽然成功
登录了首页,由于上面的拦截器那里加入了操作if(admin==null){数据为空,则返回到result中
name为login的对应的result,return "login";即表达的思想就是:连资费的数据都不存在,何
谈分页。故重定向到登录的界面。
⑥跳转到分页页面的Action.在调用findCostPageAction之前首先会调用拦截器栈,这个是拦截
器的思想,检查admin是否为null,若为null则继续停留在登录的界面,防止没有成功登录首页
的前提下,直接访问分页的页面是不允许的,也不符合软件的逻辑思维。总之,就是链接分页之
前首先得成功登录首页。
(19)编写拦截器类,正式引入拦截器的概念:
主要应用在分页检查与登录检查。若admin==null全部返回登录界面。分页检查:说明既然数据
不存在,何谈分页。登录检查:首先必须成功登录首页方能访问其他的数据,直接访问分页数据
不符合软件的设计思想,不允许非法登录。
SomeInterceptor implements Interceptor
(20)做资费模块的增加页面:
资费增加界面分析:
资费类型:(4-基本时长;5-基本费用;6-单位费用)
type==1时,为包月---只有基本费用。基本时长、单位费用均为null,同时要显示灰色;
type==2时,为套餐---全部都有;
type==3时,为计时---只有单位费用。基本时长、基本费用均为null,同时要显示灰色;
需要获取相应的操作节点:
var inputArray = document.getElementById("main").getElementByTagName("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 = "";
inputArray[6].disabled = "disabled"//不可输入任何值
但是这个 inputArray[6].readonly = “readonly”不成立。
放在input里才会成立。只读模式,只能显示,不可编辑
}
精髓分析:
readonly: 这里意为只读的意思,即不可输入(为null的意思),只能看,同时显示
灰色,只要是readonly的布尔值为true的,都要加上readonly的类选择器。
每一个属性都需要判断readonly的Boolean值为true或false。若为true,DB里的值为null,
意为只读,value属性为空,加上readyonly的类选择器(显示灰色的CSS)。若为false,意为该
资费类型需要输入相应的值,DB里有值,类选择器为“width100”;
DOM里的input、div等遍历的时候是从0开始的。
注意:加上readonly类选择器时,一定要在前面加上空格,否则无效。即:
inputArray[4].className += " readonly";
css:
background:大的背景(整个版面的);background-color:小的背景(input的输入框)
分类选择器:元素选择器与类选择器的组合。
CSS应该有两个CSS表:一个定义颜色(背景图片属于颜色)global_color.css,
另外一个定义大小形状global.css。
CSS相关属性分析:
/*height: 28px;*/
line-height: 28px;//该DIV里的文字的每一行所占的行的高度。不是该块级的高度;
height:80px;//改变的是div块级纵向的高度;
width:80px;//改变的是div块级横向的宽度;
float: right;//文字不能浮动;div之间的内容向右浮动。不是换行。若块级ABC全部浮动,块级将并排在一行上了;
假设:块级A左浮动,块级B不浮动,块级C也不浮动。不浮动的块级B将占据页面的最左边的位置,因该位置已有块级A左浮动,
那将遮住不浮动的块级B,即不显示没有浮动的块级B。因为块级C也不浮动,理应占据页面的最左边的位置,
但是现在已经被块级B占据了,那只能是排在块级B的下面,所以可以显示,若此时块级B左浮动,那块级C将
占据页面最左边的位置,即不显示。
若不浮动的块级下还有不浮动的块级,因为之前已经有块级占据页面的最左边的位置,那本
overflow: hidden;//属性可以保证div的高度或宽度不变。div里添加的东西再多,高度或宽度也不变。超出的部分隐藏
text-align: left;//块级文字的对齐方式。有三个值:center,left,right
padding-left:5px;//文字距离左边的框体5个像素。实际上了框体横向加了5个像素。即80+5=85px;
//padding-right:5px;一个道理。会改变横向的宽度,即width的宽度。
font-size:60pt;//框体里文字的字体大小;
font-weight: bold;//字体加粗;
color:red;//框体里文字的颜色;
border:5px solid #8ac1db;//div的边框为5个像素,上下左右各是5,改变框体的四边的像素。
min-height:100px; //设置了div高度的最小值。若max-height缺省,即为自适应内容的高度。
max-height:200px;//设置了div高度的最大值。若内容超过将溢出该div框体。
overflow-x:hidden;//横坐标隐藏;
overflow-y:auto;//纵坐标自动;感觉这两个x\y没太大的意义;
background:url(images/login_bg.jpg);//背景图片,使用PS处理好,默认是repeat,图片将
会重复,如果不想使用重复应该定义no-repeat;
制作背景画面:在昵图网下载psd页面后,PS打开。去底:选中后按删除。给背景颜色上
黑色,然后再选中大的背景图层,使用魔术棒选中界面,按del键,再按取消键。然后选中
大的背景图层,按Ctrl+M变换成白色,然后使用修补工具把要用的雪花圈起来,然后按
Ctrl+J复制,然后就可以拖动到背景的页面作修饰。
登录页面CSS解析:
1、大的背景图片:
body.login{
background-color: #00629f;//整个版面的颜色,补丁的颜色。
设置好背景图片之后,利用这里来填充其他没有被背景图片覆盖过的
空白颜色。要求这个颜色要与背景图片的颜色一致,方能达到融合的效果!!!
background-image: url(images/login_bg.jpg);//背景图片
background-position: center top;//图片的位置,居中
background-repeat: no-repeat;//图片是否重复,这里选择不重复
}
按照以上的方法搞定大的背景图。
2、登录框:
①导入登录框图片时:div.login_box{background:url(images/login_box.png) no-repeat;}
<div class="login_box">xxxxxx</div>,必须保证这个div必须存在文字,才能显示出登录框
的图片。
②设置登录框的宽度、高度、字体大小、字体粗细、内外边距
div.login_box{
background:url(image/login_box.jpg) no-repeat;//输入框的背景图片
width:450px;//输入框的宽度
height:263px;//输入框的高度
font-size:12pt;//输入框的文字大小
font-weight:bold;//输入框的文字粗细
margin:170px auto;//输入框的外边距
padding-top:100px;//输入框的内边距距离顶部的距离
padding-left:5px;//输入框的内边距距离左边的距离
}
③设置input的属性:
input{
height:25px;//输入框的高度
line-height:27px;//输入框的行高,行高太高,光标会看不见。
padding-left:20px;//光标离输入框左边的距离。
}
④输入框前的文字:
td.login_info{
text-align:right;//文字在单元格里靠右对齐
width:150px;//单元格的宽度
}
⑤
3、登录框里字体颜色:
div.login_box table tr td{
color:#FFF;//白色
}
调整框体的大小:
td.login_info
{
text-align: right;//文字在单元格里对齐的位置,向右对齐
width: 150px;//单元格的大小
padding-right: 5px;//文字距离元素的右边内距离为5个像素
}
页面的auto会使右边有滚动条:消除滚动条的方法是使用margin-left:平均横的长度
注意:输入框在myeclipse里的浏览器Mozilla不能显示光标在中间,但是IE却可以。
若在360浏览器中访问该页面可以正常显示光标在中间。
知识点:单选框radio如何把选中的值发送给服务器?
<div>
<input type="radio" name="fadFeeType" value="包月" id="monthly"/>
<label for="monthly">包月</label>
<input type="radio" name="fadFeeType" value="套餐" checked="checked" id="package"/>
<label for="package">套餐</label>
<input type="radio" name="fadFeeType" value="计时" id="BaseCost"/>
<label for="BaseCost">计时</label>
</div>
解决:只需要在input里面添加value属性,并且指定value的值,而name="fadFeeType"
已经绑定了value的值了;既然是单选,三个只能选其中一个,所以name必须相同;name
的值与action中的属性一致,即private String fadFeeType,这样就可以获取到对应单选
框发送给服务器的数据。
总之:input输入框的value属性是用来向服务器提交参数的。如果指定了value的值。那点
submit按钮后,页面即可向服务器提交value的值,如果没有指定value的值,页面将向服
务器提交用户输入的值。
如下的是资费增加页面的JavaScript文件解析:
页面的提示信息:
1、当用户没有任何操作时,是正常的显示“要求的输入格式信息”
2、当用户没有按要求输入信息时,才会加入错误的样式,例如字体显示红色的样式
3、当选择包月时,基本时长、单位费用的输入框是灰色的,即不可输入状态,需要添加
属性disabled = true;或disabled = "disabled"都可以。其他的资费类型同理。只要判断了
一个资费类型input输入框的disabled属性,那么剩下所有的资费类型的input输入框,都
要判断,否则其他的没有判断的输入框,样式将会受影响。
4、当选择资费类型时,例如选择了“包月”,只有“基本费用”的输入框可以输入数值,当鼠
标光标移到该输入框但没有输入任何值,但光标点击其他的地方,立即出现错误提示信息。
这时如果选择另外一种资费类型,但是刚才的“基本费用”提示信息还存在。这样就不符合
逻辑,应当是:如果鼠标选择其他的资费类型时,原先的提示信息应当消除,毕竟当我选
择其他的资费类型时,都没有任何操作,就有错误的提示信息,这样就不符合软件设计思
维。解决办法如下:
在每个资费类型的函数里的首行清除之前类型留下的样式以及留下的值;
if (type == 1) {
$("#baseCost").val("");//清除之前输入框留下的值
$("#baseCostMsg").removeClass("error_msg");//清除样式
$("#baseCostMsg").text("*0-99999.99之间的数值");//添加原始的信息
$("#baseDuration").val("");
$("#baseDurationMsg").removeClass("error_msg");
$("#baseDurationMsg").text("*1-600之间的整数");
$("#unitCost").val("");
$("#unitCostMsg").removeClass("error_msg");
$("#unitCostMsg").text("*0-99999.99之间的数值");
三个都是需要这样添加。
5、需要判断的资费类型对应的输入框提示信息:
包月:基本费用
套餐:基本时长、基本费用、单位费用
计时:单位费用
①包月:基本费用的判断:
基本费用输入框的jQuery校验方法解析:
$(function(){
$("#baseCost").blur(function(){
var bc = $(this).val();---获取用户输入的值
var Regeg_name = /^(0|[1-9]{1,5}(\.[\d]{1,2})?)$/;--正则表达式
var bc_info = document.getElementById("baseCostMsg");--定位到提示信息位置
if(bc.match(Regeg_name)==null){--比较方式,固定的格式
$("#baseCostMsg").text("*请输入0-99999.99之间的数值!");
$("#baseCostMsg").addClass("error_msg");
}else{
$("#baseCostMsg").text("*输入基本费用数值有效");
$("#baseCostMsg").removeClass("error_msg");
}
});
});
使用jQuery的经典开场白:$("#baseCost")使用单引号或双引号都可以识别
$(function(){
$("#baseCost").blur(function(){
具体的判断方法});
});
②套餐:基本时长、基本费用、单位费用
套餐的“基本费用”使用的jQuery与包月的“基本费用”是同一个输入框,
因此只要验证一次即可。
“基本时长”:
$(function(){
$("#baseDuration").blur(function(){
var bd = $(this).val();
var Regeg_str = /^([1-9]|[1-5][0-9]{1,2}|600)$/;
if(bd.match(Regeg_str)==null){
$("#baseDurationMsg").text("*请输入1-600之间的整数");
$("#baseDurationMsg").addClass("error_msg");
}else{
$("#baseDurationMsg").text("*基本时长数值有效");
$("#baseDurationMsg").removeClass("error_msg");
}
});
});
解析:其实三个输入框的校验方法都是一样的,jQuery的经典开场白,然后定位到要操作
的节点,获取到用户输入的值,写正则表达式,比较用户输入的值是否能通过正则表达式,
正则比较的固定格式:
var Regeg_str = /^([1-9]|[1-5][0-9]{1,2}|600)$/;输入1-600之间的整数
if(input_str.match(RegExp)){
//在这里编写比较后的要添加行为----这里是不通过的表现
}else{
//在这里编写比较后的要添加行为----这里是通过的表现
}
input_str:用户输入的值 RegExp:正则表达式
"单位费用":
var Regeg_str = /^(0|[1-9][\d]{0,4}(\.[\d]{1,2})?)$/;0-99999.99之间的数值
$(function(){
$("#unitCost").blur(function(){
var uc = $(this).val();
var Regeg_str = /^(0|[1-9][\d]{0,4}(\.[0-9]{1,2})?)$/;
if(uc.match(Regeg_str)==null){
$("#unitCostMsg").text("*请输入0-99999.99之间的数值");
$("#unitCostMsg").addClass("error_msg");
}else{
$("#unitCostMsg").text("*单位费用数值有效");
$("#unitCostMsg").removeClass("error_msg");
}
});
});
原理与其他三个的校验方法一致,不再赘述。
③校验资费说明:
$(function(){
$("#costExpress").blur(function(){
var ce = $(this).val();
var Regeg_str = /^([\u4E00-\u9FA5A-Za-z0-9_\/\.]{5,100})$/;
if(ce.match(Regeg_str)==null){
showExpressInfo(true);//调用函数的形式
window.setTimeout("showExpressInfo(false);",3000);//控制显示错误提示信息的时间
}else{
showExpressInfo(false);//没有错误的时候执行函数else的部分
}
});
});
//控制显示错误提示信息的时间
function showExpressInfo(type){
if(type){
$("#costExpressMsg").text("*按要求输入格式正确的资费说明描述");
$("#costExpressMsg").addClass("error_msg");
}else{
$("#costExpressMsg").text("*100长度的字母、数字、汉字和下划线的组合");
$("#costExpressMsg").removeClass("error_msg");
}
}
知识点:
在选择下一个资费类型时,必须要清除之前类型留下的值,样式等。
清除输入框的值:$("#baseCost").val("");
资费增加的页面中“资费类型”单选项不需要获取它的值,它主要的作用是控制“基本时长”、
“基本费用”、“单位费用”的输入框能否输入数据而已。数据库里的NAME是“增加页面”的资
费名称。而ID需要在DB里创建自动增长的序列(seq_on_cost),使用的时候需要改为:
seq_on_cost.nextval)。
资费列表的增加中input输入框中只要有一个输入值不符合要求,就不能提交到服务器。
必须全部符合要求了才能提交。判断条件如下:
在js开头处定义全局变量:
var nameFlag = false;
var baseDuration = false;
var baseCost = false;
var unitCost = false;
var descrCost = false;
然后在各个函数中判断,如果有那个不符合要求,那该值就为false。
最后在提交按钮的函数中再一次总的判断:
function showResult(){
//判断资费名是否重复 &!baseDuration&!baseCost&!unitCost&!descrCost
if(nameFlag==false||
baseDuration==false||
baseCost==false||
unitCost==false||
descrCost==false){//结果为true执行这里的代码块,然后return;
showResultDiv(true);
window.setTimeout("showResultDiv(false);", 3000);
return;
}else{
document.forms[0].submit();//提交第一表单
}
资费分页页面的操作部分(启用、修改、删除):
修改:
为什么点击分页页面的“修改”按钮可以将id发送给接收的action?解析如下:(此问题
已经总结并且发表到空间了)
为什么点击分页页面的“修改”按钮后却没有数据回显。已经单独做笔记!---不能在cost
前面加上类型Cost.
修改页面使用的是UI标签,自动生成HTML标记的,可以从浏览器的查看源代码中看出。
对应如下:
<s:textfield></s:textfield>-----<input type="text"/>实际上就是input
<s:radio></s:radio>----<input type="radio"/>实际上也就是input
切换资费类型,因为多了一个资费ID故inputArray[5]从第五开始
在增加的资费里没有资费ID,所以从inputArray[4]开始。也就是数从input
的序号,序号是从0开始的。
修改好后的保存实现类:当修改好后调用保存的实现类,之后不能finally{conn.close();};
因为如果关闭了就不能再次访问分页的页面,因为当我们修改好后保存到DB,然后再
跳转到分页的页面,这样才是符合软件的逻辑。
通过修改资费后有发现:
在jQuery的验证中,除了提交新增的保存(或修改保存)验证与资费说明验证外,
剩下的所有验证都是基于选择的“资费类型”里面,因为他们的验证项目不相同,例如
选择"包月"时,基本时长、单位费用就不需要验证,因为"包月"只有基本费用,只需要
验证基本费用与资费说明即可。所以:baseDuration、unitCost都是等于true。直接写:
baseDuration = true;unitCost = true;这样是为了能在最后保存的能顺利提交。只有
它们全部都是true时才能提交资料。没有的项目就不需要验证,但是又能保证最后的提
交结果不受影响,所以只能是不需要验证的项目直接赋值为true,其他的同理。赋值的
时候要注意不能是 var baseCost = true;这样写是错误的,不需要在前面加上var ,因
为它们都已经在最开始的时候声明了类型,如果还在这里添加类型,那就是错误的。如
果最后不显示:检查的方法是alert("unitCost:"+unitCost);看弹出的时true还是false。
删除:根据ID删除资费数据
<input type="button" value="删除" class="btn_delete"
οnclick="deleteFee(<s:property value="id"/>);"/>
当点击这个删除键之后,这个id就随着deleteFee这个函数一起触发到findCost.js这
脚本文件。
function deleteFee(id){
var r = window.confirm("您确定将资费ID为"+id+"的记录删除?");
if(r){//r为true时执行,false不执行
window.location.href="deleteCost.action?id="+id;
document.getElementById("operate_result_info").style.display = "block";
}
}
状态维护:(开通、暂停)
/*3、状态维护*/
需要自动识别状态,如果状态是“开通”,操作为“暂停”,
状态为“暂停”的,按钮为“开通”,首先使用立即加载函
数window.onload。把按钮封装成数组,把状态封装成数组,
遍历状态,找出他们的通项公式:[3*i+4];
//使用立即加载函数window.onload
window.onload = function(){
showStatus();//实际调用的函数
};
function showStatus(){
//把按钮封装成数组
var inputArray = document.getElementsByTagName("input");
//把状态封装成数组
var spanArray = document.getElementsByTagName("span");
//遍历状态
for(var i=0;i<spanArray.length;i++){
if(spanArray[i].innerHTML=="开通"){
//状态为“开通”的,按钮为“暂停”
inputArray[3*i+4].value="暂停";
}else if(spanArray[i].innerHTML=="暂停"){
//状态为“暂停”的,按钮为“开通”
inputArray[3*i+4].value="开通";
}
}
}
/*4、根据资费状态调用不同的函数,达到启用"开通"或"暂停"资费的目的*/
function startFee(id){
//定位到当前所单击的按钮,因为id是唯一的,所以可以获取到唯一的value
//即按钮的value也是唯一的,此时的id是一个值
var status = document.getElementById(id);
if(status.value=="开通"){
var r = window.confirm("您确定要开通ID号为"+id+"的资费套餐?");
if(r==true){
window.location.href="stopCost.action?id="+id;
document.getElementById("operate_result_info").style.display="block";
}else{
return false;
}
}else if(status.value=="暂停"){
var r = window.confirm("您确定要暂停ID号为"+id+"的资费套餐?");
if(r==true){
window.location.href="stopCost.action?id="+id;
document.getElementById("operate_result_info").style.display="block";
}else{
return false;
}
}
}
实施说明:
阶段三:围绕账务账号模块的开发
目前能互通链接的有:资费分页、资费新增、首页(toIndex)、账务分页、账务新增
补充知识点:
account.jsp页面的显示
<s:iterator value="account">
<tr>
<td><s:property value="id"/></td>
<td><a href="javascript:detailAccount(<s:property value="id"/>);"><s:property value="realName"/></a></td>
<td><s:property value="idcardNo"/></td>
<td><s:property value="loginName"/></td>
</tr>
</s:iterator>
注意是遍历的方式(OGNL表达式),显示的值value;
修改页面的回显:
<div class="input_info">
<s:textfield name="ant.id" id="id" cssClass="readonly" readonly="true"/>
<div class="validate_msg_long">* 提示:账务账号ID不可更改</div>
</div>
注意是直接输出的方式(UI标签):name直接与action的属性绑定的
1、声明接口:
分页显示的页面容量:page,pageSize
public List<Account> findByCondition(String idcardNo,String realName,
String loginName,String status,int page,int pageSize);
实现类实现接口,实现分页显示的功能。
分页的总的页数:totalPage
public int totalPage(String idcardNo,String realName,
String loginName,String status,int pageSize);
2、转向Action:
实现类需要的参数都是从action的输入属性获取到的,然后action负责把参数传
递给实现类,因为action需要调用实现类,而参数是从JSP页面端传递过来的,这就
需要一个客户提交的动作,提交给action。
在这个account的分页显示页面,可以带条件的查询分页,也可以不带条件的分页。
不带的情况下就是查找全部的账务页面。
在JSP页面这里比cost先进的地方是它是动态获取JSP页面的页码的,实现方式如下:
中间的页码:
<s:iterator begin="1" end="totalPage" var="p">
<s:if test="#p==page">
<a href="javascript:toPage(<s:property/>);" class="current_page"><s:property/>
</a></s:if>
<s:else><a href="javascript:toPage(<s:property/>);"><s:property/>
</a></s:else>
</s:iterator>
分析:这就是动态的把当前页面发送给action中的输入属性page,page如何传输过去?
这就需要一个提交的动作:
<a href="javascript:toPage(<s:property/>);" class="current_page"><s:property/>
</a>
当单击页码时,就会把当前页码发送给这个toPage(currPage);这个函数,这个函数
获取到当前的页码数,然后再把这个页码数字赋给page;实现是:
<input name="page" type="hidden" id="currPage"/>,然后函数toPage(currPage);有
提交的动作;name="page"是与action属性名绑定的,action获取到页码之后,就会根据
页码去查找该页面所要显示的数据,根据rownum的范围;
假如:page = 1;pageSize = 5;
上一页的按钮:
同样是需要把页码数发送给函数toPage();函数收到之后,提交,action返回数据。
<s:if test="page==1"><a href="#">上一页</a></s:if>
<s:else>
<a href="javascript:toPage(<s:property value="page-1"/>);">上一页</s:else>
下一页的按钮同理(略);
首页:
直接给当前页赋值为1,即为首页:
<a href="javascript:toPage(<s:property value="1"/>);">首页</a>
末页同理:直接赋值为totalPage
<a href="javascript:toPage(<s:property value="totalPage"/>);">末页</a>
根据四个条件查询:
当点击搜索按钮时,需要把其中之一的条件或四个条件同时传递给action,同时
需要调用分页的js函数:toPage();形式如下:
<input type="submit" value="搜索" οnclick="toPage(<s:property value="1"/>)"/>
函数里面需要赋予页数作为参数。否则当action调用四个条件查询时,服务将报错,虽然
报错但是页面没有受到影响;个人分析为:需要传给action中的page这个输入属性,作为
当前页。然后action根据page去查询数据。
function toPage(currPage){
//alert(currPage);
document.getElementById("currPage").value = currPage;
document.forms[0].submit();
}
职业选项:
<s:select name="occupation" list="#{'0':'干部','1':'学生','2':'技术人员','3':'其他'}"
headerKey="4" headerValue="--请选择--"/>
name与action中的status绑定,当提交之后action将会获取到key值,即数字的部分;因为
这样的形式是以key:value的方式存在的,即map的形式;
headerKey="4" headerValue="--请选择--":默认显示的第一项
性别选项:
<s:radio list="#{'0':'男','1':'女'}" name="gender" value="0"/>
默认选中value="0"即男 name="gender" 与action绑定;使用这个表达式
已经默认使用了laber for的功能。
新增的功能:虽然界面没有last_login_time,last_login_ip,但是写SQL语句时,
同样要赋值问号:
String sql = "insert into account values(" +
"seq_on_account.nextval,?,?,?,'0',SYSDATE,?,?,?,?,?,?,?,?,?,?,?,?,?,?)";
生日:需要转换时间的格式,因为从界面获取到的时间各个与DB的时间格式不一样,需要
如下转换方可顺利insert到DB:
java.sql.Date birthday = new java.sql.Date(birthdate.getTime());
注意jquery要验证非空且唯一的几个值:login_name、idcard_no
密码的第一次与第二次输入的验证:
使用jQuery验证,第一次验证的方式与第二次验证的思想一致。都要获取到对方的
输入的密码。如果对方为空,只需要验证自身的正则表达式即可;如果对方不为空既要
验证正则,同时还要验证与对方的输入是否相等(只能使用==比较,不能使用match)。
可选项的收缩:如果<div id="optionalInfo" class="hide">的类选择器为hide,那么
它的图标是显示打开的状态,如果为show,则相反。
<img src="../images/show.png" alt="展开" οnclick="showOptionalInfo(this);" />
this是绑定到src的,更换图片。
可选项的判断:
//因为可选项是在点击输入框后才会触动函数,故赋初值为true,方能顺利的判断
提交的结果是否为true还是false,没有点击可选项时,如果赋初值为false,那么
如果它们为空且没有点击触动输入框的情况下,将是false,那就不能提交。如果
赋初值为true,在它们为空且没有点击触动输入框的情况下,将直接是true,那么
就可以顺利的提交,假若他们在最终的验证中传来Boolean值也能顺利的判断。
var recommenderIdcardNos = true;
var emails = true;
var mailAddresss = true;
var zipcodes = true;
var qqs = true;
新增account的推荐人的身份证号码的验证:
$.post("checkRecommenderInfo",
{"recommenderIdCardNo":recommenderIdCardNo},
function(data){
var account = data;
alert(account.id);
}
);
account是从checkRecommenderInfo对应的action中获取到的,可以把整个account对象
发送到addAccount.jsp页面。可以获取到该对象的属性。例如:account.id;
删除功能:当点击删除时不是真正的物理删除,而是修改DB里数据的状态,这样便于
追溯性的维护数据。当点击后修改数据的状态及改行数据的颜色即可;
修改颜色:需要动态的加载与按钮的动态显示相同的思想。即页面加载后立即加载
这些数据。需要遍历tr
//给已删除的数据添加颜色便于区分
for(var i=1;i<tr.length;i++){//第0行是标题行,故从第二行开始
if(spanArray[i-1].innerHTML=="已删除"){
tr[i].className = "delete_tr_color";
}
}
修改功能:
---------------------------------------------------------------------
$.post();触发流程:
$(function(){
$("#recommenderIdcardNo").blur(function(){
var recommenderIdcardNo = $(this).val();
//alert("推荐人88:"+recommenderIdcardNo);
$.post("getRecommIdcardNo",//url必选
{"recommenderIdcardNo":recommenderIdcardNo});//可选
});
});
modifyAccount.js文件通过$().blur();获取到身份证号码后,发送到
getRecommIdcardNo的action,该action获取到这个recommenderIdcardNo
值,然后查询它对应的推荐人的ID号码。
com.netctoss.account.action.SaveModifyAccountAction"
实践证明:使用触发的方式不可行,因为必须触发了才能提交给action。假设
客户不修改,就不需要触发,那么action就没有办法获取到身份证号码。但是
触发的分析思路是正确的。解决客户不修改的情况下,提交保存时也能获取到
身份证号码的方式是直接把idcardNo作为输出属性。即下面的解决方法:
---------------------------------------------------------------------
如何获取到推荐人的身份证号码?
因为查询每一条数据时,只能查询到该条数据的推荐人的ID号(recommenderId)
但是修改页面要显示推荐人的身份证号码。解决方法:
获取到推荐人的ID之后,查询推荐人的身份证号码。
//需要判断recommenderId是否为空,为空就不能调用方法
//查找,否则将报错.注意recommenderId空值为0不是null.
if(recommenderId==0){
idcardNo = null;
}else{
//把查询到的RecommenderId作为参数传入去查询推荐人的信息
recommAnt = dao.findByRecommenderId(recommenderId);
//直接获取到推荐人的身份证号码
idcardNo = recommAnt.getIdcardNo();
}
另外:idcardNo可以直接作为输出属性,直接输出到页面,这样也方便客户修改
好之后提交给保存的action(saveModifyAccountAction).
解决没有推荐人也能保存修改的问题。(即不用填写推荐人的身份证号码)
//因为null与任何值比较都不成立,故可以判断它的长度
if(idcardNo.length()==0){
//不存在推荐人:身份证号码为空,保存时直接设置recommenderId为null,所以
//就不用传递推荐人的身份证号码了。同样是修改保存,但调用的方法不一样。
dao.saveUpdateAccount(ant);
}else{
//存在推荐人:有身份证号码,还需查找recommenderId,同样是修改保存,
//但调用的方法不一样。需要传递idcardNo这个参数
dao.saveModifyAccount(ant,idcardNo);
}
修改页面的各种验证:
jQuery的验证发送到action时,可以同时发送几个参数,例如
$.post(
"checkRepeat",//url地址action地址
{"id":id,"name",name,...},//可以同时发送几个参数
function(){});
密码验证时:第一次要验证第二次的,第二次也要验证第一次的,即双方互相
验证,避免写一致后,又多余添加。
显示account账户的详析信息:
推荐人的身份证号码:根据推荐人的ID号,查询推荐人的身份证号码。同时
需要判断推荐人是否含有推荐人ID,否则就不能调用查找的方法。
开通/暂停/删除时间:根据状态,查询各自的对应时间。使用的是回显的组件,
都是通过ID去查询该条数据,根据ID查询到状态,输出相应的时间。状态为开通---
输出createDate,状态为暂停---输出pauseDate,状态为删除---输出closeDate.
特别注意:从jQuery异步发送数据到action的,不需要配置struts.xml文件,相反
如果从action返回到jQuery文件的,就需要配置struts.xml文件。例如:
$.post(
"checkRepeat",//url地址action地址
{"name":name},//发送数据到action的,不需要配置struts.xml文件
function(data){
var repeat = data;//action至jQuery需要配置struts.xml文件
if(repeat){
$("#costNameMsg").text("用户名不可重复");
}else{
$("#costNameMsg").text("用户名有效");
}
});
反之,action至jQuery需要配置struts.xml文件
<action name="checkRepeat" class="com.netctoss.cost.action.CheckRepeatAction">
<result name="success" type="json">
<param name="root">repeat</param>
</result>
</action>
注意的问题:
业务逻辑上可能没有问题,容易出现问题的地方是在判断条件上。判断条件不
正确就不会输出相应的字段。
当页面真正的去提交到action的时候才会去调用action,如果是页面加载的时候触
发函数调用action,那么即使输出了相关的字段,但是却不能显示到界面,例如:
accountwindow.onload();。
idcardNo能够正常的显示在界面,是因为当点击了“修改”按钮,触发了:
window.location.href="toModifyAccount.action?id="+id;
lastOperationTime能够正常显示在详细信息(detailAccount.jsp)页面是因为,
当单击了名字的链接后,触发了以下函数:
function detailAccount(id){
$.post(
"queryAccount",
{"id":id});
window.location.href="queryAccount.action?id="+id;
}
上次登录时间:
获取当前系统时间为上次登录时间,当点击“返回”时,把当前系统时间更新到
上次登录时间。当单击“名字”时获取上次的登录时间。同样是需要借助
modifyAccountAction这个类。因为都是从ID去查询获取的。
lastOperationTime最后操作的时间(操作开通、暂停、删除的时间),
lastLoginTime上次登录的时间(登录查看,但不一定操作),如果登录了操作
了那两个的时间是一样的。
上次登录IP地址:(通过InetAddress获取)
自动获取当前计算机系统的IP地址。当点击“返回”时自动把IP地址更新到
lastLoginIp,当点击“名字”时,查询该IP地址作显示。同样是需要借助
modifyAccountAction这个类,为了不用写重复的组件!!
通过以上两次点击“返回按钮”可以得出:单击一个按钮可以同时调用两个action
实施说明:
阶段四:围绕业务账号模块的开发
策略(为了节省时间,提高开发效率,综合运用开发好的模块)
1、service业务账号的分页。
创建ServiceVO属性类:业务账号ServiceVO,用于封装关联查询后的结果,相当于
一个伪的实体类,仅仅是在查询时使用,持久化时还是用原来的Service。
因为ServiceVO的分页SQL语句比较长,所以使用了可变换的StringBuffer来添加;
StringBuffer sb = new StringBuffer();
输出SQL语句,用于检查语句是否完整正确。
System.out.println(sb.toString());
sql语句:
select * from(select s.*,a.IDCARD_NO,a.REAL_NAME,c.NAME,
c.DESCR,rownum r from SERVICE s inner join ACCOUNT a on
s.ACCOUNT_ID=a.ID inner join COST c on s.COST_ID = c.id
where 1=1
and s.OS_USERNAME = ?
and s.UNIX_HOST = ?
and a.IDCARD_NO = ?
and s.STATUS = ?
and rownum < ?)where r > ?
本次有两个错误:
select * from(select----缺少了select,报错为表名错误!!
遍历问号preps.setObject(i+1, params.get(i));把i+1写成了1+1,
报错为:java.sql.SQLException: 索引中丢失 IN 或 OUT 参数:: 1
遍历问号参数:因为params的添加到list时是按顺序添加的,所以取时也是按顺序取,
因为问号是从1开始的,故+1,对应参数的问号。
for(int i=0;i<params.size();i++){
preps.setObject(i+1, params.get(i));
}
业务账号分页的结果集采用方法调用的方式:
while(rst.next()){
//使用ServiceVO去接收,结果集查询出来的数据。
//createServiceVO(rst)调用方法
ServiceVO s = createServiceVO(rst);
serlist.add(s);//添加到集合里
}
封装成集合的形式:
List<ServiceVO> serlist = new ArrayList<ServiceVO>();
注意导入支持OGNL标签的声明:
<%@taglib uri="/struts-tags" prefix="s"%>
注意:没有使用totalPage之前也能输出页面的数据,因为已经给page、pageSize
赋了初值,是根据rownum来显示数据的。输出了service的集合。
总的页数SQL:
select count(*)from SERVICE s inner join ACCOUNT a
on s.ACCOUNT_ID=a.ID inner join COST c on s.COST_ID= c.id
where 1=1
分页总页数时遇到的错误:
select count(*)from SERVICE s inner join ACCOUNT a on
s.ACCOUNT_ID=a.ID inner join COST c on s.COST_ID = c.id
where 1=1 and s.STATUS = ? )
java.sql.SQLSyntaxErrorException: ORA-00933: SQL 命令未正确结束
发生以上错误“命令未正确结束”主要是因为SQL语句最后处多了一边括号!
因为检查好SQL语句是否正确书写!!
2、新增业务账户:
注意OS_USERNAME业务账户的约束条件是所在的服务器上的OS_USERNAME是唯一的,
不要求所在的表中唯一。约束条件为:
OS_USERNAME VARCHAR2(8) NOT NULL
CONSTRAINT SERVICE_UNIXHOST_OSUSERNAME_UK
UNIQUE(UNIX_HOST,OS_USERNAME),
从JSP页面发送过来的服务器需要转换,因为从JSP发送过来的是数字1、2、3、4
转换成对应的服务器IP地址:
if(service.getUnixHost().equals("1")){
preps.setString(2, "192.168.0.20");
}else if(service.getUnixHost().equals("2")){
preps.setString(2, "192.168.0.23");
}else if(service.getUnixHost().equals("3")){
preps.setString(2, "192.168.0.26");
}else if(service.getUnixHost().equals("4")){
preps.setString(2, "192.168.0.200");
}
知识点:新增时有的参数是从JSP页面发送给action,有的直接从实现类里获取到。
3、状态维护---删除(不是真正的物理删除,而是改变数据的状态)
把status更改为2
状态:
0-开通(创建时即默认开通)
1-暂停(手动操作)
2-删除(手动操作)
4、状态维护---暂停
把status更改为1,时记录PAUSE_DATE = SYSDATE;
5、状态维护---开通
把status更改为0,同时删除PAUSE_DATE = NULL;
6、修改页面:月初实现触发新的资费类型方法如下:
(1)创建一张新的数据库表service_modify_cost,改变含有的字段有:
id 采用外键的方式references service(id)
old_cost_id,
new_cost_id,
unix_host,
os_username,
modify_date,
action_date;
create table service_modify_cost(
//不能使用ID应用于最后update更新的条件插入,因为同一个人可以开多个业务账号
id number(10) constraint modify_service_id_fk
references service(id) not null,
old_cost_id number(10),
new_cost_id number(10),
unix_host varchar2(15),//应用于最后update更新的条件插入,在该服务器上os账号唯一
os_username varchar2(8),//应用于最后update更新的条件插入,在该服务器上os账号唯一
modify_date date,
action_date date);
);
(2)当点击修改时,服务器需要保存以上7个字段的值,它们获取的方法:
id:service.getId();//修改页面的栈上回显时,提交还是service
old_cost_id:通过id查询该条数据的全部信息,Service ser = new Service();
然后ser.getCostId();
new_cost_id:service.getCostId();//修改页面的栈上回显时,提交还是service
unix_host:修改页面的栈上回显时,提交还是service,故:service.getUnixHost();
os_username:修改页面的栈上回显时,提交还是service,故:service.getOsUserName();
modify_date:在显示类直接获取当前的系统时间;
action_date:暂时为null插入表格。
(3) 点击登录界面的"登录"时,就会调用查询所有的service_modify_cost所有信息,
获取五个字段:
old_cost_id://主要应用于比较cost_id,如果相同则不执行update
new_cost_id://主要应用于比较cost_id,如果相同则不执行update
unix_host://主要应用于最后update更新的条件插入
os_username://主要应用于最后update更新的条件插入
modify_date://主要应用于比较月份,决定是否执行update
实现类的策略:
查询该表的所有信息。获取当前系统的时间,把月份截取出来。
获取修改时间,截取月份。rst.next(),也是一个一个的获取数据的,
并不是一下子全部获取完成具有遍历结果集的作用,所有包含在
while(rst.next()){}里面的都是每运行一次执行一遍,直到执行完毕。
可以不使用集合,也可以顺序添加参数,因为它都是每执行一遍获取一
个参数的,只要该条数据的月份比较大于等于1后就会立刻赋值修改的
SQL问号。比较月份,如果月份大于1,则修改service表的cost_id。
可以不使用集合,之前使用集合,输出listnewcostid.size(),只输出
1,是因为while(rst.next()){}每遍历一遍,就执行了listnewcostid.size()
所以都是输出一的。
(4)把调用更新的触发点放在登录界面,当点击"登录"时,服务器需要
做两件事情:
/*2、向服务器提交登录的验证表单*/
$(function(){
$("#login").click(function(){
//第一件:提交登录验证的表单
document.forms[0].submit();
//第二件:触发service表的修改资费,下月初触发。
$.post("updateServiceCost");//调用该action,action调用DAO
});
});
为什么要放在这里?
因为这里时机是最好的,当点击登录界面的登录时就会去调用更新的action,
如果符合条件了,就会执行更新service表里的cost_id,再未点击“service”
分页的情况下,已经及时的把更新的结果同步到了DB里面,当点击“service”
的分页时,就已经可以显示“实现下月触发新的资费标准”的结果,即实现在
用户不知道的情况下,服务器已经在后台实现了触发,这就体现了软件优雅
的一面。
下次改进的地方:因为这次比较的只是月份而已,如果是从12月跳转到明年的1月
时就无法比较,建议改进成比较年份、月份同时比较。(已改进)
(5)如果同一条数据在同一个月内修改数次,那最后一次修改的那条数据有效,即
以最后修改的那条数据为准,这个是系统自动的。无需进行任何编写。
(6)删除、暂停状态的数据不可修改。如果新的资费ID与旧的资费ID相同,将不保
存结果到DB因为修改成与原来的,那修改就没有意义了,故不用保存,既然没有
保存数据到service_modify_cost,那即使跳转到下个月,也不会修改成功。
额外补充知识:
window.location.href="modifyService.action?id="+id;
理解为:通过js文件向modifyService.action发送一个ID号,并且调用这个
action.
6、详细信息页面
SQL:
select * from (select s.*,a.IDCARD_NO,a.REAL_NAME,
c.NAME,c.DESCR,rownum r from SERVICE s inner join
ACCOUNT a on s.ACCOUNT_ID = a.ID inner join COST c
on s.COST_ID = c.ID where s.id=?)
今天在显示详细信息折腾了一天了,主要是alert(id);不能正常弹出,且整个
findService.js的其他的操作按钮也是不能正常弹出alert(id);最后解决办法:
把整个findService.js的js文件删除了,重新新建一个,然后把内容复制过来,
即可解决问题。
回显数据时,action可以以对象的形式输出,也可以以集合的形式输出。但是在
JSP页面显示时有区别:以下使用的是UI标签
对象的形式输出:service是对象的变量<s:textfield name="service.id"/>
集合的形式输出:service是集合的变量,需要遍历
<s:iterator value="service">
<s:property value="id"/>
</s:iterator>
分析以下两个的区别:
①window.location.href="detailService.action?id="+id;
② $.post(
"detailService",
{"id":id});
相同点:都是把id发送给detailService这个action
不同点: ①不仅发送了id,同时还调用这个detailService。而②仅仅是把id
发送给了detailService,但是却没有任何调用。
实施说明:
阶段五:围绕管理员模块的开发
管理员的分页显示,遇到的
问题一:不能正常显示页面的数据
<s:iterator value="admin">
<tr>
<td><s:textfield name="id"/></td>
</tr>
</s:iterator>
原因:使用了错误的使用了UI标签,应该使用OGNL表达式;
<s:iterator value="admin">
<tr>
<td><s:property value="id"/></td>
</tr>
</s:iterator>
问题二:SQL语句,sql语句未正确结束
select * from ADMIN(select a.*,rownum r from ADMIN a
where 1=1 and rownum < 6) where r > 0;
原因:select * from ADMIN多了一个ADMIN,应该直接接括号
问题三:带条件搜索时,出现下面的分页页码都是2页。
原因:select count(*) from admin。只写了这样的SQL语句;故
不管是否带条件查询,始终都是根据admin表的rownum来统计条数的;
正确的做法:需要带上条件,
sb.append("select count(*) from admin where 1 = 1");
if(module!=null && !module.equals("-1")){
sb.append("and module = ? ");
param.add(module);
}
if(role!=null && role.length()>0){
sb.append("and role = ?");
param.add(role);
}
where 1 = 1永远不能缺少了这个条件,否则带条件查询时,将不会显示
分页的条件。
注意:带条件的搜索,直接是提交的按钮,无需另外的js文件
<input type="submit" value="搜索"
οnclick="toPage(<s:property value="1"/>);"
class="btn_search"/>
角色表只需要一个表
role_info角色信息表:记录角色信息,字段有如下
ID---主键(序列名称:seq_on_role_info)
NAME---角色名称
PRIVILEGE_ID--权限ID,只保存权限ID即可
create table ROLE_PRIVILEGE(
ID NUMBER(11) CONSTRAINT ROLE_PRIVILEGE_ID_PK PRIMARY KEY,
NAME VARCHAR2(20) not null,
PRIVILEGE_ID VARCHAR2 not null
);
create sequence seq_on_rp
increment by 1
start with 1000
nomaxvalue
nocycle
nocache;
PRIVILEGE_ID---角色权限的ID
保存ID即可,具体的权限描述不用保存,
仅保存权限相对应的ID(例如:1234567或1、2、3),
action显示根据ID进行转换显示
ROLE_id 1000 --- PRIVILEGE_ID 1234567 (超级管理员:权限全部拥有)
ROLE_id 1001 --- PRIVILEGE_ID 1 (角色管理员:角色管理)
ROLE_id 1002 --- PRIVILEGE_ID 2 (管理员:管理员)
ROLE_id 1003 --- PRIVILEGE_ID 3 (资费管理员:资费管理)
ROLE_id 1004 --- PRIVILEGE_ID 4 (账务账号管理员:账务账号)
ROLE_id 1005 --- PRIVILEGE_ID 5 (业务账号管理员:业务账号)
ROLE_id 1006 --- PRIVILEGE_ID 6 (账单管理员:账单)
ROLE_id 1007 --- PRIVILEGE_ID 7 (报表管理员:报表)
注意:JSP页面中的<s:checkboxlist
name="" listKey="" listValue=""></s:checkboxlist>
属性一个都不能少;否则将不能正常显示;
验证权限信息只能在action里验证,虽然权限ID重复的情况下,
不能保存新增,但是页面有任何提示,这需要改进的地方。
主要是因为不知道如何用jQuery获取到<s:checkboxlist>
中listKey的值,所以没有在这里验证。但在action中验证了
如若是repeat=false,证明权限ID没有重复,可以保存,如果等于
true证明已经重复了,不能保存
admin管理员基本情况表:(已建成)
ID---管理员的ID
ROLE_ID角色ID,使用外键
管理员页面的分页使用了模糊查找。
把admin与role_privilege表自连起来,组成一张大表,把符合搜
索条件的数据留下。查找出来的数据,需要的显示在界面。查询出来
的数据不一定要显示。
第一个搜索条件:权限的搜索,含有符合的权限都会被找出。
所有使用了通配符查找的方法。
select * from (select a.*,r.* from admin a
inner join role_privilege r on
a.role = r.privilege_id where 1 = 1
and r.privilege_id like '%2%'
and r.role_name = '管理员'
);
复习oracle知识:
%(百分号通配符:百分比符号用来指定零个或多个通配符字符,
而下划线字符指定一个通配符字符。)
select first_name from employees where first_name like 'A%';
将会查找到以A开头的名字,若果是%A%,表示只要含有A的都出现。
下面两个是等价的,因为没有使用通配符,相当于精确查找
where last_name like 'King';
where last_name = 'King';
_下划线通配符:下划线字符指定一个通配符字符。
where last_name like 'K_ng';
中间的是未知的。但是已经固定了剩下三个的。
注意:几乎80%的错误都是出现在SQL语句上。所以要先在数据库中运行了
之后再写入Java代码。
在action的表达方式:
admin = dao.findByCondition("%"+privilegeId+"%", roleName,
page, pageSize);
totalPage = dao.totalPage("%"+privilegeId+"%", roleName,
pageSize);
因为使用了模糊查找,即通配符查找,所以要带上%,这样只要含有某个
字符,就会带出所有的。
实现类的SQL语句:
if(!privilegeId.equals("%null%") &&
!privilegeId.equals("%-1%")){
sb.append("and p.privilege_id like ? ");
param.add(privilegeId);
}
因为action已经是把这个参数"%"+privilegeId+"%"传过去,例如%2%,
所以判断条件也要跟着变化。"%null%"、"%-1%"
注意:虽然拼接成了一个大大的表,但是如果a表没有这个字段,写成
a.privilege_id 这样是错误的。要写成a.role
第二个搜索条件是精确的查找方法。
阶段一:登录模块模块的开发
没有结合拦截器进行开发,仅仅使用的是Struts2以下围绕登录模块进行:
技术实现:java+struts2+jdbc+jsp+jquery/ajax
表示层:jsp+jquery/ajax
业务层:action充当业务层
控制层:Struts2
数据层:jdbc + dao
基础工作:
(1)导入相关的Struts2中使用到的jar包。特别注意不能导错。
json导入到lib下;jQuery导入到新建一个js文件,然后粘贴进去;
其他都要正常的导入。
(2)配置前端控制器:filter充当,在web.xml中配置
(3)导入样式.css文件,在JSP页面中引入。
配置文件:
(4)配置struts.xml文件,本项目需要引入的方式:
struts.xml:主要起到引入全部配置文件的作用。例如:
<struts>
<!-- 引入登录模块配置信息 -->
<include file="struts-login.xml"></include>
</struts>
比较用户名:
(5)创建Admin实体类,与DB数据库字段相呼应。
(6)编写登录的接口、及实现类(ILoginDAO、LoginDAOImpl)
(7)编写登录的Action类(LoginAction),具体的调用DAO实现类
(通过工厂的方法去调用),比较然后得到数据,判断。
(8)配置Struts.xml文件.登录成功则进首页,失败则停留在登录页
输出验证码:
(补充:只要是数据为空,即admin==null,return "fail"
其他抛异常的时候return "error",配置文件的result name="fail")
(9)编写生成验证码的工具类,ImageUtil类(普通类),生成后保存到map里面return map;。
(10)编写输出验证码的属性类,ImageCodeAction类(Action类),输出到JSP页面。
(11)编写登录页面的action类,直接在LoginAction(Action类)添加比较验证码的方法,跟
比较用户名密码都是在同一个execute()方法里。
补充总结:
第一:若一个类的方法使用的是纯对象类型的方法,
即public AdminInfo findByCodeAndPwd(String adminCode,String passWord)
也就是项目中MyNetCtoss_Struts2中的LoginAction,若账号密码不正确,那么
数据是查询不到的,这样输出下面的admin.getTelePhone()的话就会引发500报错!!
//System.out.println(admin.getTelePhone());
但是这样输出:System.out.println("admin的数据是: "+admin);
账号密码不正确会输出null,不会报错500.
第二:若一个类的方法使用的是不是纯对象类型的方法,即public List<AdminInfo>
findByCodeAndPwd(String adminCode,String passWord),如果账号密码不正确,那
调用时:List<AdminInfo> admin = dao.findByCodeAndPwd(adminCode, passWord);
输出:System.out.println(admin);//即使账户密码不正确它永远也不会输出null,
因为它是集合的类型形式,即使查询不到数据,输出的是“[]”,我们知道null与任何数据
比较都是false,永远不成立的。所以下面的判断:永远不可能return "fail";下面的
比较判断永远都是return "success";
if(admin==null){
return "fail";
}else{
return "success";
}
第三:总之使用一个方法时一定要弄明白它返回的数据类型是什么,是返回我们需要的
数据还是返回null,还是空,null不等于空,空也不等于null,二者比较永远不成立。
第一阶段后期任务:
1、分别使用ajax/jquery作登录检查
2、使用拦截器作登录首页
实施说明:
阶段二:资费界面模块的开发
以下围绕资费界面进行:
技术实现:java+struts2+jdbc+jsp+jquery/ajax
表示层:jsp+jquery/ajax
业务层:action充当业务层
控制层:Struts2
数据层:jdbc + dao
二、资费模块开发(分页、增加、启用、修改、删除)
分页显示:
(12)创建资费表cost表的实体类
(13)创建资费模块分页显示的接口并编写实现类,同时需要测试方法
能否返回所需要的数据。使用工厂的方法。
注意dao.properties文件的配置name:接口名 value:实现类的具体包名类名
ICostDAO dao = (ICostDAO)Factory.getInstance("ICostDAO");
是接口,不是实现类。
(14)创建资费模块的Action类---FindCostPageAction(含有execute方法)
注意:cost = dao.findByPage(page, pageSize);//这个数据查询到之后是要在findCost.jsp里显示的
totalPage = dao.totalPage(pageSize);
//cost前面无需List<Cost>,因为变量定义的部分已经声明了。
(15)注意JSP页面的接收数据:
<s:iterator value="cost">要与上面的变量名一致,方能接收到数据。
(16)配置struts.xml文件,本项目需要引入的方式:
struts.xml:主要起到引入全部配置文件的作用。例如:
<struts>
<!-- 引入登录模块配置信息 -->
<include file="struts-login.xml"></include>
<!--引入资费模块配置信息 -->
<include file="struts-cost.xml"></include>
</struts>
(17)登录首页后,为了能成功的链接到各个模块。配置文件必须按下面的配置来描述:
第一种解决办法:
只需要一步即可:在package的namespace="/相同的包名空间",即指定相同的包名空间。
其他均不变,注意包名一定不能相同。这样就可以解决!!
第二种解决办法:
在struts-main.xml这个公共包package中定义了<package name="netctoss"
namespace="netInter" extends="json-default">,然其让其他的配置文件的package包
extends="netctoss"继承了netctoss(其他包必须省略namespace,因为继承了netctoss中的
namespace),目的为了能够使登录成功进入首页后,能与每个模块页面之间相互链接。前提
就是保证namespace相同。所以继承是一个很好的解决办法。
(18)FindCostPageAction使用的拦截器详析:
①目前只有FindCostPageAction引入了拦截器栈,当登录成功后,即session上绑定有了admin
的数据,资费的分页FindCostPageAction才会被调用(调用Action之前首先会调用拦截器的方法
如果拦截器的方法不被调用,那对应的action,即分页的action不会被调用,这就是拦截器的
优势所在)
②本项目的登录暂时与拦截器无关,条件符合即可登录首页,即账号密码验证码正确即可登录。
③因为FindCostPageAction中调用了拦截器,在这个分页的action被调用之前,首先会调用拦
截器中的方法。拦截器方法中进行判断,若admin==null,(admin数据绑定在session上,绑定的
类是登录的action--LoginAction。在LoginInterceptor拦截器中若获取不到数据,证明登录不
成功那么拦截器的方法中将不会调用FindCostPageAction,而是return "login",将返回到name
为login的result,即状态还是停留在登录的页面。这就体现了拦截器的原理:动态的调用action
在action方法被调用之前或之后,在拦截器的方法中加入某些操作,本项目是在之前调用)
④调用拦截器的action文件配置Struts.xml中的<iterator-ref name=""></iterator-ref>下的
result,这个result的name=“success”是来自FindCostPageAction中的success,并不是来自拦截器
的。拦截器中的return可以返回任何字符串,不是success都可以。
⑤在LoginAction中只要把下面的session注释掉,那session上就没有admin的数据了。虽然成功
登录了首页,由于上面的拦截器那里加入了操作if(admin==null){数据为空,则返回到result中
name为login的对应的result,return "login";即表达的思想就是:连资费的数据都不存在,何
谈分页。故重定向到登录的界面。
⑥跳转到分页页面的Action.在调用findCostPageAction之前首先会调用拦截器栈,这个是拦截
器的思想,检查admin是否为null,若为null则继续停留在登录的界面,防止没有成功登录首页
的前提下,直接访问分页的页面是不允许的,也不符合软件的逻辑思维。总之,就是链接分页之
前首先得成功登录首页。
(19)编写拦截器类,正式引入拦截器的概念:
主要应用在分页检查与登录检查。若admin==null全部返回登录界面。分页检查:说明既然数据
不存在,何谈分页。登录检查:首先必须成功登录首页方能访问其他的数据,直接访问分页数据
不符合软件的设计思想,不允许非法登录。
SomeInterceptor implements Interceptor
(20)做资费模块的增加页面:
资费增加界面分析:
资费类型:(4-基本时长;5-基本费用;6-单位费用)
type==1时,为包月---只有基本费用。基本时长、单位费用均为null,同时要显示灰色;
type==2时,为套餐---全部都有;
type==3时,为计时---只有单位费用。基本时长、基本费用均为null,同时要显示灰色;
需要获取相应的操作节点:
var inputArray = document.getElementById("main").getElementByTagName("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 = "";
inputArray[6].disabled = "disabled"//不可输入任何值
但是这个 inputArray[6].readonly = “readonly”不成立。
放在input里才会成立。只读模式,只能显示,不可编辑
}
精髓分析:
readonly: 这里意为只读的意思,即不可输入(为null的意思),只能看,同时显示
灰色,只要是readonly的布尔值为true的,都要加上readonly的类选择器。
每一个属性都需要判断readonly的Boolean值为true或false。若为true,DB里的值为null,
意为只读,value属性为空,加上readyonly的类选择器(显示灰色的CSS)。若为false,意为该
资费类型需要输入相应的值,DB里有值,类选择器为“width100”;
DOM里的input、div等遍历的时候是从0开始的。
注意:加上readonly类选择器时,一定要在前面加上空格,否则无效。即:
inputArray[4].className += " readonly";
css:
background:大的背景(整个版面的);background-color:小的背景(input的输入框)
分类选择器:元素选择器与类选择器的组合。
CSS应该有两个CSS表:一个定义颜色(背景图片属于颜色)global_color.css,
另外一个定义大小形状global.css。
CSS相关属性分析:
/*height: 28px;*/
line-height: 28px;//该DIV里的文字的每一行所占的行的高度。不是该块级的高度;
height:80px;//改变的是div块级纵向的高度;
width:80px;//改变的是div块级横向的宽度;
float: right;//文字不能浮动;div之间的内容向右浮动。不是换行。若块级ABC全部浮动,块级将并排在一行上了;
假设:块级A左浮动,块级B不浮动,块级C也不浮动。不浮动的块级B将占据页面的最左边的位置,因该位置已有块级A左浮动,
那将遮住不浮动的块级B,即不显示没有浮动的块级B。因为块级C也不浮动,理应占据页面的最左边的位置,
但是现在已经被块级B占据了,那只能是排在块级B的下面,所以可以显示,若此时块级B左浮动,那块级C将
占据页面最左边的位置,即不显示。
若不浮动的块级下还有不浮动的块级,因为之前已经有块级占据页面的最左边的位置,那本
overflow: hidden;//属性可以保证div的高度或宽度不变。div里添加的东西再多,高度或宽度也不变。超出的部分隐藏
text-align: left;//块级文字的对齐方式。有三个值:center,left,right
padding-left:5px;//文字距离左边的框体5个像素。实际上了框体横向加了5个像素。即80+5=85px;
//padding-right:5px;一个道理。会改变横向的宽度,即width的宽度。
font-size:60pt;//框体里文字的字体大小;
font-weight: bold;//字体加粗;
color:red;//框体里文字的颜色;
border:5px solid #8ac1db;//div的边框为5个像素,上下左右各是5,改变框体的四边的像素。
min-height:100px; //设置了div高度的最小值。若max-height缺省,即为自适应内容的高度。
max-height:200px;//设置了div高度的最大值。若内容超过将溢出该div框体。
overflow-x:hidden;//横坐标隐藏;
overflow-y:auto;//纵坐标自动;感觉这两个x\y没太大的意义;
background:url(images/login_bg.jpg);//背景图片,使用PS处理好,默认是repeat,图片将
会重复,如果不想使用重复应该定义no-repeat;
制作背景画面:在昵图网下载psd页面后,PS打开。去底:选中后按删除。给背景颜色上
黑色,然后再选中大的背景图层,使用魔术棒选中界面,按del键,再按取消键。然后选中
大的背景图层,按Ctrl+M变换成白色,然后使用修补工具把要用的雪花圈起来,然后按
Ctrl+J复制,然后就可以拖动到背景的页面作修饰。
登录页面CSS解析:
1、大的背景图片:
body.login{
background-color: #00629f;//整个版面的颜色,补丁的颜色。
设置好背景图片之后,利用这里来填充其他没有被背景图片覆盖过的
空白颜色。要求这个颜色要与背景图片的颜色一致,方能达到融合的效果!!!
background-image: url(images/login_bg.jpg);//背景图片
background-position: center top;//图片的位置,居中
background-repeat: no-repeat;//图片是否重复,这里选择不重复
}
按照以上的方法搞定大的背景图。
2、登录框:
①导入登录框图片时:div.login_box{background:url(images/login_box.png) no-repeat;}
<div class="login_box">xxxxxx</div>,必须保证这个div必须存在文字,才能显示出登录框
的图片。
②设置登录框的宽度、高度、字体大小、字体粗细、内外边距
div.login_box{
background:url(image/login_box.jpg) no-repeat;//输入框的背景图片
width:450px;//输入框的宽度
height:263px;//输入框的高度
font-size:12pt;//输入框的文字大小
font-weight:bold;//输入框的文字粗细
margin:170px auto;//输入框的外边距
padding-top:100px;//输入框的内边距距离顶部的距离
padding-left:5px;//输入框的内边距距离左边的距离
}
③设置input的属性:
input{
height:25px;//输入框的高度
line-height:27px;//输入框的行高,行高太高,光标会看不见。
padding-left:20px;//光标离输入框左边的距离。
}
④输入框前的文字:
td.login_info{
text-align:right;//文字在单元格里靠右对齐
width:150px;//单元格的宽度
}
⑤
3、登录框里字体颜色:
div.login_box table tr td{
color:#FFF;//白色
}
调整框体的大小:
td.login_info
{
text-align: right;//文字在单元格里对齐的位置,向右对齐
width: 150px;//单元格的大小
padding-right: 5px;//文字距离元素的右边内距离为5个像素
}
页面的auto会使右边有滚动条:消除滚动条的方法是使用margin-left:平均横的长度
注意:输入框在myeclipse里的浏览器Mozilla不能显示光标在中间,但是IE却可以。
若在360浏览器中访问该页面可以正常显示光标在中间。
知识点:单选框radio如何把选中的值发送给服务器?
<div>
<input type="radio" name="fadFeeType" value="包月" id="monthly"/>
<label for="monthly">包月</label>
<input type="radio" name="fadFeeType" value="套餐" checked="checked" id="package"/>
<label for="package">套餐</label>
<input type="radio" name="fadFeeType" value="计时" id="BaseCost"/>
<label for="BaseCost">计时</label>
</div>
解决:只需要在input里面添加value属性,并且指定value的值,而name="fadFeeType"
已经绑定了value的值了;既然是单选,三个只能选其中一个,所以name必须相同;name
的值与action中的属性一致,即private String fadFeeType,这样就可以获取到对应单选
框发送给服务器的数据。
总之:input输入框的value属性是用来向服务器提交参数的。如果指定了value的值。那点
submit按钮后,页面即可向服务器提交value的值,如果没有指定value的值,页面将向服
务器提交用户输入的值。
如下的是资费增加页面的JavaScript文件解析:
页面的提示信息:
1、当用户没有任何操作时,是正常的显示“要求的输入格式信息”
2、当用户没有按要求输入信息时,才会加入错误的样式,例如字体显示红色的样式
3、当选择包月时,基本时长、单位费用的输入框是灰色的,即不可输入状态,需要添加
属性disabled = true;或disabled = "disabled"都可以。其他的资费类型同理。只要判断了
一个资费类型input输入框的disabled属性,那么剩下所有的资费类型的input输入框,都
要判断,否则其他的没有判断的输入框,样式将会受影响。
4、当选择资费类型时,例如选择了“包月”,只有“基本费用”的输入框可以输入数值,当鼠
标光标移到该输入框但没有输入任何值,但光标点击其他的地方,立即出现错误提示信息。
这时如果选择另外一种资费类型,但是刚才的“基本费用”提示信息还存在。这样就不符合
逻辑,应当是:如果鼠标选择其他的资费类型时,原先的提示信息应当消除,毕竟当我选
择其他的资费类型时,都没有任何操作,就有错误的提示信息,这样就不符合软件设计思
维。解决办法如下:
在每个资费类型的函数里的首行清除之前类型留下的样式以及留下的值;
if (type == 1) {
$("#baseCost").val("");//清除之前输入框留下的值
$("#baseCostMsg").removeClass("error_msg");//清除样式
$("#baseCostMsg").text("*0-99999.99之间的数值");//添加原始的信息
$("#baseDuration").val("");
$("#baseDurationMsg").removeClass("error_msg");
$("#baseDurationMsg").text("*1-600之间的整数");
$("#unitCost").val("");
$("#unitCostMsg").removeClass("error_msg");
$("#unitCostMsg").text("*0-99999.99之间的数值");
三个都是需要这样添加。
5、需要判断的资费类型对应的输入框提示信息:
包月:基本费用
套餐:基本时长、基本费用、单位费用
计时:单位费用
①包月:基本费用的判断:
基本费用输入框的jQuery校验方法解析:
$(function(){
$("#baseCost").blur(function(){
var bc = $(this).val();---获取用户输入的值
var Regeg_name = /^(0|[1-9]{1,5}(\.[\d]{1,2})?)$/;--正则表达式
var bc_info = document.getElementById("baseCostMsg");--定位到提示信息位置
if(bc.match(Regeg_name)==null){--比较方式,固定的格式
$("#baseCostMsg").text("*请输入0-99999.99之间的数值!");
$("#baseCostMsg").addClass("error_msg");
}else{
$("#baseCostMsg").text("*输入基本费用数值有效");
$("#baseCostMsg").removeClass("error_msg");
}
});
});
使用jQuery的经典开场白:$("#baseCost")使用单引号或双引号都可以识别
$(function(){
$("#baseCost").blur(function(){
具体的判断方法});
});
②套餐:基本时长、基本费用、单位费用
套餐的“基本费用”使用的jQuery与包月的“基本费用”是同一个输入框,
因此只要验证一次即可。
“基本时长”:
$(function(){
$("#baseDuration").blur(function(){
var bd = $(this).val();
var Regeg_str = /^([1-9]|[1-5][0-9]{1,2}|600)$/;
if(bd.match(Regeg_str)==null){
$("#baseDurationMsg").text("*请输入1-600之间的整数");
$("#baseDurationMsg").addClass("error_msg");
}else{
$("#baseDurationMsg").text("*基本时长数值有效");
$("#baseDurationMsg").removeClass("error_msg");
}
});
});
解析:其实三个输入框的校验方法都是一样的,jQuery的经典开场白,然后定位到要操作
的节点,获取到用户输入的值,写正则表达式,比较用户输入的值是否能通过正则表达式,
正则比较的固定格式:
var Regeg_str = /^([1-9]|[1-5][0-9]{1,2}|600)$/;输入1-600之间的整数
if(input_str.match(RegExp)){
//在这里编写比较后的要添加行为----这里是不通过的表现
}else{
//在这里编写比较后的要添加行为----这里是通过的表现
}
input_str:用户输入的值 RegExp:正则表达式
"单位费用":
var Regeg_str = /^(0|[1-9][\d]{0,4}(\.[\d]{1,2})?)$/;0-99999.99之间的数值
$(function(){
$("#unitCost").blur(function(){
var uc = $(this).val();
var Regeg_str = /^(0|[1-9][\d]{0,4}(\.[0-9]{1,2})?)$/;
if(uc.match(Regeg_str)==null){
$("#unitCostMsg").text("*请输入0-99999.99之间的数值");
$("#unitCostMsg").addClass("error_msg");
}else{
$("#unitCostMsg").text("*单位费用数值有效");
$("#unitCostMsg").removeClass("error_msg");
}
});
});
原理与其他三个的校验方法一致,不再赘述。
③校验资费说明:
$(function(){
$("#costExpress").blur(function(){
var ce = $(this).val();
var Regeg_str = /^([\u4E00-\u9FA5A-Za-z0-9_\/\.]{5,100})$/;
if(ce.match(Regeg_str)==null){
showExpressInfo(true);//调用函数的形式
window.setTimeout("showExpressInfo(false);",3000);//控制显示错误提示信息的时间
}else{
showExpressInfo(false);//没有错误的时候执行函数else的部分
}
});
});
//控制显示错误提示信息的时间
function showExpressInfo(type){
if(type){
$("#costExpressMsg").text("*按要求输入格式正确的资费说明描述");
$("#costExpressMsg").addClass("error_msg");
}else{
$("#costExpressMsg").text("*100长度的字母、数字、汉字和下划线的组合");
$("#costExpressMsg").removeClass("error_msg");
}
}
知识点:
在选择下一个资费类型时,必须要清除之前类型留下的值,样式等。
清除输入框的值:$("#baseCost").val("");
资费增加的页面中“资费类型”单选项不需要获取它的值,它主要的作用是控制“基本时长”、
“基本费用”、“单位费用”的输入框能否输入数据而已。数据库里的NAME是“增加页面”的资
费名称。而ID需要在DB里创建自动增长的序列(seq_on_cost),使用的时候需要改为:
seq_on_cost.nextval)。
资费列表的增加中input输入框中只要有一个输入值不符合要求,就不能提交到服务器。
必须全部符合要求了才能提交。判断条件如下:
在js开头处定义全局变量:
var nameFlag = false;
var baseDuration = false;
var baseCost = false;
var unitCost = false;
var descrCost = false;
然后在各个函数中判断,如果有那个不符合要求,那该值就为false。
最后在提交按钮的函数中再一次总的判断:
function showResult(){
//判断资费名是否重复 &!baseDuration&!baseCost&!unitCost&!descrCost
if(nameFlag==false||
baseDuration==false||
baseCost==false||
unitCost==false||
descrCost==false){//结果为true执行这里的代码块,然后return;
showResultDiv(true);
window.setTimeout("showResultDiv(false);", 3000);
return;
}else{
document.forms[0].submit();//提交第一表单
}
资费分页页面的操作部分(启用、修改、删除):
修改:
为什么点击分页页面的“修改”按钮可以将id发送给接收的action?解析如下:(此问题
已经总结并且发表到空间了)
为什么点击分页页面的“修改”按钮后却没有数据回显。已经单独做笔记!---不能在cost
前面加上类型Cost.
修改页面使用的是UI标签,自动生成HTML标记的,可以从浏览器的查看源代码中看出。
对应如下:
<s:textfield></s:textfield>-----<input type="text"/>实际上就是input
<s:radio></s:radio>----<input type="radio"/>实际上也就是input
切换资费类型,因为多了一个资费ID故inputArray[5]从第五开始
在增加的资费里没有资费ID,所以从inputArray[4]开始。也就是数从input
的序号,序号是从0开始的。
修改好后的保存实现类:当修改好后调用保存的实现类,之后不能finally{conn.close();};
因为如果关闭了就不能再次访问分页的页面,因为当我们修改好后保存到DB,然后再
跳转到分页的页面,这样才是符合软件的逻辑。
通过修改资费后有发现:
在jQuery的验证中,除了提交新增的保存(或修改保存)验证与资费说明验证外,
剩下的所有验证都是基于选择的“资费类型”里面,因为他们的验证项目不相同,例如
选择"包月"时,基本时长、单位费用就不需要验证,因为"包月"只有基本费用,只需要
验证基本费用与资费说明即可。所以:baseDuration、unitCost都是等于true。直接写:
baseDuration = true;unitCost = true;这样是为了能在最后保存的能顺利提交。只有
它们全部都是true时才能提交资料。没有的项目就不需要验证,但是又能保证最后的提
交结果不受影响,所以只能是不需要验证的项目直接赋值为true,其他的同理。赋值的
时候要注意不能是 var baseCost = true;这样写是错误的,不需要在前面加上var ,因
为它们都已经在最开始的时候声明了类型,如果还在这里添加类型,那就是错误的。如
果最后不显示:检查的方法是alert("unitCost:"+unitCost);看弹出的时true还是false。
删除:根据ID删除资费数据
<input type="button" value="删除" class="btn_delete"
οnclick="deleteFee(<s:property value="id"/>);"/>
当点击这个删除键之后,这个id就随着deleteFee这个函数一起触发到findCost.js这
脚本文件。
function deleteFee(id){
var r = window.confirm("您确定将资费ID为"+id+"的记录删除?");
if(r){//r为true时执行,false不执行
window.location.href="deleteCost.action?id="+id;
document.getElementById("operate_result_info").style.display = "block";
}
}
状态维护:(开通、暂停)
/*3、状态维护*/
需要自动识别状态,如果状态是“开通”,操作为“暂停”,
状态为“暂停”的,按钮为“开通”,首先使用立即加载函
数window.onload。把按钮封装成数组,把状态封装成数组,
遍历状态,找出他们的通项公式:[3*i+4];
//使用立即加载函数window.onload
window.onload = function(){
showStatus();//实际调用的函数
};
function showStatus(){
//把按钮封装成数组
var inputArray = document.getElementsByTagName("input");
//把状态封装成数组
var spanArray = document.getElementsByTagName("span");
//遍历状态
for(var i=0;i<spanArray.length;i++){
if(spanArray[i].innerHTML=="开通"){
//状态为“开通”的,按钮为“暂停”
inputArray[3*i+4].value="暂停";
}else if(spanArray[i].innerHTML=="暂停"){
//状态为“暂停”的,按钮为“开通”
inputArray[3*i+4].value="开通";
}
}
}
/*4、根据资费状态调用不同的函数,达到启用"开通"或"暂停"资费的目的*/
function startFee(id){
//定位到当前所单击的按钮,因为id是唯一的,所以可以获取到唯一的value
//即按钮的value也是唯一的,此时的id是一个值
var status = document.getElementById(id);
if(status.value=="开通"){
var r = window.confirm("您确定要开通ID号为"+id+"的资费套餐?");
if(r==true){
window.location.href="stopCost.action?id="+id;
document.getElementById("operate_result_info").style.display="block";
}else{
return false;
}
}else if(status.value=="暂停"){
var r = window.confirm("您确定要暂停ID号为"+id+"的资费套餐?");
if(r==true){
window.location.href="stopCost.action?id="+id;
document.getElementById("operate_result_info").style.display="block";
}else{
return false;
}
}
}
实施说明:
阶段三:围绕账务账号模块的开发
目前能互通链接的有:资费分页、资费新增、首页(toIndex)、账务分页、账务新增
补充知识点:
account.jsp页面的显示
<s:iterator value="account">
<tr>
<td><s:property value="id"/></td>
<td><a href="javascript:detailAccount(<s:property value="id"/>);"><s:property value="realName"/></a></td>
<td><s:property value="idcardNo"/></td>
<td><s:property value="loginName"/></td>
</tr>
</s:iterator>
注意是遍历的方式(OGNL表达式),显示的值value;
修改页面的回显:
<div class="input_info">
<s:textfield name="ant.id" id="id" cssClass="readonly" readonly="true"/>
<div class="validate_msg_long">* 提示:账务账号ID不可更改</div>
</div>
注意是直接输出的方式(UI标签):name直接与action的属性绑定的
1、声明接口:
分页显示的页面容量:page,pageSize
public List<Account> findByCondition(String idcardNo,String realName,
String loginName,String status,int page,int pageSize);
实现类实现接口,实现分页显示的功能。
分页的总的页数:totalPage
public int totalPage(String idcardNo,String realName,
String loginName,String status,int pageSize);
2、转向Action:
实现类需要的参数都是从action的输入属性获取到的,然后action负责把参数传
递给实现类,因为action需要调用实现类,而参数是从JSP页面端传递过来的,这就
需要一个客户提交的动作,提交给action。
在这个account的分页显示页面,可以带条件的查询分页,也可以不带条件的分页。
不带的情况下就是查找全部的账务页面。
在JSP页面这里比cost先进的地方是它是动态获取JSP页面的页码的,实现方式如下:
中间的页码:
<s:iterator begin="1" end="totalPage" var="p">
<s:if test="#p==page">
<a href="javascript:toPage(<s:property/>);" class="current_page"><s:property/>
</a></s:if>
<s:else><a href="javascript:toPage(<s:property/>);"><s:property/>
</a></s:else>
</s:iterator>
分析:这就是动态的把当前页面发送给action中的输入属性page,page如何传输过去?
这就需要一个提交的动作:
<a href="javascript:toPage(<s:property/>);" class="current_page"><s:property/>
</a>
当单击页码时,就会把当前页码发送给这个toPage(currPage);这个函数,这个函数
获取到当前的页码数,然后再把这个页码数字赋给page;实现是:
<input name="page" type="hidden" id="currPage"/>,然后函数toPage(currPage);有
提交的动作;name="page"是与action属性名绑定的,action获取到页码之后,就会根据
页码去查找该页面所要显示的数据,根据rownum的范围;
假如:page = 1;pageSize = 5;
上一页的按钮:
同样是需要把页码数发送给函数toPage();函数收到之后,提交,action返回数据。
<s:if test="page==1"><a href="#">上一页</a></s:if>
<s:else>
<a href="javascript:toPage(<s:property value="page-1"/>);">上一页</s:else>
下一页的按钮同理(略);
首页:
直接给当前页赋值为1,即为首页:
<a href="javascript:toPage(<s:property value="1"/>);">首页</a>
末页同理:直接赋值为totalPage
<a href="javascript:toPage(<s:property value="totalPage"/>);">末页</a>
根据四个条件查询:
当点击搜索按钮时,需要把其中之一的条件或四个条件同时传递给action,同时
需要调用分页的js函数:toPage();形式如下:
<input type="submit" value="搜索" οnclick="toPage(<s:property value="1"/>)"/>
函数里面需要赋予页数作为参数。否则当action调用四个条件查询时,服务将报错,虽然
报错但是页面没有受到影响;个人分析为:需要传给action中的page这个输入属性,作为
当前页。然后action根据page去查询数据。
function toPage(currPage){
//alert(currPage);
document.getElementById("currPage").value = currPage;
document.forms[0].submit();
}
职业选项:
<s:select name="occupation" list="#{'0':'干部','1':'学生','2':'技术人员','3':'其他'}"
headerKey="4" headerValue="--请选择--"/>
name与action中的status绑定,当提交之后action将会获取到key值,即数字的部分;因为
这样的形式是以key:value的方式存在的,即map的形式;
headerKey="4" headerValue="--请选择--":默认显示的第一项
性别选项:
<s:radio list="#{'0':'男','1':'女'}" name="gender" value="0"/>
默认选中value="0"即男 name="gender" 与action绑定;使用这个表达式
已经默认使用了laber for的功能。
新增的功能:虽然界面没有last_login_time,last_login_ip,但是写SQL语句时,
同样要赋值问号:
String sql = "insert into account values(" +
"seq_on_account.nextval,?,?,?,'0',SYSDATE,?,?,?,?,?,?,?,?,?,?,?,?,?,?)";
生日:需要转换时间的格式,因为从界面获取到的时间各个与DB的时间格式不一样,需要
如下转换方可顺利insert到DB:
java.sql.Date birthday = new java.sql.Date(birthdate.getTime());
注意jquery要验证非空且唯一的几个值:login_name、idcard_no
密码的第一次与第二次输入的验证:
使用jQuery验证,第一次验证的方式与第二次验证的思想一致。都要获取到对方的
输入的密码。如果对方为空,只需要验证自身的正则表达式即可;如果对方不为空既要
验证正则,同时还要验证与对方的输入是否相等(只能使用==比较,不能使用match)。
可选项的收缩:如果<div id="optionalInfo" class="hide">的类选择器为hide,那么
它的图标是显示打开的状态,如果为show,则相反。
<img src="../images/show.png" alt="展开" οnclick="showOptionalInfo(this);" />
this是绑定到src的,更换图片。
可选项的判断:
//因为可选项是在点击输入框后才会触动函数,故赋初值为true,方能顺利的判断
提交的结果是否为true还是false,没有点击可选项时,如果赋初值为false,那么
如果它们为空且没有点击触动输入框的情况下,将是false,那就不能提交。如果
赋初值为true,在它们为空且没有点击触动输入框的情况下,将直接是true,那么
就可以顺利的提交,假若他们在最终的验证中传来Boolean值也能顺利的判断。
var recommenderIdcardNos = true;
var emails = true;
var mailAddresss = true;
var zipcodes = true;
var qqs = true;
新增account的推荐人的身份证号码的验证:
$.post("checkRecommenderInfo",
{"recommenderIdCardNo":recommenderIdCardNo},
function(data){
var account = data;
alert(account.id);
}
);
account是从checkRecommenderInfo对应的action中获取到的,可以把整个account对象
发送到addAccount.jsp页面。可以获取到该对象的属性。例如:account.id;
删除功能:当点击删除时不是真正的物理删除,而是修改DB里数据的状态,这样便于
追溯性的维护数据。当点击后修改数据的状态及改行数据的颜色即可;
修改颜色:需要动态的加载与按钮的动态显示相同的思想。即页面加载后立即加载
这些数据。需要遍历tr
//给已删除的数据添加颜色便于区分
for(var i=1;i<tr.length;i++){//第0行是标题行,故从第二行开始
if(spanArray[i-1].innerHTML=="已删除"){
tr[i].className = "delete_tr_color";
}
}
修改功能:
---------------------------------------------------------------------
$.post();触发流程:
$(function(){
$("#recommenderIdcardNo").blur(function(){
var recommenderIdcardNo = $(this).val();
//alert("推荐人88:"+recommenderIdcardNo);
$.post("getRecommIdcardNo",//url必选
{"recommenderIdcardNo":recommenderIdcardNo});//可选
});
});
modifyAccount.js文件通过$().blur();获取到身份证号码后,发送到
getRecommIdcardNo的action,该action获取到这个recommenderIdcardNo
值,然后查询它对应的推荐人的ID号码。
com.netctoss.account.action.SaveModifyAccountAction"
实践证明:使用触发的方式不可行,因为必须触发了才能提交给action。假设
客户不修改,就不需要触发,那么action就没有办法获取到身份证号码。但是
触发的分析思路是正确的。解决客户不修改的情况下,提交保存时也能获取到
身份证号码的方式是直接把idcardNo作为输出属性。即下面的解决方法:
---------------------------------------------------------------------
如何获取到推荐人的身份证号码?
因为查询每一条数据时,只能查询到该条数据的推荐人的ID号(recommenderId)
但是修改页面要显示推荐人的身份证号码。解决方法:
获取到推荐人的ID之后,查询推荐人的身份证号码。
//需要判断recommenderId是否为空,为空就不能调用方法
//查找,否则将报错.注意recommenderId空值为0不是null.
if(recommenderId==0){
idcardNo = null;
}else{
//把查询到的RecommenderId作为参数传入去查询推荐人的信息
recommAnt = dao.findByRecommenderId(recommenderId);
//直接获取到推荐人的身份证号码
idcardNo = recommAnt.getIdcardNo();
}
另外:idcardNo可以直接作为输出属性,直接输出到页面,这样也方便客户修改
好之后提交给保存的action(saveModifyAccountAction).
解决没有推荐人也能保存修改的问题。(即不用填写推荐人的身份证号码)
//因为null与任何值比较都不成立,故可以判断它的长度
if(idcardNo.length()==0){
//不存在推荐人:身份证号码为空,保存时直接设置recommenderId为null,所以
//就不用传递推荐人的身份证号码了。同样是修改保存,但调用的方法不一样。
dao.saveUpdateAccount(ant);
}else{
//存在推荐人:有身份证号码,还需查找recommenderId,同样是修改保存,
//但调用的方法不一样。需要传递idcardNo这个参数
dao.saveModifyAccount(ant,idcardNo);
}
修改页面的各种验证:
jQuery的验证发送到action时,可以同时发送几个参数,例如
$.post(
"checkRepeat",//url地址action地址
{"id":id,"name",name,...},//可以同时发送几个参数
function(){});
密码验证时:第一次要验证第二次的,第二次也要验证第一次的,即双方互相
验证,避免写一致后,又多余添加。
显示account账户的详析信息:
推荐人的身份证号码:根据推荐人的ID号,查询推荐人的身份证号码。同时
需要判断推荐人是否含有推荐人ID,否则就不能调用查找的方法。
开通/暂停/删除时间:根据状态,查询各自的对应时间。使用的是回显的组件,
都是通过ID去查询该条数据,根据ID查询到状态,输出相应的时间。状态为开通---
输出createDate,状态为暂停---输出pauseDate,状态为删除---输出closeDate.
特别注意:从jQuery异步发送数据到action的,不需要配置struts.xml文件,相反
如果从action返回到jQuery文件的,就需要配置struts.xml文件。例如:
$.post(
"checkRepeat",//url地址action地址
{"name":name},//发送数据到action的,不需要配置struts.xml文件
function(data){
var repeat = data;//action至jQuery需要配置struts.xml文件
if(repeat){
$("#costNameMsg").text("用户名不可重复");
}else{
$("#costNameMsg").text("用户名有效");
}
});
反之,action至jQuery需要配置struts.xml文件
<action name="checkRepeat" class="com.netctoss.cost.action.CheckRepeatAction">
<result name="success" type="json">
<param name="root">repeat</param>
</result>
</action>
注意的问题:
业务逻辑上可能没有问题,容易出现问题的地方是在判断条件上。判断条件不
正确就不会输出相应的字段。
当页面真正的去提交到action的时候才会去调用action,如果是页面加载的时候触
发函数调用action,那么即使输出了相关的字段,但是却不能显示到界面,例如:
accountwindow.onload();。
idcardNo能够正常的显示在界面,是因为当点击了“修改”按钮,触发了:
window.location.href="toModifyAccount.action?id="+id;
lastOperationTime能够正常显示在详细信息(detailAccount.jsp)页面是因为,
当单击了名字的链接后,触发了以下函数:
function detailAccount(id){
$.post(
"queryAccount",
{"id":id});
window.location.href="queryAccount.action?id="+id;
}
上次登录时间:
获取当前系统时间为上次登录时间,当点击“返回”时,把当前系统时间更新到
上次登录时间。当单击“名字”时获取上次的登录时间。同样是需要借助
modifyAccountAction这个类。因为都是从ID去查询获取的。
lastOperationTime最后操作的时间(操作开通、暂停、删除的时间),
lastLoginTime上次登录的时间(登录查看,但不一定操作),如果登录了操作
了那两个的时间是一样的。
上次登录IP地址:(通过InetAddress获取)
自动获取当前计算机系统的IP地址。当点击“返回”时自动把IP地址更新到
lastLoginIp,当点击“名字”时,查询该IP地址作显示。同样是需要借助
modifyAccountAction这个类,为了不用写重复的组件!!
通过以上两次点击“返回按钮”可以得出:单击一个按钮可以同时调用两个action
实施说明:
阶段四:围绕业务账号模块的开发
策略(为了节省时间,提高开发效率,综合运用开发好的模块)
1、service业务账号的分页。
创建ServiceVO属性类:业务账号ServiceVO,用于封装关联查询后的结果,相当于
一个伪的实体类,仅仅是在查询时使用,持久化时还是用原来的Service。
因为ServiceVO的分页SQL语句比较长,所以使用了可变换的StringBuffer来添加;
StringBuffer sb = new StringBuffer();
输出SQL语句,用于检查语句是否完整正确。
System.out.println(sb.toString());
sql语句:
select * from(select s.*,a.IDCARD_NO,a.REAL_NAME,c.NAME,
c.DESCR,rownum r from SERVICE s inner join ACCOUNT a on
s.ACCOUNT_ID=a.ID inner join COST c on s.COST_ID = c.id
where 1=1
and s.OS_USERNAME = ?
and s.UNIX_HOST = ?
and a.IDCARD_NO = ?
and s.STATUS = ?
and rownum < ?)where r > ?
本次有两个错误:
select * from(select----缺少了select,报错为表名错误!!
遍历问号preps.setObject(i+1, params.get(i));把i+1写成了1+1,
报错为:java.sql.SQLException: 索引中丢失 IN 或 OUT 参数:: 1
遍历问号参数:因为params的添加到list时是按顺序添加的,所以取时也是按顺序取,
因为问号是从1开始的,故+1,对应参数的问号。
for(int i=0;i<params.size();i++){
preps.setObject(i+1, params.get(i));
}
业务账号分页的结果集采用方法调用的方式:
while(rst.next()){
//使用ServiceVO去接收,结果集查询出来的数据。
//createServiceVO(rst)调用方法
ServiceVO s = createServiceVO(rst);
serlist.add(s);//添加到集合里
}
封装成集合的形式:
List<ServiceVO> serlist = new ArrayList<ServiceVO>();
注意导入支持OGNL标签的声明:
<%@taglib uri="/struts-tags" prefix="s"%>
注意:没有使用totalPage之前也能输出页面的数据,因为已经给page、pageSize
赋了初值,是根据rownum来显示数据的。输出了service的集合。
总的页数SQL:
select count(*)from SERVICE s inner join ACCOUNT a
on s.ACCOUNT_ID=a.ID inner join COST c on s.COST_ID= c.id
where 1=1
分页总页数时遇到的错误:
select count(*)from SERVICE s inner join ACCOUNT a on
s.ACCOUNT_ID=a.ID inner join COST c on s.COST_ID = c.id
where 1=1 and s.STATUS = ? )
java.sql.SQLSyntaxErrorException: ORA-00933: SQL 命令未正确结束
发生以上错误“命令未正确结束”主要是因为SQL语句最后处多了一边括号!
因为检查好SQL语句是否正确书写!!
2、新增业务账户:
注意OS_USERNAME业务账户的约束条件是所在的服务器上的OS_USERNAME是唯一的,
不要求所在的表中唯一。约束条件为:
OS_USERNAME VARCHAR2(8) NOT NULL
CONSTRAINT SERVICE_UNIXHOST_OSUSERNAME_UK
UNIQUE(UNIX_HOST,OS_USERNAME),
从JSP页面发送过来的服务器需要转换,因为从JSP发送过来的是数字1、2、3、4
转换成对应的服务器IP地址:
if(service.getUnixHost().equals("1")){
preps.setString(2, "192.168.0.20");
}else if(service.getUnixHost().equals("2")){
preps.setString(2, "192.168.0.23");
}else if(service.getUnixHost().equals("3")){
preps.setString(2, "192.168.0.26");
}else if(service.getUnixHost().equals("4")){
preps.setString(2, "192.168.0.200");
}
知识点:新增时有的参数是从JSP页面发送给action,有的直接从实现类里获取到。
3、状态维护---删除(不是真正的物理删除,而是改变数据的状态)
把status更改为2
状态:
0-开通(创建时即默认开通)
1-暂停(手动操作)
2-删除(手动操作)
4、状态维护---暂停
把status更改为1,时记录PAUSE_DATE = SYSDATE;
5、状态维护---开通
把status更改为0,同时删除PAUSE_DATE = NULL;
6、修改页面:月初实现触发新的资费类型方法如下:
(1)创建一张新的数据库表service_modify_cost,改变含有的字段有:
id 采用外键的方式references service(id)
old_cost_id,
new_cost_id,
unix_host,
os_username,
modify_date,
action_date;
create table service_modify_cost(
//不能使用ID应用于最后update更新的条件插入,因为同一个人可以开多个业务账号
id number(10) constraint modify_service_id_fk
references service(id) not null,
old_cost_id number(10),
new_cost_id number(10),
unix_host varchar2(15),//应用于最后update更新的条件插入,在该服务器上os账号唯一
os_username varchar2(8),//应用于最后update更新的条件插入,在该服务器上os账号唯一
modify_date date,
action_date date);
);
(2)当点击修改时,服务器需要保存以上7个字段的值,它们获取的方法:
id:service.getId();//修改页面的栈上回显时,提交还是service
old_cost_id:通过id查询该条数据的全部信息,Service ser = new Service();
然后ser.getCostId();
new_cost_id:service.getCostId();//修改页面的栈上回显时,提交还是service
unix_host:修改页面的栈上回显时,提交还是service,故:service.getUnixHost();
os_username:修改页面的栈上回显时,提交还是service,故:service.getOsUserName();
modify_date:在显示类直接获取当前的系统时间;
action_date:暂时为null插入表格。
(3) 点击登录界面的"登录"时,就会调用查询所有的service_modify_cost所有信息,
获取五个字段:
old_cost_id://主要应用于比较cost_id,如果相同则不执行update
new_cost_id://主要应用于比较cost_id,如果相同则不执行update
unix_host://主要应用于最后update更新的条件插入
os_username://主要应用于最后update更新的条件插入
modify_date://主要应用于比较月份,决定是否执行update
实现类的策略:
查询该表的所有信息。获取当前系统的时间,把月份截取出来。
获取修改时间,截取月份。rst.next(),也是一个一个的获取数据的,
并不是一下子全部获取完成具有遍历结果集的作用,所有包含在
while(rst.next()){}里面的都是每运行一次执行一遍,直到执行完毕。
可以不使用集合,也可以顺序添加参数,因为它都是每执行一遍获取一
个参数的,只要该条数据的月份比较大于等于1后就会立刻赋值修改的
SQL问号。比较月份,如果月份大于1,则修改service表的cost_id。
可以不使用集合,之前使用集合,输出listnewcostid.size(),只输出
1,是因为while(rst.next()){}每遍历一遍,就执行了listnewcostid.size()
所以都是输出一的。
(4)把调用更新的触发点放在登录界面,当点击"登录"时,服务器需要
做两件事情:
/*2、向服务器提交登录的验证表单*/
$(function(){
$("#login").click(function(){
//第一件:提交登录验证的表单
document.forms[0].submit();
//第二件:触发service表的修改资费,下月初触发。
$.post("updateServiceCost");//调用该action,action调用DAO
});
});
为什么要放在这里?
因为这里时机是最好的,当点击登录界面的登录时就会去调用更新的action,
如果符合条件了,就会执行更新service表里的cost_id,再未点击“service”
分页的情况下,已经及时的把更新的结果同步到了DB里面,当点击“service”
的分页时,就已经可以显示“实现下月触发新的资费标准”的结果,即实现在
用户不知道的情况下,服务器已经在后台实现了触发,这就体现了软件优雅
的一面。
下次改进的地方:因为这次比较的只是月份而已,如果是从12月跳转到明年的1月
时就无法比较,建议改进成比较年份、月份同时比较。(已改进)
(5)如果同一条数据在同一个月内修改数次,那最后一次修改的那条数据有效,即
以最后修改的那条数据为准,这个是系统自动的。无需进行任何编写。
(6)删除、暂停状态的数据不可修改。如果新的资费ID与旧的资费ID相同,将不保
存结果到DB因为修改成与原来的,那修改就没有意义了,故不用保存,既然没有
保存数据到service_modify_cost,那即使跳转到下个月,也不会修改成功。
额外补充知识:
window.location.href="modifyService.action?id="+id;
理解为:通过js文件向modifyService.action发送一个ID号,并且调用这个
action.
6、详细信息页面
SQL:
select * from (select s.*,a.IDCARD_NO,a.REAL_NAME,
c.NAME,c.DESCR,rownum r from SERVICE s inner join
ACCOUNT a on s.ACCOUNT_ID = a.ID inner join COST c
on s.COST_ID = c.ID where s.id=?)
今天在显示详细信息折腾了一天了,主要是alert(id);不能正常弹出,且整个
findService.js的其他的操作按钮也是不能正常弹出alert(id);最后解决办法:
把整个findService.js的js文件删除了,重新新建一个,然后把内容复制过来,
即可解决问题。
回显数据时,action可以以对象的形式输出,也可以以集合的形式输出。但是在
JSP页面显示时有区别:以下使用的是UI标签
对象的形式输出:service是对象的变量<s:textfield name="service.id"/>
集合的形式输出:service是集合的变量,需要遍历
<s:iterator value="service">
<s:property value="id"/>
</s:iterator>
分析以下两个的区别:
①window.location.href="detailService.action?id="+id;
② $.post(
"detailService",
{"id":id});
相同点:都是把id发送给detailService这个action
不同点: ①不仅发送了id,同时还调用这个detailService。而②仅仅是把id
发送给了detailService,但是却没有任何调用。
实施说明:
阶段五:围绕管理员模块的开发
管理员的分页显示,遇到的
问题一:不能正常显示页面的数据
<s:iterator value="admin">
<tr>
<td><s:textfield name="id"/></td>
</tr>
</s:iterator>
原因:使用了错误的使用了UI标签,应该使用OGNL表达式;
<s:iterator value="admin">
<tr>
<td><s:property value="id"/></td>
</tr>
</s:iterator>
问题二:SQL语句,sql语句未正确结束
select * from ADMIN(select a.*,rownum r from ADMIN a
where 1=1 and rownum < 6) where r > 0;
原因:select * from ADMIN多了一个ADMIN,应该直接接括号
问题三:带条件搜索时,出现下面的分页页码都是2页。
原因:select count(*) from admin。只写了这样的SQL语句;故
不管是否带条件查询,始终都是根据admin表的rownum来统计条数的;
正确的做法:需要带上条件,
sb.append("select count(*) from admin where 1 = 1");
if(module!=null && !module.equals("-1")){
sb.append("and module = ? ");
param.add(module);
}
if(role!=null && role.length()>0){
sb.append("and role = ?");
param.add(role);
}
where 1 = 1永远不能缺少了这个条件,否则带条件查询时,将不会显示
分页的条件。
注意:带条件的搜索,直接是提交的按钮,无需另外的js文件
<input type="submit" value="搜索"
οnclick="toPage(<s:property value="1"/>);"
class="btn_search"/>
角色表只需要一个表
role_info角色信息表:记录角色信息,字段有如下
ID---主键(序列名称:seq_on_role_info)
NAME---角色名称
PRIVILEGE_ID--权限ID,只保存权限ID即可
create table ROLE_PRIVILEGE(
ID NUMBER(11) CONSTRAINT ROLE_PRIVILEGE_ID_PK PRIMARY KEY,
NAME VARCHAR2(20) not null,
PRIVILEGE_ID VARCHAR2 not null
);
create sequence seq_on_rp
increment by 1
start with 1000
nomaxvalue
nocycle
nocache;
PRIVILEGE_ID---角色权限的ID
保存ID即可,具体的权限描述不用保存,
仅保存权限相对应的ID(例如:1234567或1、2、3),
action显示根据ID进行转换显示
ROLE_id 1000 --- PRIVILEGE_ID 1234567 (超级管理员:权限全部拥有)
ROLE_id 1001 --- PRIVILEGE_ID 1 (角色管理员:角色管理)
ROLE_id 1002 --- PRIVILEGE_ID 2 (管理员:管理员)
ROLE_id 1003 --- PRIVILEGE_ID 3 (资费管理员:资费管理)
ROLE_id 1004 --- PRIVILEGE_ID 4 (账务账号管理员:账务账号)
ROLE_id 1005 --- PRIVILEGE_ID 5 (业务账号管理员:业务账号)
ROLE_id 1006 --- PRIVILEGE_ID 6 (账单管理员:账单)
ROLE_id 1007 --- PRIVILEGE_ID 7 (报表管理员:报表)
注意:JSP页面中的<s:checkboxlist
name="" listKey="" listValue=""></s:checkboxlist>
属性一个都不能少;否则将不能正常显示;
验证权限信息只能在action里验证,虽然权限ID重复的情况下,
不能保存新增,但是页面有任何提示,这需要改进的地方。
主要是因为不知道如何用jQuery获取到<s:checkboxlist>
中listKey的值,所以没有在这里验证。但在action中验证了
如若是repeat=false,证明权限ID没有重复,可以保存,如果等于
true证明已经重复了,不能保存
admin管理员基本情况表:(已建成)
ID---管理员的ID
ROLE_ID角色ID,使用外键
管理员页面的分页使用了模糊查找。
把admin与role_privilege表自连起来,组成一张大表,把符合搜
索条件的数据留下。查找出来的数据,需要的显示在界面。查询出来
的数据不一定要显示。
第一个搜索条件:权限的搜索,含有符合的权限都会被找出。
所有使用了通配符查找的方法。
select * from (select a.*,r.* from admin a
inner join role_privilege r on
a.role = r.privilege_id where 1 = 1
and r.privilege_id like '%2%'
and r.role_name = '管理员'
);
复习oracle知识:
%(百分号通配符:百分比符号用来指定零个或多个通配符字符,
而下划线字符指定一个通配符字符。)
select first_name from employees where first_name like 'A%';
将会查找到以A开头的名字,若果是%A%,表示只要含有A的都出现。
下面两个是等价的,因为没有使用通配符,相当于精确查找
where last_name like 'King';
where last_name = 'King';
_下划线通配符:下划线字符指定一个通配符字符。
where last_name like 'K_ng';
中间的是未知的。但是已经固定了剩下三个的。
注意:几乎80%的错误都是出现在SQL语句上。所以要先在数据库中运行了
之后再写入Java代码。
在action的表达方式:
admin = dao.findByCondition("%"+privilegeId+"%", roleName,
page, pageSize);
totalPage = dao.totalPage("%"+privilegeId+"%", roleName,
pageSize);
因为使用了模糊查找,即通配符查找,所以要带上%,这样只要含有某个
字符,就会带出所有的。
实现类的SQL语句:
if(!privilegeId.equals("%null%") &&
!privilegeId.equals("%-1%")){
sb.append("and p.privilege_id like ? ");
param.add(privilegeId);
}
因为action已经是把这个参数"%"+privilegeId+"%"传过去,例如%2%,
所以判断条件也要跟着变化。"%null%"、"%-1%"
注意:虽然拼接成了一个大大的表,但是如果a表没有这个字段,写成
a.privilege_id 这样是错误的。要写成a.role
第二个搜索条件是精确的查找方法。