简介:这份资源面向学习 Oracle 数据库设计与项目实战的开发者与在校学生,以招商银行某分行开放式基金交易平台为业务背景,完整呈现从需求描述到数据表设计的落地过程,适合作为数据库课程设计或面试项目复盘的参考案例。压缩包内共 1 个 doc 文档,约 245KB,以项目实战文档形式组织,涵盖需求描述、问题分析、相关技术与工具、阶段划分及项目总结等章节。文档围绕基金公司表、基金表、活期账户表、理财账户表、基金账户表等核心表展开,逐字段给出数据类型与说明,并说明表间外键关联关系,同时延伸至基金购买表与交易表的设计思路,帮助读者理解账户冻结、交易审核等业务场景如何映射为数据库结构。目前已有 452 人学习下载,适合需要一份可直接对照的 Oracle 项目案例来梳理建表逻辑与业务建模思路的读者。
1. 从一份“只有需求文档”的 Oracle 课设说起
带过几届数据库课设之后,我发现一个规律:真正卡住人的从来不是 Oracle 装不上,而是拿到一份需求描述后不知道从哪下手。基金交易平台这个题目尤其典型——需求里列了后台管理、前台管理两大块,涉及基金公司、基金、活期账户、理财账户、基金账户、购买记录、交易流水七张表,还要处理冻结解冻、审核、撤单、手续费这些业务规则。很多同学第一反应是打开 PL/SQL Developer 就开始建表,建到一半发现外键顺序不对,或者触发器写完了序列还没建,来回返工。
这套方案要解决的就是这件事:把一份开放式基金交易平台的数据库设计,从需求拆解、表结构设计、约束与对象规划,一路做到程序包实现和验收答辩。适合正在做 Oracle 数据库课设、毕设,或者需要交一份完整数据库设计方案的读者。下面按真实交付顺序展开,每一步都给出可执行的 SQL 和 PL/SQL,照着改参数就能跑。
2. 需求拆解与表结构设计:七张表怎么摆、外键怎么连
2.1 先把业务角色和账户层级理清楚
这份需求最容易翻车的地方,是账户体系有三层:活期账户、理财账户、基金账户。很多同学一上来就按表名建表,结果发现“一个活期账户只能开一个理财账户”“一个理财账户对同一家公司只能开一个基金账户”这两条规则没地方落。正确的做法是先画清楚层级关系:活期账户是资金入口,理财账户挂在活期账户下管理投资资金,基金账户再挂在理财账户下、并且绑定到具体的基金公司。资金流向是活期转理财、理财买基金,赎回后资金回到理财账户的冻结资金里,审核通过才真正解冻。
理清这层关系之后,七张表的职责就明确了:FundCompany 和 Fund 是产品侧,CurrentAccount、FinancingAccount、FundAccount 是账户侧,FundBuy 和 Trade 是交易侧。产品侧和账户侧通过 FundAccount 的 CompanyID 关联,交易侧通过 PactNo 把购买记录和交易流水串起来。这个划分决定了后面建表的顺序,也决定了外键能不能一次建对。
2.2 建表顺序与外键依赖
外键有依赖,建表就必须讲顺序。被引用的表先建,引用别人的表后建。按这个原则,顺序是:FundCompany → Fund → CurrentAccount → FinancingAccount → FundAccount → FundBuy → Trade。其中 FundBuy 引用 FinancingAccount 和 Fund,Trade 引用 FundBuy、FinancingAccount、Fund、FundAccount。如果顺序反了,Oracle 会直接报 ORA-02291,这时候不用慌,把建表语句顺序调过来重跑即可,但已经插入的数据要清掉。
下面给出核心几张表的建表语句,字段类型和约束严格按需求文档来,检查约束和默认值一并加上:
-- 基金公司表:State 只允许 0/1,用检查约束兜住 CREATE TABLE FundCompany ( CompanyId VARCHAR2(20) PRIMARY KEY, Name VARCHAR2(30) NOT NULL, Content CLOB, Money NUMBER(10,2), State NUMBER(1,0) DEFAULT 0, CONSTRAINT ck_company_state CHECK (State IN (0,1)) ); -- 基金表:年利率必须在 0-1 之间,这是需求里明确写的 CREATE TABLE Fund ( FundNo VARCHAR2(20) PRIMARY KEY, CompanyId VARCHAR2(20), FundName VARCHAR2(20) NOT NULL, Price NUMBER(10,2), FundType NUMBER(1,0), Invest NUMBER(1,0), BuyLimit NUMBER(5,0), IsChange NUMBER(1,0) DEFAULT 0, YearRate NUMBER(6,5), ApplyDate DATE DEFAULT SYSDATE, State NUMBER(1,0) DEFAULT 0, CONSTRAINT fk_fund_company FOREIGN KEY (CompanyId) REFERENCES FundCompany(CompanyId), CONSTRAINT ck_year_rate CHECK (YearRate >= 0 AND YearRate <= 1), CONSTRAINT ck_fund_state CHECK (State IN (0,1)) ); -- 理财账户表:可用余额 + 冻结资金,购买未审核时资金进冻结 CREATE TABLE FinancingAccount ( FinancingAccount VARCHAR2(20) PRIMARY KEY, FinancePassWord VARCHAR2(20) NOT NULL, MoneyType NUMBER(1,0) DEFAULT 1, AccountBalance NUMBER(10,2) DEFAULT 0, EnableBalance NUMBER(10,2) DEFAULT 0, CongealFund NUMBER(10,2) DEFAULT 0, State NUMBER(1,0) DEFAULT 0, CurrentAccount VARCHAR2(20), CONSTRAINT fk_fin_current FOREIGN KEY (CurrentAccount) REFERENCES CurrentAccount(CurrentAccount), CONSTRAINT ck_fin_state CHECK (State IN (0,1)) );建表时有两个细节值得单独说。第一,YearRate NUMBER(6,5)这个精度意味着最多存 0.99999,配合检查约束刚好卡住 0 到 1 的区间,比用NUMBER裸存再在应用层判断要稳。第二,理财账户的AccountBalance和EnableBalance是两个字段,前者是总金额,后者是可用余额,购买基金时冻结的是EnableBalance里的钱、记到CongealFund,审核通过才从总金额里扣。这个设计如果一开始没想清楚,后面写购买存储过程时会发现资金对不上账。
2.3 约束设计:三类完整性怎么落到 DDL 上
数据库的三类完整性——实体完整性、参照完整性、用户定义完整性——在这套表里都有明确落点。实体完整性靠主键,七张表的主键分别是 CompanyId、FundNo、CurrentAccount、FinancingAccount、FundAccount、PactNo,Trade 表没有独立主键,用 PactNo 加交易类型组合区分。参照完整性靠外键,上面建表语句里已经体现。用户定义完整性靠检查约束,除了年利率和状态位,还有几个容易漏的:活期账户的CardType只能是 1/2/3,基金类型只能是 1/2,投资方向只能是 1 到 4。
-- 活期账户表:证件类型和状态都要卡住 CREATE TABLE CurrentAccount ( CurrentAccount VARCHAR2(20) PRIMARY KEY, CurrentPassword VARCHAR2(20) NOT NULL, DepositSum NUMBER(10,2) DEFAULT 0, CardType NUMBER(1,0), CardNo VARCHAR2(20), Name VARCHAR2(10), Address VARCHAR2(100), Phone VARCHAR2(20), Sex NUMBER(1,0), OpenAccDate DATE DEFAULT SYSDATE, State NUMBER(1,0) DEFAULT 0, CONSTRAINT ck_card_type CHECK (CardType IN (1,2,3)), CONSTRAINT ck_sex CHECK (Sex IN (1,2)), CONSTRAINT ck_cur_state CHECK (State IN (0,1)) );这里有个血泪经验:检查约束的命名一定要规范,用ck_表名缩写_字段的格式。等到第五阶段调试购买流程、报出ORA-02290: check constraint violated的时候,你能一眼从约束名看出是哪个字段越界,不用去翻USER_CONSTRAINTS视图一条条对。这个习惯在课设阶段看着多余,答辩现场被问到“你怎么保证数据合法性”时,直接指着约束名讲,比空谈理论有说服力得多。
3. 序列与触发器:主键自动生成的完整实现
3.1 六种编号规则与序列创建
需求对编号格式有硬性要求:基金公司 K+5 位数字、基金代码 V+6 位、活期账号 13 位纯数字、理财账号 13 位、基金账户 L+5 位、合同号 Z+6 位。Oracle 的序列本身只能生成纯数字,带字母前缀要靠触发器拼接。所以思路是:每个编号建一个序列,序列从 1 开始、步长 1、不缓存(课设环境数据量小,NOCACHE避免序列跳号带来的困惑),触发器在插入前把前缀和补零后的序列值拼起来。
-- 基金公司编号序列:K + 5 位 CREATE SEQUENCE seq_company START WITH 1 INCREMENT BY 1 NOCACHE; -- 基金代码序列:V + 6 位 CREATE SEQUENCE seq_fund START WITH 1 INCREMENT BY 1 NOCACHE; -- 活期账号序列:13 位,起始值给足位数 CREATE SEQUENCE seq_current START WITH 1000000000001 INCREMENT BY 1 NOCACHE; -- 理财账号序列:13 位 CREATE SEQUENCE seq_financing START WITH 1000000000001 INCREMENT BY 1 NOCACHE; -- 基金账户序列:L + 5 位 CREATE SEQUENCE seq_fundacc START WITH 1 INCREMENT BY 1 NOCACHE; -- 合同号序列:Z + 6 位 CREATE SEQUENCE seq_pact START WITH 1 INCREMENT BY 1 NOCACHE;活期和理财账号起始值设成 13 位,是为了让LPAD补零之后位数刚好够,不用在触发器里做额外判断。如果从 1 开始,补零到 13 位也能用,但起始值给足更直观,答辩时解释起来也顺。
3.2 触发器拼接前缀与补零
触发器用BEFORE INSERT行级触发器,在:NEW上赋值。以基金公司为例:
CREATE OR REPLACE TRIGGER trg_company_id BEFORE INSERT ON FundCompany FOR EACH ROW BEGIN IF :NEW.CompanyId IS NULL THEN :NEW.CompanyId := 'K' || LPAD(seq_company.NEXTVAL, 5, '0'); END IF; END; /这里加IF :NEW.CompanyId IS NULL判断,是为了兼容手动指定编号的场景——比如测试时想插一条固定 ID 的数据,触发器不会覆盖。基金代码、基金账户、合同号的触发器结构完全一样,只改表名、字段名、前缀和补零位数。活期和理财账号是纯数字 13 位,触发器里用LPAD(seq_current.NEXTVAL, 13, '0'),不加字母前缀。
六个触发器写完,建议按表逐个测试:插一条不带主键的记录,查一下生成的编号格式对不对。这一步看着琐碎,但能提前暴露序列起始值、补零位数、触发器触发时机的问题。我一般会写一个简单的验证脚本,六个表各插一条,然后SELECT出来肉眼核对格式,比等到第五阶段联调时才发现编号不对要省事得多。
3.3 程序包骨架:把过程按模块归位
需求把功能划成了五个程序包:FundManager_pack(基金管理)、FundAccountManager_pack(基金账户)、FinancingAccountManager_pack(理财账户)、ClientAccountManager_pack(客户账户)、Auditing_pack(交易审核)、Information_pack(信息查询)、Consign_pack(委托交易)。程序包的好处是把相关过程打包,调用时用包名.过程名,答辩演示时结构清晰。
CREATE OR REPLACE PACKAGE FundManager_pack AS PROCEDURE add_company(p_name IN VARCHAR2, p_content IN CLOB, p_money IN NUMBER); PROCEDURE add_fund(p_company_id IN VARCHAR2, p_fund_name IN VARCHAR2, p_price IN NUMBER, p_buy_limit IN NUMBER); PROCEDURE toggle_company(p_company_id IN VARCHAR2, p_state IN NUMBER); PROCEDURE toggle_fund(p_fund_no IN VARCHAR2, p_state IN NUMBER); PROCEDURE query_fund_by_company(p_company_id IN VARCHAR2, p_cursor OUT SYS_REFCURSOR); END FundManager_pack; /包规范里只声明过程签名,包体里写实现。查询类过程用SYS_REFCURSOR出参,方便在 PL/SQL Developer 的测试窗口里直接看结果集。这个骨架先搭好,后面每个阶段往里填过程,不会出现“过程写到一半不知道放哪个包”的情况。
4. 核心业务过程实现:购买、赎回、撤单与资金冻结
4.1 购买基金:六道校验加资金冻结
购买是整套系统里校验最多、最容易出错的过程。需求列了六条判断:基金账户是否开通、公司和基金是否冻结、购买数量是否达到下限、资金是否足够、基金账户是否冻结、手续费 0.75%。这六条要按顺序校验,任何一条不满足就RAISE_APPLICATION_ERROR抛出明确错误码,方便前端定位。
CREATE OR REPLACE PROCEDURE Consign_pack.buy_fund( p_fund_account IN VARCHAR2, p_fund_no IN VARCHAR2, p_shares IN NUMBER ) AS v_price Fund.Price%TYPE; v_buy_limit Fund.BuyLimit%TYPE; v_company Fund.CompanyId%TYPE; v_fund_state Fund.State%TYPE; v_comp_state FundCompany.State%TYPE; v_acc_state FundAccount.CongealState%TYPE; v_enable FinancingAccount.EnableBalance%TYPE; v_cost NUMBER(10,2); v_pact_no VARCHAR2(20); BEGIN -- 1. 取基金信息,同时校验基金和公司状态 SELECT f.Price, f.BuyLimit, f.CompanyId, f.State, c.State INTO v_price, v_buy_limit, v_company, v_fund_state, v_comp_state FROM Fund f JOIN FundCompany c ON f.CompanyId = c.CompanyId WHERE f.FundNo = p_fund_no; IF v_fund_state = 1 THEN RAISE_APPLICATION_ERROR(-20001, '该基金已冻结,无法购买'); END IF; IF v_comp_state = 1 THEN RAISE_APPLICATION_ERROR(-20002, '该基金公司已冻结,无法购买'); END IF; -- 2. 校验基金账户状态 SELECT CongealState INTO v_acc_state FROM FundAccount WHERE FundAccount = p_fund_account; IF v_acc_state = 1 THEN RAISE_APPLICATION_ERROR(-20003, '基金账户已冻结,无法交易'); END IF; -- 3. 校验购买下限 IF p_shares < v_buy_limit THEN RAISE_APPLICATION_ERROR(-20004, '购买份数低于下限 ' || v_buy_limit); END IF; -- 4. 计算金额(含 0.75% 手续费),校验可用余额 v_cost := p_shares * v_price * 1.0075; SELECT EnableBalance INTO v_enable FROM FinancingAccount WHERE FinancingAccount = (SELECT FinancingAccount FROM FundAccount WHERE FundAccount = p_fund_account); IF v_enable < v_cost THEN RAISE_APPLICATION_ERROR(-20005, '可用余额不足,需 ' || v_cost); END IF; -- 5. 冻结资金:可用余额减少,冻结资金增加 UPDATE FinancingAccount SET EnableBalance = EnableBalance - v_cost, CongealFund = CongealFund + v_cost WHERE FinancingAccount = (SELECT FinancingAccount FROM FundAccount WHERE FundAccount = p_fund_account); -- 6. 生成合同号并插入购买记录,状态 0 未审核 v_pact_no := 'Z' || LPAD(seq_pact.NEXTVAL, 6, '0'); INSERT INTO FundBuy(PactNo, FinancingAccount, FundNo, Fundname, Fundnumber, BuyDate, State) VALUES(v_pact_no, (SELECT FinancingAccount FROM FundAccount WHERE FundAccount = p_fund_account), p_fund_no, (SELECT FundName FROM Fund WHERE FundNo = p_fund_no), p_shares, SYSDATE, 0); -- 7. 插入交易流水,类型 1 购买,状态 0 未完成 INSERT INTO Trade(PactNo, FinancingAccount, FundNo, FundName, DealType, FundQuotient, BargainPrice, DealMoney, FundAccount, DealDate, Status) VALUES(v_pact_no, (SELECT FinancingAccount FROM FundAccount WHERE FundAccount = p_fund_account), p_fund_no, (SELECT FundName FROM Fund WHERE FundNo = p_fund_no), 1, p_shares, v_price, v_cost, p_fund_account, SYSDATE, 0); END; /这段过程有几个设计点值得展开。手续费 0.75% 是在购买时就算进冻结金额的,但需求明确写了“没有审核前不能扣去这次交易的手续费用”,所以冻结的是含手续费的全额,审核时才真正从总金额里扣。资金冻结用EnableBalance减、CongealFund加,两个字段一减一加保证总额守恒,这是账务类系统的基本功。合同号在过程里用序列生成,虽然 FundBuy 表也有触发器,但这里显式生成更可控,避免触发器和过程逻辑打架。
4.2 赎回与撤单:资金回流的两个方向
赎回的校验比购买少一条下限判断,但多一条“持有份数是否足够”。手续费率也不同,赎回是 1.5%。赎回成功后,基金份数要减掉,等价的资金加到冻结资金里,审核通过才进可用余额。撤单则只允许在购买成功、未审核之前操作,撤单不扣手续费,把冻结的资金原路退回可用余额。
CREATE OR REPLACE PROCEDURE Consign_pack.cancel_order( p_pact_no IN VARCHAR2 ) AS v_state FundBuy.State%TYPE; v_cost Trade.DealMoney%TYPE; v_fin FundBuy.FinancingAccount%TYPE; BEGIN SELECT State, FinancingAccount INTO v_state, v_fin FROM FundBuy WHERE PactNo = p_pact_no; IF v_state = 1 THEN RAISE_APPLICATION_ERROR(-20010, '该笔交易已审核,不能撤单'); END IF; SELECT DealMoney INTO v_cost FROM Trade WHERE PactNo = p_pact_no AND DealType = 1; -- 冻结资金退回可用余额 UPDATE FinancingAccount SET EnableBalance = EnableBalance + v_cost, CongealFund = CongealFund - v_cost WHERE FinancingAccount = v_fin; -- 购买记录置为已撤销(用状态 2 区分),交易流水补一条撤单记录 UPDATE FundBuy SET State = 2 WHERE PactNo = p_pact_no; INSERT INTO Trade(PactNo, FinancingAccount, FundNo, FundName, DealType, FundQuotient, BargainPrice, DealMoney, FundAccount, DealDate, Status) SELECT PactNo, FinancingAccount, FundNo, FundName, 2, Fundnumber, 0, v_cost, NULL, SYSDATE, 1 FROM FundBuy WHERE PactNo = p_pact_no; END; /撤单这里有个容易忽略的点:FundBuy 的 State 字段需求里只定义了 0 未审核、1 审核,撤单后的状态没明说。我一般用 2 表示已撤销,并在答辩时主动说明这个扩展,比含糊带过要好。交易流水里补一条 DealType=2 的撤单记录,保证流水完整可追溯,这也是账务系统审计的基本要求。
4.3 转账与冻结联动:跨账户的状态一致性
转账分活期转理财和理财转活期两个方向,每个方向都要校验密码、余额、冻结状态。更麻烦的是冻结联动规则:冻结活期账户时必须同时冻结理财账户,启用活期账户时不能启用理财账户。这条规则如果只写在应用层,数据库层面就形同虚设,所以要在存储过程里强制。
CREATE OR REPLACE PROCEDURE ClientAccountManager_pack.freeze_current( p_current_acc IN VARCHAR2 ) AS BEGIN UPDATE CurrentAccount SET State = 1 WHERE CurrentAccount = p_current_acc; -- 活期冻结,联动冻结其下所有理财账户 UPDATE FinancingAccount SET State = 1 WHERE CurrentAccount = p_current_acc; END; /反向的启用过程里要加判断:如果活期账户还是冻结状态,理财账户不允许单独启用。这个判断放在UPDATE之前用SELECT查一下活期状态即可。这类联动规则是答辩时的高频提问点,能讲清楚“为什么冻结要级联、启用不能级联”,说明你真的想过业务,而不是照着需求抄。
5. 避坑与排查:这套方案里最容易翻车的五个点
5.1 外键建表顺序错导致 ORA-02291
现象是建表脚本跑到一半报ORA-02291: integrity constraint violated - parent key not found。原因是被引用的父表还没建,或者建了但主键没加。解决办法是按 FundCompany → Fund → CurrentAccount → FinancingAccount → FundAccount → FundBuy → Trade 的顺序建表,每建一张先确认主键约束生效,再建下一张。如果已经建乱了,用DROP TABLE 表名 CASCADE CONSTRAINTS清掉重来,比逐条改外键快。
5.2 触发器与过程重复生成主键
现象是插入数据时报主键冲突,或者编号出现两个不同格式。原因是表上既有BEFORE INSERT触发器生成主键,存储过程里又用序列显式赋值,两边都往:NEW上写。解决办法是二选一:要么触发器里加IF :NEW.主键 IS NULL判断,过程显式赋值时触发器不覆盖;要么过程里不赋值,完全交给触发器。我一般选前者,灵活度高,测试时也能手动指定 ID。
5.3 资金冻结后总额对不上账
现象是购买几笔之后,理财账户的AccountBalance和EnableBalance + CongealFund不相等。原因通常是某条更新语句只改了可用余额没改冻结资金,或者审核时重复扣款。排查方法是写一条对账 SQL:SELECT FinancingAccount, AccountBalance, EnableBalance + CongealFund FROM FinancingAccount WHERE AccountBalance <> EnableBalance + CongealFund,查出来不为空就说明有过程漏了字段。解决靠统一资金变动模板:任何涉及资金的更新,都同时处理EnableBalance和CongealFund,审核通过时再从AccountBalance扣。
5.4 序列跳号与 NOCACHE 的取舍
现象是编号不连续,中间缺了几个。原因是序列默认CACHE 20,实例重启或回滚时会跳号。课设环境数据量小,用NOCACHE能保证连续,但高并发场景下NOCACHE会影响性能。这个取舍要在答辩时讲清楚:课设选NOCACHE是为了演示和核对方便,生产环境一般用缓存换性能,编号连续性靠业务层容忍。能主动说出这个权衡,比被问到时支支吾吾强。
5.5 检查约束报错定位不到字段
现象是插入数据报ORA-02290,但错误信息里只有约束名,不知道是哪个字段。原因是约束命名不规范,用了系统默认的SYS_C00xxxx。解决办法是建表时给每个检查约束显式命名,格式ck_表缩写_字段,比如ck_fund_state、ck_year_rate。这样报错时一眼能定位,调试效率差好几倍。这个习惯我从第一个项目被坑过之后就再没丢过。
6. 验收与答辩:怎么证明这套数据库设计是站得住的
验收这套方案,我一般分三步走,每步都有可量化的检查点,答辩时直接按这个顺序演示。
第一步是结构验收,查数据字典。跑下面这条 SQL,七张表、主键、外键、检查约束、序列、触发器、程序包应该全部列出来,数量对得上才算结构完整:
SELECT object_type, COUNT(*) FROM user_objects WHERE object_type IN ('TABLE','SEQUENCE','TRIGGER','PACKAGE','PACKAGE BODY') GROUP BY object_type;预期结果是 TABLE 7 张、SEQUENCE 6 个、TRIGGER 6 个、PACKAGE 7 个、PACKAGE BODY 7 个。数字对不上就说明有对象没建全,先补齐再往下走。
第二步是业务验收,按五个阶段的功能点逐条跑。基金管理:添加公司、添加基金、冻结解冻、按公司查基金。账户管理:活期开户、理财开户、基金账户开户、三级冻结联动。交易审核:购买后资金进冻结、审核后扣款、撤单退回。委托交易:购买六道校验、赎回份数校验、当日/未完成/历史三类查询。每条功能准备一组正常数据和一组异常数据,异常数据要能触发对应的错误码,比如余额不足报 -20005、基金冻结报 -20001。答辩时先跑正常流程,再故意触发一个异常,展示错误处理,比只跑通流程有说服力。
第三步是数据一致性验收,重点查三本账:资金账、份额账、流水账。资金账用前面那条对账 SQL,AccountBalance必须等于EnableBalance + CongealFund。份额账查FundBuy里未审核和已审核的份数总和,与Trade流水里购买减赎回的净额对得上。流水账查每笔FundBuy是否都有对应的Trade记录,撤单是否有 DealType=2 的流水。这三本账对平,整套设计的严谨性就立住了。
答辩时还有一个技巧:主动讲设计取舍,别等老师问。比如为什么活期和理财账号用 13 位纯数字而基金公司用字母前缀,为什么撤单状态用 2 扩展,为什么序列选 NOCACHE。每个取舍背后都有场景,讲清楚场景比背概念得分高。我带过的项目里,凡是能主动说出“这里我本来想用 X,但考虑到 Y 改成了 Z”的同学,答辩基本不会卡壳。
从那以后我每次交数据库方案,都强制自己先跑一遍对象数量核对和三本账对平,再动手写文档。结构没对平就写实现,等于在沙子上盖楼,返工的成本远高于前期多花的那半小时。希望帮到你。
本文还有配套的精品资源,点击获取