ATM取款机的数据库模拟开发和实战总结

一.ATM实战开发的简介。

 学习了几天的Oracle,开始着手用数据库PL/SQL语言做一个简单的ATM取款机业务,主要是为了巩固数据库的知识,并非真正的去实现高端的业务。有兴趣的可以看看,希望对同胞们都有用。

  •  ATM的表。它有四张表,用户信息表userinfo,卡号信息表cardinfo,交易信息表tradeinfo,存款类型表deposit。
  用户信息表userInfo
customerIDint客户编号主键
customerNamevarchar(10)用户名not null
personIDvarcahr(20)身份证号

not null unique 只能是15位

或者18位符合实际的身份证号

telephonevarcahr(20)联系电话

not null,格式为xxxx-xxxxxxxx或者xxx-xxxxxxxx

或者11手机号

addressvarchar(30)居住地址可选
--创建userinfo表--
create table UserInfo(
customerID int primary key,
customerName varchar(10) not null,
personID varchar(20) not null unique,
telephone varchar(20) not null,
address varchar(30)
);
--为身份证号和电话增加正则表达式约束--
alter table userinfo add constraint CK_TELEPHONE
check(regexp_like(telephone,'^1[3,4,5,7,8][0-9]-[0-9]{8}$|^[0-9]{3,4}-[0-9]{8}$'));
alter table userinfo add constraint CK_PERSONID
check(regexp_like(personid,'^[0-9]{15}$|^[0-9]{17}[0-9,x]$'));
卡号信息表cardInfo
cardIDvarchar(30)卡号

主键,如1010 3576 xxxx xxxx,

每4位后面有空格,卡号随机产生。

curIDvarchar(5)货币种类必填,默认为RMB
savingIDvarchar(5)存款类型外键,必填。
openDatedate开户日期必填。默认为当前时间
openMoneydecimal(10,2)开户金额必填,不低于1.
balancedecimal(10,2)余额必填,不低于1.
pwdvarchar(10)密码必填,6位数字。默认为888888
isReportLosschar(2)是否挂失必填,只能是'是'或'否'。默认为'否'

  customerid    int    开户编号        外键,必填。

--创建cardinfo表--
create table CardInfo(
cardID varchar(30) primary key,
curID varchar(5) default 'RMB' not null,
savingID varchar(5) not null,
openDate date default sysdate not null,
openMoney decimal(10,2) not null check(openMoney>=1),
balance decimal(10,2) not null check(balance>=1),
pwd varchar(10) default '888888' not null ,
IsReportLoss char(2) default '否' not null,
customerID int not null references UserInfo(customerID)
);
--为卡号和密码增加正则表达式约束--
alter table cardinfo add constraint CK_PWD check
(regexp_like(pwd,'^[0-9]{6}$')); alter table cardinfo add constraint CK_CARDID check
(regexp_like(cardid,'^1010[[:space:]]3576[[:space:]][0-9]{4}[[:space:]][0-9]{4}$'));
交易信息表tradeInfo
transdatedate交易日期必填。默认为系统时间
cardIDvarchar(30)卡号外键,必填。
transTypevarchar(10)交易类型

必填,只能是存入或者支取

transmoneydecimal(10,2)交易金额必填,大于0
remarkvarchar(50)备注可选
--创建tradeinfo表--
create table TradeInfo(
transDate date default sysdate not null,
cardID varchar(30) not null references CardInfo(cardID),
transType varchar(10) not null,
transMoney decimal(10,2) not null,
remark varchar(50)
);

  --为transtype增加约束--
  alter table tradeinfo add constraint CK_TRANSTYPE check (transtype in('支取','存入'));

存款类型表deposit
savingIDint类型编号主键 
savingNamevarchar(20)存款类型名称not null unique
  • ATM模拟实现的业务。

   1.修改密码。

   2.挂失。

   3.查询本周开户的卡号。

   4.查询本月一次性交易金额最高的卡号。

   5.查询卡号挂失的用户的信息。

   6.开户。

   7.存款或者取款。

二.插入数据。

  •    为deposit表插入数据。

   插入数据前我们应该有这样一个认识,与客户无关的信息表先插入数据,与客户有关的次之。因为你开户的时候,客户存款类型必须与存款类型表deposit表的一条记录匹配才行。这就像一个银行一样,不管有多少客户,你银行本身的数据以及功能是必须有的。所以,先插入与银行的信息有关与客户无关的表的数据。

   为deposit插入数据的时候,存款类型编号我们一般也不会自己去输入,所以要创建一个插入类型名称时,自动插入类型编号的触发器。

    首先创建一个savingid的序列。

