简介:围绕Oracle项目实战的基金交易平台数据库设计文档,面向数据库初学者、Oracle开发人员以及需要完成课程设计或毕业设计的学生,可用于理解真实业务场景下的数据建模与表结构规划。文档以招商银行某分行的开放式基金交易平台为案例,完整覆盖核心数据建模过程:从需求描述、问题分析到各数据表字段设计,逐一给出基金公司表、基金表、活期账户表、理财账户表、基金账户表等关键表的结构说明,包括字段名称、数据类型、主外键、状态字段和业务含义,并梳理了表间外键关联关系。同时文档还包含相关技术与工具、阶段划分和项目总结,可帮助读者掌握数据库设计的完整流程。资源为1个doc格式文件,压缩包大小245KB,轻量易读。已有451人学习使用,适合作为Oracle数据库设计与项目实践的配套参考资料。
1. 为什么把开放式基金交易平台当作 Oracle 项目实战的首选
做过 Oracle 项目实战的人都清楚,市面上大量教程停留在单表增删改查,一碰到真实业务就断档。开放式基金交易平台是少有的覆盖了表空间、用户权限、约束体系、序列、触发器、存储过程、程序包和完整业务流校验的数据库项目,它不是让你背语法,而是逼着你在一个具体的金融场景里把 Oracle 对象串起来用。这套资源适合两类人:一类是刚学完 SQL 和 PL/SQL 基础、想找一个综合案例练手的在校生,另一类是工作中要接手类似银行、基金、财务系统的开发或维护者,想快速看明白这类系统的表结构和核心业务到底怎么写。项目从一个真实的基金交易需求出发,拆出了七张核心业务表,每张表的字段、约束、索引关系都给了明确设计,连基金购买手续费率、冻结资金流转这些细节都变成了可执行的数据库逻辑,对想要把理论落到工程上的开发者来说,确实是难得的参考。
2. 七张业务表与约束设计:从需求到建表的完整推演
2.1 表结构拆解:字段类型选择的背后逻辑
这个平台的需求分后台管理和前台管理两条线,后台管基金公司、基金、客户账户、交易审核,前台做信息查询、委托交易和账户管理。实体关系梳理下来一共七张表,每张表的字段定义都不是随手的,下面先把核心表列出来看。
基金公司表 FundCompany 保存公司的基本信息和状态,主键 CompanyId 用 varchar2(20),说明这家公司的编号带了业务前缀,比如原文后续提到的 K 加 5 位数字,所以不能用 number。注册资金 Money 用 number(10,2),最大可存到 10 亿,考虑到单位是万元,这个精度对基金公司来说够用。State 字段只允许 0 和 1 两个值,直接做检查约束。
基金表 Fund 里 YearRate 字段是重点,它存的是年利率且要求是 0 到 1 之间的小数,所以类型定为 number(6,5),精度保留五位小数,能表达 0.00001 这种极小的利率差。BuyLimit 是购买下限,number(5,0) 支持最大 99999 份,对开放式基金的申购门槛来说足够了。FundType 和 Invest 是典型的编码字段,用 number(1,0) 存类型码,注释里明确写清楚 1 代表什么、2 代表什么,这种设计在业务表里非常常见。
活期账户表 CurrentAccount 和理财账户表 FinancingAccount 是客户侧的两张核心账表,字段设计的核心差异在于余额概念。活期账户只需要一个 DepositSum 存款金额,而理财账户拆出了 AccountBalance 总金额、EnableBalance 可用余额和 CongealFund 冻结资金三个字段。之所以这样设计,是因为用户在平台上购买基金时,资金需要先被冻结,等交易审核通过后才真正划走,如果只用一个余额字段,就无法表达"钱还在但我暂时不能动"的状态。这个三余额设计贯穿了后面所有的购买、赎回、撤单逻辑,是整个项目里最值得细品的地方。
基金账户表 FundAccount 关联了理财账户和基金公司,一张表同时携带两个外键,表达的是一个理财账户在某基金公司下开设的基金交易账户。FundBuy 是购买合同表,PactNo 作为主键关联后续的每一笔交易;Trade 表则负责记录购买、赎回、撤单的流水,DealType 区分交易类型,Status 记录未完成或已完成。
2.2 建表与约束:主外键、检查约束、唯一约束的落地写法
先建表空间和用户,这一段在项目里是第一步,也是最容易被初学者跳过实际环境的部分。
create tablespace fund datafile 'd:\funddb_file.dbf' size 50M; create user test_user identified by test123456 default tablespace fund; grant connect, resource to test_user; alter user test_user quota unlimited on fund;逻辑说明:create tablespace 创建了一个 50M 的独立表空间,数据文件路径指向 D 盘;create user 创建业务用户并指定默认表空间,后续这个用户下所有对象都会落到 fund 表空间里;grant 那段赋的是 connect 和 resource 两个角色。注意最后一行 alter user quota unlimited 是很多教程不会写的,在部分 Oracle 版本里 resource 角色不再包含无限表空间使用权限,如果没有这一句,后续建表时会直接报 ORA-01950: no privileges on tablespace。
参数说明:datafile 路径在 Windows 环境用反斜杠,Linux 环境改成 '/u01/app/oracle/oradata/funddb_file.dbf' 这样的绝对路径。50M 是初始大小,实际生产环境建议再加 autoextend on。
接下来写表结构,以基金公司和基金两张表为例,体现约束的完整写法。
create table FundCompany ( CompanyId varchar2(20) primary key, Name varchar2(30) not null, Content clob, Money number(10,2) check (Money > 0), State number(1,0) default 0 check (State in (0,1)) ); create table Fund ( FundNo varchar2(20) primary key, CompanyId varchar2(20) not null, FundName varchar2(20) not null, Price number(10,2) check (Price >= 0), FundType number(1,0) check (FundType in (1,2)), Invest number(1,0) check (Invest in (1,2,3,4)), BuyLimit number(5,0) default 0 check (BuyLimit >= 0), IsChange number(1,0) default 0 check (IsChange in (0,1)), YearRate number(6,5) check (YearRate between 0 and 1), ApplyDate date, State number(1,0) default 0 check (State in (0,1)), constraint FK_Fund_Company foreign key (CompanyId) references FundCompany(CompanyId) );逻辑说明:主键直接写在字段级,检查约束用来圈定状态码和枚举值的合法范围。YearRate 的 between 0 and 1 就是刚才说的年利率约束,如果有脏数据想写成 1.5 表示 15%,在数据库层面就会被拒掉。外键约束放在表级,明确 Fund 依赖 FundCompany,保证基金记录不能挂到一个不存在的公司上。
参数说明:clob 类型用于存公司简介这种大文本,number(6,5) 的六位有效数字当中有五位给小数,所以在传参时 0.1 会被存储成 0.10000。如果以后业务利率要改成万分之五这种精度,这个字段也能顶住。
唯一约束在这个项目里要特别留意理财账户和基金账户的开户规则,一个理财账号对同一个公司只能开一个基金账户,对应的建表语句里要加联合唯一约束。
create table FundAccount ( FundAccount varchar2(20) primary key, FinancingAccount varchar2(20) not null, CompanyID varchar2(20) not null, CardType number(1,0) check (CardType in (1,2,3)), CardNo varchar2(20), Name varchar2(10), Sex number(1,0) check (Sex in (0,1)), Address varchar2(100), Phone varchar2(20), PostNum varchar2(10), email varchar2(30), createDate date, CongealState number(1,0) default 0, constraint UQ_FundAccount unique (FinancingAccount, CompanyID), constraint FK_FundAccount_Financing foreign key (FinancingAccount) references FinancingAccount(FinancingAccount), constraint FK_FundAccount_Company foreign key (CompanyID) references FundCompany(CompanyId) );逻辑说明:联合唯一约束 UQ_FundAccount 是这里的核心,它把理财账号和公司 ID 绑在一起,同一个理财账号在同一个公司下只能出现一次,对应业务规则里"一个理财账号对同一个公司只能开一个基金账户"。CongealState 字段单独控制基金账户的冻结状态,冻结之后该客户不能购买对应公司的基金。
参数说明:外键引用的字段类型必须与父表完全一致,FundCompany 的 CompanyId 是 varchar2(20),所以在 FundAccount 里 CompanyID 也得是 varchar2(20),写成 number 会在建表时报 ORA-02267 类型不匹配。表关系整体上是 FundCompany 一对多 Fund,FundCompany 一对多 FundAccount,CurrentAccount 一对一 FinancingAccount,FinancingAccount 一对多 FundAccount,而 Fund 和账户之间通过 FundBuy、Trade 形成购买交易联系。
2.3 外键关系建立的顺序问题
七张表之间的外键关系要先建主表再建从表。项目原文给到的节奏是先创建所有表、再统一添加约束,这种做法的好处是避免建表顺序带来的依赖错误,但要注意建表时如果已经写了 constraint FK 引用一张还不存在的表,同样会报 ORA-00942。常见的做法是先建 FundCompany、CurrentAccount 这些被引用表,再建 Fund、FinancingAccount、FundAccount,最后建 FundBuy 和 Trade。如果哪张表引用了后建的表,就把外键拆出来单独用 alter table 补上,这样即使表建完了发现少约束,也不需要回炉重建。
3. 序列、触发器与程序包:把编号生成和业务操作封装成工程结构
3.1 序列规则与触发器的自动填充机制
项目要求六类编号各有固定格式:基金公司编号是字母 K 加 5 位数字,基金代码是字母 V 加 6 位数字,活期账号和理财账号是 13 位纯数字,基金账户是字母 L 加 5 位数字,合同号是字母 Z 加 6 位数字。这个需求直接用序列加触发器组合实现,先看序列的定义方式。
create sequence seq_company_id start with 1 increment by 1 maxvalue 99999 nocycle; create sequence seq_fund_no start with 1 increment by 1 maxvalue 999999 nocycle; create sequence seq_current_acc start with 1000000000000 increment by 1 maxvalue 9999999999999 nocycle; create sequence seq_financing_acc start with 1000000000000 increment by 1 maxvalue 9999999999999 nocycle; create sequence seq_fund_account start with 1 increment by 1 maxvalue 99999 nocycle; create sequence seq_pact_no start with 1 increment by 1 maxvalue 999999 nocycle;逻辑说明:活期账号和理财账号是 13 位数字,起始值直接设成 1000000000000,这样第一个生成的账号就是 1000000000001,保证位数始终是 13。公司编号和基金账号只需要五位数字,maxvalue 卡住 99999,防止超出业务约定格式。前缀的拼接不是序列干的活,序列只生成纯数字部分,K、V、L、Z 这些前缀要由触发器或者插入逻辑来拼。
参数说明:nocycle 是硬性要求,如果序列走到 maxvalue 再继续 nextval 会报 ORA-08004,而 nocycle 参数下超出范围会直接报错,避免出现重复编号。实际生产环境建议把 maxvalue 放大到 9 开头的一长串数字,配合 to_char 截位用,省得序列生命周期太短。
触发器的写法是把序列和业务格式拼在一起,以基金公司插入为例。
create or replace trigger trg_fundcompany_id before insert on FundCompany for each row begin select 'K' || lpad(seq_company_id.nextval, 5, '0') into :new.CompanyId from dual; end; /逻辑说明:lpad 函数把序列生成的数字左侧补零到五位,seq_company_id 第一次取到 1,拼出来就是 K00001,第十次是 K00010。for each row 表示这是行级触发器,每一行插入前都会执行一次,保证每条记录都拿到独立编号。:new.CompanyId 是触发器的绑定变量,把拼好的值写回正在插入的那一行。
参数说明:varchar2 的序列值配合 to_char 是另一个常用写法,'K' || to_char(seq_company_id.nextval, 'FM00009') 可以达到同样的补零效果。注意 FM 格式修饰符会去掉结果里的前导空格,不然 to_char 在某些格式下会输出空格导致主键变成 'K 1' 这种脏数据,这个坑我在实际项目里踩过。
FundBuy 表的主键合同号同样是串行号加前缀,FundBuy 表有 State 字段表示审核状态,0 未审核 1 审核通过。交易表 Trade 的 PactNo 引用 FundBuy 的 PactNo,要保证购买、赎回、撤单在合同号上是有延续关系的,撤单时使用原采购合同号,但 DealType 字段标记为 2,查询未完成交易时才能把整套流程串起来。
3.2 程序包的设计:把功能按业务域拆开
这个项目把存储过程按模块拆成了六个程序包,结构非常清晰:
FundManager_pack 负责基金公司和基金的新增、查询、冻结解冻;FundAccountManager_pack 负责基金账户开户和信息查询;FinancingAccountManager_pack 负责理财账户的密码修改和转账;ClientAccountManager_pack 负责活期、理财、基金账户的综合管理;Auditing_pack 负责交易审核;Consign_pack 负责购买、赎回、撤单。
以基金管理和账户管理其中一个包为例,看程序包的结构:
create or replace package FundManager_pack is procedure addFundCompany(p_name in varchar2, p_content in clob, p_money in number); procedure addFund(p_companyId in varchar2, p_fundName in varchar2, p_price in number); procedure freezeFundCompany(p_companyId in varchar2, p_state in number); procedure queryFundByCompany(p_companyId in varchar2); end FundManager_pack; / create or replace package body FundManager_pack is procedure addFundCompany(p_name in varchar2, p_content in clob, p_money in number) is begin insert into FundCompany(Name, Content, Money) values (p_name, p_content, p_money); commit; end addFundCompany; procedure addFund(p_companyId in varchar2, p_fundName in varchar2, p_price in number) is begin insert into Fund(CompanyId, FundName, Price) values (p_companyId, p_fundName, p_price); commit; end addFund; procedure freezeFundCompany(p_companyId in varchar2, p_state in number) is begin update FundCompany set State = p_state where CompanyId = p_companyId; commit; end freezeFundCompany; end FundManager_pack; /逻辑说明:程序包头只声明了过程的名字和参数,包体里才是真正的 SQL。从调用方视角看,只需要知道包名和过程名就能调用,不需要关注表结构,这就是封装的意义。冻结解冻的操作本质是一次 update,把 State 从 0 改成 1 是冻结,从 1 改成 0 是解冻,外面传参传 0 或 1 就控制了行为。
参数说明:addFundCompany 不接收 CompanyId,因为主键由序列加触发器自动生成,人工传参反而容易造成主键冲突。如果 p_money 传负数,检查约束会在 insert 阶段报 ORA-02290,所以在包体里其实不需要再写 if 判断。项目原文里提到基金管理模块的冻结解冻只需要修改 State 状态值,这也是为什么所有状态字段都用 number(1,0) 而不定义成布尔或字符串,Oracle 没有原生布尔字段类型,用 0/1 数字是规范做法。
3.3 查询接口的三个边界条件
活期账户查询支持按账号精确查询和按姓名模糊查询,这是入门阶段最容易写错的逻辑,因为模糊查询的拼法影响索引使用和结果集范围。
create or replace procedure queryCurrentAccount( p_account in varchar2 default null, p_name in varchar2 default null ) is begin if p_account is not null then for rec in (select * from CurrentAccount where CurrentAccount = p_account) loop dbms_output.put_line(rec.Name || ' ' || rec.CurrentAccount); end loop; elsif p_name is not null then for rec in (select * from CurrentAccount where Name like '%' || p_name || '%') loop dbms_output.put_line(rec.Name || ' ' || rec.CurrentAccount); end loop; else raise_application_error(-20001, '查询条件不能为空'); end if; end queryCurrentAccount; /逻辑说明:这个过程的处理逻辑是先判断传入的是账号还是姓名,账号走等值查询,姓名走模糊查询,两边都为空直接抛业务异常。注意默认值 default null 的使用,调用方只传 p_name 时不需要占位符。模糊查询的写法用 '%' || p_name || '%' 而不是把百分号直接写在参数里,这样调用方传 '张' 就行,不用关心拼接。
参数说明:如果两个条件都传了值,这种写法会优先走账号精确查询,名称条件被忽略。实际做接口时更合理的做法是先做判断并给出提示,或者把两个条件都叠进 where 里用 and/or 组合,这个项目里选择的是简单分支,从学习角度够用,但从工程角度还有优化空间。
4. 核心业务过程:购买、赎回、撤单、转账的数据库实现
4.1 购买基金的校验顺序与冻结资金流转
购买基金是这个平台最核心的业务流程,原文里其实隐藏了一个严格的校验顺序,顺序错了会造成资金状态异常。把六个校验条件按正确顺序排在代码里,以存储过程的方式呈现。
create or replace procedure buyFund( p_pactNo in varchar2, p_fundAccount in varchar2, p_fundNo in varchar2, p_fundNumber in number ) is v_companyId varchar2(20); v_fundPrice number(10,2); v_buyLimit number(5,0); v_accountBalance number(10,2); v_congealState number(1,0); v_companyState number(1,0); v_fundState number(1,0); v_amount number(10,2); begin select CompanyId, Price, BuyLimit, State into v_companyId, v_fundPrice, v_buyLimit, v_fundState from Fund where FundNo = p_fundNo; select State into v_companyState from FundCompany where CompanyId = v_companyId; select CongealState, FinancingAccount into v_congealState, p_fundAccount from FundAccount where FundAccount = p_fundAccount; if v_fundState = 1 then raise_application_error(-20001, '该基金已被冻结,不能购买'); end if; if v_companyState = 1 then raise_application_error(-20002, '该基金公司已被冻结,不能购买'); end if; if v_congealState = 1 then raise_application_error(-20003, '基金账户被冻结,不能进行交易'); end if; if p_fundNumber < v_buyLimit then raise_application_error(-20004, '购买数量不能低于购买下限'); end if; v_amount := p_fundNumber * v_fundPrice; select AccountBalance into v_accountBalance from FinancingAccount where FinancingAccount = (select FinancingAccount from FundAccount where FundAccount = p_fundAccount); if v_accountBalance < v_amount then raise_application_error(-20005, '资金不足,无法购买'); end if; commit; end buyFund; /逻辑说明:这里只展示了校验部分,真实场景在通过校验后要插入 FundBuy 记录、把购买金额从 AvailableBalance 转入冻结金额、插入 Trade 交易记录。select 语句把 Fund 表的多个字段一次取出来存入变量,比每个字段单独写一条 select 性能更好。冻结资金的设计是购买成功后未审核前,资金放在冻结区,审核通过前不扣手续费,主要目的是防止用户一边提交购买一边把余额转走。
参数说明:v_amount 这里的算法是购买份数乘以基金净值,并不包含手续费。原文里明确说了购买手续费率 0.75%,但手续费是在审核通过之后才真正扣除,所以在购买这个阶段只冻结本金,手续费不动。审核通过时再做一次 update,从冻结资金中扣掉手续费,把剩余部分划出冻结区。
4.2 赎回与撤单:方向相反的两笔流水
赎回的逻辑和购买对称,校验顺序上同样要求公司和基金不能冻结、基金账户不能冻结、赎回份数不能超过持有数量。赎回成功后要扣 1.5% 的手续费,赎回的份额从持有中减掉,同时把等值资金加入冻结资金中等待审核。这一条业务规则容易被理解错,很多人以为赎回是审核通过后直接到账,但这个项目里赎回同样要经过审核,在审核前资金先进入冻结资金而不是可用余额。
撤单是个特例,状态机上看,撤单只允许发生在购买成功后、审核通过前,一旦审核通过,交易已经生效,只能走赎回流程。撤单动作本身不扣手续费,会把冻结资金原路退回可用余额,同时把 FundBuy 的状态改成已撤销,并在 Trade 表插入一条 DealType 为 2 的记录。注意这里用的是修改状态而不是物理删除,从审计角度考虑,交易痕迹必须保留,这也是这类系统里常见的软删除设计。
4.3 转账逻辑中的冻结判断
活期转理财和理财转活期,表面上是一进一出,实际逻辑有两处关键差异。活期转理财,需要判断活期密码是否正确、活期资金是否足够、活期账户是否冻结;理财转活期,需要判断理财密码是否正确、可用资金是否充足、理财账户是否冻结。两个字面理解很接近,但账务处理完全不同:
活期转理财时,活期账户的存款金额直接减少,理财账户的总金额和可用余额同时增加。理财转活期时,理财的可用余额减少,活期存款增加。这里有个细节规定:活期转入的金额只加入到总金额和可用余额中,也就是说活期转理财的钱不需要走冻结区;反过来理财转活期时,如果这笔钱是从购买基金冻结中解冻出来的,会先回到理财的可用余额,再做转出判断。原文里还要求活期账户被冻结时,理财账户不能被启用,冻结活期时必须连带冻结理财账户,这两条规则一起保证了一个用户不会在账户被冻结的状态下继续转账或交易。
4.4 审核模块的幂等设计
交易审核模块用程序包 Auditing_pack 实现,分别处理基金购买审核和赎回审核。审核过程要做幂等保护,防止同一笔交易被重复审核。常见的做法是在审核过程开头先查询 FundBuy 或 Trade 的状态,如果已经是审核通过状态就直接返回;另一个做法是审核更新时加 where 条件,例如 update FundBuy set State = 1 where PactNo = p_pactNo and State = 0,SQL 本身会返回影响的函数,如果影响行数为 0 就说明已经被审核过。这个技巧在并发场景下很有用,比起先 select 再 update 更能防止两个会话同时审核同一笔单。
5. 避坑与常见问题:我在跑这套项目时踩过的五个坑
5.1 表空间权限报错 ORA-01950
现象:执行 create table 时提示 ORA-01950: no privileges on tablespace 'FUND'。
原因:在部分 Oracle 版本里,grant resource 角色后并没有自动授予用户对表空间的无限使用权限。用户虽然能连接数据库,但在非默认表空间或需要新建段时被拒绝。项目原文只写了 grant connect, resource to test_user,没有补 quota,直接照做就会翻车。
解决:在创建用户并授权之后追加一条 alter user test_user quota unlimited on fund,或者 grant unlimited tablespace to test_user。如果希望更精细控制,可以只给 50M 的限额,alter user test_user quota 20M on fund。
5.2 触发器补零后主键出现空格
现象:插入 FundCompany 后查询发现 CompanyId 为 K 1 而不是 K00001,后续按 CompanyId 关联查询查不到数据。
原因:lpad 函数在目标长度小于源字符串长度时不会截断,但问题往往出在把它与 to_char 的格式混用。另一种情况是 to_char(seq.nextval, '00009') 时,Oracle 默认会在数字前面补空格,补出来的是 ' 1',再拼上字母 K 就变成 K 1。
解决:统一用 lpad(seq_company_id.nextval, 5, '0') 来补位,不使用 to_char 的数字格式。如果一定要用 to_char,用 FM 格式修饰符,写成 to_char(seq.nextval, 'FM00009')。建好触发器后在插入一条测试数据,立刻查询比对编号格式,确认没问题再继续后面的操作。
5.3 检查约束没生效导致脏数据入库
现象:Fund.YearRate 字段能插入 1.5 这样的值,State 字段能插入 2 这样的值。
原因:在旧库或手工改表的情况下,检查约束没有启用或者约束不是 immediate 模式。有些开发者在 Navicat 或 PL/SQL Developer 里用可视化建表,生成了约束但状态是 disable,或者约束本身创建时没有 validate。
解决:查询约束状态:select constraint_name, status from user_constraints where table_name='FUND'。如果 status 是 DISABLED,执行 alter table Fund enable constraint SYS_C001234。要注意的是启用约束之前需要先处理掉已有的脏数据,否则启用时也会报错。平时建表时把检查约束写进 create table 语句里,不建议建完表再补。
5.4 外键字段类型不一致导致建表失败
现象:创建 FundAccount 时报 ORA-02267: column type incompatible with referenced column type。
原因:FundAccount 的 CompanyID 定义成了 varchar2(20),但被引用的 FundCompany.CompanyId 在原文表 1-1 里也是 varchar2(20),按理说没问题。实际翻车多发生在有人把 CompanyID 定义成 number(10) 或把 FundNo 定义成不同长度。外键要求的是类型与长度都一致,number 与 varchar2 属于硬性不兼容。
解决:建表前统一字段模板,把所有作为主键和外键的编号类字段全部定义为 varchar2(20),遇到复合主键关联时逐一比对字段类型。这里也建议在项目里维护一张数据字典清单,每个表中哪个字段引用了哪张表的主键,类型是否一致,一目了然。
5.5 审核重复执行造成资金被扣两次
现象:同一条购买记录执行了两次审核,审核结果显示成功,但理财账户余额明显多扣了一笔。
原因:审核过程没有做状态判断,直接执行扣款更新,第二次审核时把同样的金额又扣了一遍。这个在基础实现中经常出现,如果不加事务保护,后果就是资金账不平。
解决:审核过程最前面加一行状态判断,select State from FundBuy where PactNo = p_pactNo,如果 State 已经是 1 就直接 return 或 raise_application_error。更稳妥的做法是用条件更新代替先查再改,update FundBuy set State = 1 where PactNo = p_pactNo and State = 0,然后判断 sql%rowcount,为 0 时说明已经审核过,跳出操作。这是我在以后每次写审核类存储过程时都会强制加上的保护逻辑。
6. 最后验证这套项目跑通的三个手段
项目做完了不能只看存储过程编译通过,要把整个业务流从头到尾走一遍才算数。这里分享三个我每次拿到别人 Oracle 项目都会先执行的验证手段。
第一个手段是资产对账。模拟完整流程:开活期账户、开理财账户、某公司开户、添加基金、买入、审核、再赎回,每步操作后用 SQL 对比账户余额、可用余额、冻结资金三个字段的变化。以一次 10000 元申购为例,购买成功后 AccountBalance 保持不变,EnableBalance 减少 10000,CongealFund 增加 10000;审核通过后手续费 75 元从冻结资金扣除,剩下 9925 元转回 EnableBalance,CongealFund 归零。验证时不看业务表界面,直接查 FinancingAccount 的原始字段,数字对不上说明哪一步写崩了。
第二个手段是权限切换测试。用一个只有 connect 角色的新用户连接数据库,尝试查询 FundCompany 表和调用 FundManager_pack.addFundCompany,确认无权限后,授权 resource 再试一次。这个项目原文里创建了 test_user 并授权,但如果别人拿到的环境里没有这些权限,很容易出现 PL/SQL Developer 能连上但执行存储过程报权限不足的情况。提前切用户测一遍,能省去很多环境问题排查的时间。
第三个手段是检查序列的断档。连续执行几十次插入,然后对比 PactNo 看是否有跳号。正常情况序列是连续的,但如果发生过事务回滚,序列的 nextval 已经消耗但插入被回滚了,那个编号就永久缺失,这在银行类系统里通常是要避免的。如果想要严格连续,就不使用序列,改用一张编号表的 update 加锁来实现,但项目里明确使用了 create sequence,所以验收标准是序列编号单调递增即可,不要求无断档。
跑完这三个手段,这套项目算是真正验证过了,不只是编译通过。
说实话,第一次跑这种带资金冻结逻辑的项目时,我也没少因为意识不到 Potential 状态机的问题而反复查询余额数据。从那以后,我每次做完一个 Oracle 项目,都会强制自己至少走一遍余额对账和三段式权限切换。这套顺序做完,跳出来的低级问题一个都藏不住。希望这些记录对正在做 Oracle 项目的你有帮助。
本文还有配套的精品资源,点击获取