--创建savingid序列--
create sequence savingid_incr --创建序列不能加or replace
start with 1
increment by 1
nomaxvalue
nocycle
cache 30;

    然后创建savingid的触发器。

--创建savingid的触发器--
create or replace trigger savingid_insert
before insert on deposit
for each row
declare
begin
:new.savingid:=savingid_incr.nextval;
end;

    现在就可以插入数据了。

insert into deposit(savingname) values('定期');
insert into deposit(savingname) values('活期期');
insert into deposit(savingname) values('定活两便');

    检测数据。select * from deposit;可以看到三条savingid自动产生并插入的记录。

  •    为userinfo表和cardinfo表插入数据:

     对于userinfo表,在插入数据的时候,我们不可能每次都去插入客户编号,所以要为客户编号创建一个插入其他数据时的客户编号自动插入的触发器;还要为卡号创建一个随机产生的过程,并且开户的时候通过过程去插入,因为卡号是随机产生并且不能重复,所以我把这个判断写入了开户的业务逻辑中。

     在这里的话,我直接去实现开户,然后通过开户为userinfo表和cardinfo表插入数据,东西很多,我们一步一步来。

     先为customerid创建序列。

create  sequence id_incr --创建序列不能加or replace
start with 1001
increment by 1
nomaxvalue
nocycle
cache 30;

     再为customerid创键触发器。

create or replace trigger id_insert
before insert on userinfo
for each row
declare
next_customerid userinfo.customerid%type;
begin
:new.customerid:=id_incr.nextval;
end;

    为cardid创建随机产生的过程。

create or replace procedure r_cardid(out_id out varchar2)
as
r_num number;
front_id varchar2(4);
back_id varchar2(4);
real_id varchar2(20);
begin
select lpad(trunc(dbms_random.value*100000000),8,0)into r_num from dual;
front_id:=to_char(substr(r_num,1,4));
back_id:=to_char(substr(r_num,5,4));
real_id:='1010 3576 '||front_id||' '||back_id;
out_id:=real_id;
end;

   开户的时候,除了customerid和cardid以及表给的默认值,其他都是与用户开户有关的信息。所以准备工作做好之后,我们就可以实现开户的业务了。

*****************************************************开户******************************************************

******开户的数据*********

  开户时需要用户输入的有:

    身份证号——personid
    存款类型——savingid
    存款金额——openmoney
    本卡密码——pwd
    (在开户时如果用户是第一次开户的话,就又需要的输入的有:)
    姓名——customername
    联系方式——telephone
    地址——address

  系统自动赋予的值有:
    用户号——customerid(触发器触发)
    卡号——cardid(调用r_cardid(outid)过程)

    其他都为系统给的默认值。

******开户的存储过程逻辑******

    1.先判断用户是不是第一个开卡,如果是,那么先创建用户信息,再开户。

    2.用户创建之后或者用户已存在时,调用卡号随机产生的存储过程,产生一个随机产生的不重复的卡号。

    3.卡号产生之后,判断用户输入的存款类型是否正确,正确,就可以开户了。不正确则撤销之前所有的操作并且提示开户终止。

    4.开户就是插入一条符合逻辑的cardinfo记录,并且提示开户的最终信息。

******开户的存储过程************

create or replace procedure openAccount(
temp_personid in userinfo.personid%type,
temp_savingname in deposit.savingname%type,
temp_openmoney in cardinfo.openmoney%type,
temp_pwd in cardinfo.pwd%type,
temp_customername in userinfo.customername%type,
temp_telephone in userinfo.telephone%type,
temp_address in userinfo.address%type)
as
isnullpersonid userinfo.personid%type; --select into判断身份证号是否存在。
temp_cardid cardinfo.cardid%type; --select into判断新产生的卡号是否存在,并且在后来是要用的。
temp_savingid cardinfo.savingid%type;--select into 判断用户输入的存款类型是否可用。
temp_customerid userinfo.customerid%type;
begin
begin
--判断用户是否存在
select personid into isnullpersonid from userinfo where personid=temp_personid;
exception
when no_data_found then
-----创建用户----
insert into userinfo(customername,personid,telephone,address)
values(temp_customername,temp_personid,temp_telephone,temp_address);
end;
begin
while 1=1 loop --产生一个唯一不重复的卡号
r_cardid(temp_cardid);
select cardid into temp_cardid from cardinfo where cardid=temp_cardid;
--如果没有找到,则证明新产生的卡号是唯一的,进入exception继续完成操作。
end loop;
exception
when no_data_found then
--来到这里说明产生的卡号是可用的,接下来就应该判断存款类型temp_savingid.
begin
select savingid into temp_savingid from deposit where savingname=temp_savingname;
--如果存在,那么就可以开户了,如果不存在,则撤销之前的所有操作,用事务。 --customerid是之前就有或者在开户中自动产生的,所以这里要通过SQL找到它。
select customerid into temp_customerid from userinfo where personid=temp_personid;
--开户---
insert into cardinfo(cardid,savingid,openmoney,balance,pwd,customerid)
values(temp_cardid,temp_savingid,temp_openmoney,temp_openmoney,temp_pwd,temp_customerid);
dbms_output.put_line(' 开户成功!');
dbms_output.put_line('您的银行卡号为:'||temp_cardid);
dbms_output.put_line('开户日期:'||sysdate||' 开户金额 '||temp_openmoney);
exception
when no_data_found then
rollback;--撤销之前的所有操作
raise_application_error(-20000, '存款类型不正确,开户终止!');
end;
end;
end;

***************利用开户存储过程来插入数据******************

插入一条用户第一次开户的数据:

set serveroutput on;
execute openAccount(410181199308084016,'定期',50000,762723,'徐万轩','151-03891462','河南省郑州市');

插入一条老用户开户的数据:

set serveroutput on;
execute openAccount(410181199308084016,'活期',50000,762723,'徐万轩','151-03891462','河南省郑州市');

此时查表就会发现有两条cardinfo记录,一条userinfo记录。

  •    为tradeinfo表插入数据。

    tradeinfo表示记录交易信息的,所以我们可以调用存取款的过程来为tradeinfo表插入数据。

*****************************************************存取款存储过程*******************************************

******存取款需要用户的数据**********

  需要用户输入的信息:

    存款或者取款的金额:temp_money
    存款或者取款的类型:temp_transtype
    银行卡号:temp_cardid

******存取款的实现逻辑*************

  1.首先判断用户输入的卡号是否存在,不存在则退出操作。

  2.卡号存在,再判断卡是否挂失,如果挂失,提醒并退出操作。

  3.没有挂失的话,判断存取款类型是否正确,如果正确,就可以存取款了。

  4.存取款时,更新cardinfo表卡的balance余额,并且插入一条交易信息表。

  5.如果取款之后,余额小于1,就会发生检查约束错误,捕获错误并且利用事务的原理rollback之前的操作。

*******存取款的存储过程**************

create or replace procedure inout_money(
temp_money in number,
temp_transtype in tradeinfo.transtype%type,
temp_cardid in cardinfo.cardid%type
)
as
isnulltranstype tradeinfo.transtype%type;--判断存取款类型是否正确
isnullcardid cardinfo.cardid%type;--判断银行卡是否存在
isnullloss cardinfo.isreportloss%type;--判断银行卡是否冻结
begin
begin
--判断卡号是否存在
select cardid into isnullcardid from cardinfo where cardid=temp_cardid;
exception
when no_data_found then
begin
raise_application_error(-20000,'卡号不存在!');
end;
end;
begin
--先判断卡号是否冻结
select isreportloss into isnullloss from cardinfo where cardid=temp_cardid;
if isnullloss='是' then
raise_application_error(-20001,'该卡已冻结,不能执行该操作!');
end if;
--判断存取款类型是否存在
select distinct transtype into isnulltranstype from tradeinfo where transtype=temp_transtype;
if temp_transtype='支取'then
update cardinfo set balance=balance-temp_money where cardid=temp_cardid;
insert into tradeinfo(cardid,transtype,transmoney)
values(temp_cardid,temp_transtype,temp_money);
dbms_output.put_line('取出'||temp_money||'RMB!');
elsif temp_transtype='存入' then
update cardinfo set balance=balance+temp_money where cardid=temp_cardid;
insert into tradeinfo(cardid,transtype,transmoney)
values(temp_cardid,temp_transtype,temp_money);
dbms_output.put_line('存入'||temp_money||'RMB!');
end if;
exception
when no_data_found then
rollback;
raise_application_error(-20002,'存取款类型不正确!');
when others then
dbms_output.put_line('余额不能少于1');
rollback; end;
end;

*************利用存取款存储过程来插入tradeinfo数据*****************

  set serveroutput on;
  execute inout_money(500,'存入','1010 3576 1685 3672');

  set serveroutput on;
  execute inout_money(500,'支取','1010 3576 1685 3672');

  这时,trande表会有两条记录,一个是1010 3576 1685 3672支取的记录,一个是1010 3576 1685 3672的存入记录。

  其实,开户和存取款的过程与插入数据时两回事,但是我们却可以利用这两个过程来实现数据的插入,本人在这里也是懒省事。其实,这样插入数据的话,思路上也比较好理解。毕竟,对于练习来说,我们在实现业务之前的数据只是为了检查这些数据的插入是否满足表的约束和它的合理性,但是,在实际的开发过程中,一个业务的完成的前提条件是你必须有实现这个业务的功能,所以先实现业务,通过业务来插入数据时最为合理的。不过这样对于初学者或者基础不太扎实的同胞可能就有些难以理解了,没事,慢慢来吧。

三.实现业务逻辑。

  • 挂失。

 ********挂失需要用户输入的信息**********

  1.卡号。

  2.密码。

  3.账户ID。

  ******挂失的存储过程*********

create or replace procedure lose(temp_cardid in cardinfo.cardid%type,
temp_pwd in number,temp_customerid in varchar2)
as
row_info cardinfo%rowtype;
islose varchar2(5);
begin
select isreportloss into islose from cardinfo where cardid=temp_cardid;
if islose='是' then
dbms_output.put_line('此卡已经挂失!');
goto last_point;
end if;
select * into row_info from cardinfo
where cardid=temp_cardid;
if row_info.pwd=temp_pwd and row_info.customerid=temp_customerid then
update cardinfo set IsReportLoss='是' where cardid=temp_cardid;
dbms_output.put_line('挂失成功!');
else
dbms_output.put_line('对不起,您输入卡的信息不正确,不能挂失!');
end if;
<<last_point>>
null;
exception
when NO_DATA_FOUND then
dbms_output.put_line('您输入的卡号不存在!');
end;

  **********测试***********

  set serveroutput on;
  execute lose('1010 3576 4654 1134','888866','1001');

  • 修改密码。

  ********修改密码所需的用户的信息********

  1.卡号

  2.密码

  **********修改密码的存储过程**********

create or replace procedure up_pwd(temp_cardid in varchar2,
temp_newpwd in number)
as
temp varchar2(30);
BEGIN
select cardid into temp from cardinfo where cardid=temp_cardid;
update cardinfo set pwd=temp_newpwd where cardid=temp_cardid;
dbms_output.put_line('修改密码成功!');
EXCEPTION
when no_data_found then
dbms_output.put_line('输入的卡号不存在!');
when others then
dbms_output.put_line('违反检查约束');
END;

  *********修改密码的测试*************

    set serveroutput on;
    execute up_pwd('1010 3576 0030 0000','666652');

  • 查询本周开户的卡号。

select * from cardinfo where opendate>=trunc(sysdate,'day');
  • 查询本月一次性交易金额最高的卡号。

select * from tradeinfo;
--from后面跟本月的搜索结果,where处控制transmoney为最大值。
select distinct * from(select * from tradeinfo where transdate>trunc(sysdate,'month'))
where transmoney in( select max(transmoney) from tradeinfo);
  • 查询卡号挂失的用户的信息。

select u.customername,u.customerid,u.personid,u.telephone,u.address
from userinfo u inner join cardinfo c on
c.customerid=u.customerid
where c.isreportloss='是';

  

四.开发中遇到的一些问题。

  在开发的过程中,遇到了许多问题,因为我学习Oracle数据库也就十天左右的时间,对于一些基本的还不是很熟悉。

  遇到的问题:

    1.创建过程的时候不能有declare关键字。

    2.goto流程控制不能goto到exception执行体中。

    3.select into语句千万别返回多行语句,在编译的时候是不会出错的,调用的时候出错也会提示较多的错误,修改很麻烦。比如,查看存款类型是否合法的时候,就需要在select语句前加上distinct来确保返回的是一条语句。

    4.创建序列不可以使用 or replace。

    5.添加行级触发器的时候,赋值用 :new.属性值来赋值。如果select into语句中也赋值的话,就会用两次序列自动产生的值,所以编译器不会报错,但是你的序列的增长率却是你想要的二倍。

    6.在像开户这样的逻辑实现过程中,需要多次去用select去判断。而且它还有判断之后需要共同实现的部分,所以对于我一些新手来说,还是比较难于理解begin end的嵌套结构。bengin  end里面可以嵌套begin end;exception之后的when then也可以接着begin end并且也可以嵌套。

    7.日期函数不是很熟悉。

      tranc(date,day)日期date所在周的周日的日期。

      interval '2' month将2作为month来运算。日期的加减默认是天。

      last_day(date),date日期所在月的最后一天的日期。

      add_months(date,2) date日期两个月之后的日期。

    8.数值函数不是很熟悉。

      dbms_random.value产生0-1之间的小数,精确到很多位。

      lpad(对象,位数,0)向对象左侧填充0,知道对象的位数=输出的位数。

      trunc(数值对象[,截取精度]),如果截取精度>0,则截取到小数点后第几位,如果截取精度<0,则截取到小数点前第几位,截取的部分用0填充。如果截取精度=0,则去掉小数部分。截取精度不写的话默认为0.

04-14 22:29