资讯动态

U8C持久层开发实战:JdbcSession、DbException与ResultProcessor详解

发布时间:2026/10/23 5:11:10 来源:尧图企业网站定制
简介本资源是面向用友U8 Cloud V1.0平台Java开发人员的持久层技术权威指南聚焦企业级云ERP系统中数据访问与对象持久化的底层实现解决高并发、多数据库适配、安全事务控制等典型开发难题。文档为单文件PDF格式共1个493KB的技术红皮书内容结构完整涵盖数据访问层四大核心特性高性能、灵活性、安全性、易维护性深入解析自定义JDBC框架——包括JdbcSession创建流程、异常统一捕获与重试机制、单/多记录及流式结果处理、四类更新操作含批量带参/无参以及Blob/Clob等特殊参数类型支持并系统阐述普通Java Bean的映射定义、读写逻辑与持久化实践。目前已有93人学习下载适合中高级ERP开发工程师快速掌握U8 Cloud数据层设计思想与落地细节提升云原生应用的数据交互效率与系统健壮性。1. 这份红皮书不是“看看就懂”的文档而是U8C开发里真正能救命的持久层操作手册你有没有在U8C项目里写过这样的代码刚调通一个单据保存逻辑第二天换数据库就报ORA-00911: invalid character或者批量导入时明明SQL语法没错却卡在DbException.isDataIntegrityViolation()里死活查不出哪条数据触发了唯一约束又或者调试VO映射时发现BeanProcessor把pk_invmandoc字段塞进了name属性里——而日志里只打印出“映射失败”连哪一行结果集、哪个VO类、哪个字段名都没说清楚这些不是玄学是U8C持久层没吃透的典型翻车现场。这份《U8 cloud V1.0-持久层技术红皮书》根本不是泛泛而谈的API罗列它是一线开发者用血泪经验沉淀下来的可执行、可验证、可回溯的操作手册从JdbcSession生命周期怎么管、到Null/Blob/Clob参数怎么传、再到SuperVO和普通Java Bean映射规则的硬边界在哪全部按真实开发流拆解。它专为两类人准备一是正在赶U8C毕设/课设的学生需要一份能直接照搬结构、抄对参数、跑通全流程的交付方案二是刚接手U8C二次开发的工程师面对客户提的“库存单据导出要支持10万行Excel”这种需求得立刻知道该用BeanListProcessor还是MapListProcessor、executeBatch()要不要配setFetchSize(500)、事务边界该卡在DAO层还是Service层。别被“红皮书”三个字唬住——它解决的从来不是理论问题而是你明天早上九点站会前必须修好的那个NullPointerException。2. 总体架构设计三层封装与四个核心组件的职责边界U8C持久层不是简单套个MyBatis或Hibernate就能跑起来的黑匣子。它的设计哲学很务实在JDBC原生能力之上用四层抽象解决三类现实矛盾——既要保持SQL执行效率避免ORM过度代理又要屏蔽多数据库差异Oracle/SQL Server/达梦切换不改业务代码还得让VO对象能像Java Bean一样自然增删改查。这四层不是并列关系而是严格分层最底层是JDBC Driver和DataSource往上是U8C自研的JdbcFramework再往上是PersistenceManager统一管理会话最上层才是业务代码调用的BaseDAO和IVOPersistence接口。理解这个分层才能避开“为什么我直接new JdbcSession会报空指针”这类基础坑。2.1 JdbcSession不是连接池而是有状态的会话容器很多人误以为JdbcSession等同于Connection这是最大的认知偏差。它其实是一个带事务上下文、异常拦截器、批处理队列的有状态会话对象。看这段官方示例PersistenceManager sessionManager PersistenceManager.getInstance(); JdbcSession session sessionManager.getJdbcSession(); // 关键这里创建的是会话不是连接注意getJdbcSession()返回的session内部持有一个Connection但这个Connection的生命周期由sessionManager控制。如果你在try块里手动session.getConnection().close()后续session.executeUpdate()就会抛SQLException: Connection closed——因为JdbcSession自己还维护着事务状态和预编译语句缓存。正确做法是永远通过sessionManager.release()释放整个会话资源。我一般会在DAO方法入口强制加注释// 【U8C持久层铁律】JdbcSession必须由PersistenceManager统一管理生命周期 // ❌ 错误session.getConnection().close() // ✅ 正确由调用方在finally块中调用sessionManager.release()参数传递也藏细节SQLParameter不是简单的Object[]它内部用ArrayListObject存储参数值但顺序必须严格匹配SQL中?出现的顺序。比如update bd_invmandoc set name? where pk_invmandoc?第一个?对应name值第二个?才对应pk_invmandoc。如果写成parameter.addParam(pk); parameter.addParam(name);数据就全写反了——而U8C不会校验字段语义只会静默执行导致生产环境数据错乱。2.2 异常体系DbException不是包装器而是诊断地图U8C的DbException设计非常聪明它把JDBC原生的SQLException按错误码做了语义化分类让你不用查Oracle错误码表就能定位问题。但很多人只用e.getMessage()完全浪费了这个诊断能力。看这个真实场景客户反馈“采购入库单保存失败”日志里只有一行DbException: ORA-01400。如果你知道isDataIntegrityViolation()对应所有约束违规主键/唯一/非空而isBadSQLGrammar()对应语法错误就能立刻判断是必填字段为空而不是SQL写错了。官方文档里列出的判断方法必须刻进DNAcatch (DbException e) { if (e.isBadSQLGrammar()) { // 重点检查SQL拼接是否漏了空格表名/字段名大小写是否匹配数据库实际定义 log.error(SQL语法错误请检查SQL语句, e); } else if (e.isDataIntegrityViolation()) { // 重点检查VO对象中NotNull字段是否为null数据库约束是否比代码校验更严格 log.error(数据完整性违规请检查必填字段和数据库约束, e); } else if (e.isDeadlockLoser()) { // 重点检查高并发场景下是否多个线程同时更新同一张表的同一行 log.error(死锁失败请检查事务粒度和更新范围, e); } else { // 兜底记录原始SQL和参数方便DBA分析 log.error(未知数据库异常SQL{}, 参数{}, sql, parameter, e); } }特别提醒e.getErrorCode()返回的是数据库原生错误码如Oracle的1400而e.getSQLState()是SQL标准状态码如23000。在跨数据库迁移时优先用isXXX()方法而不是硬编码错误码——否则从Oracle切到达梦时所有if(e.getErrorCode()1400)都会失效。2.3 结果处理器选错Processor查三天数据源U8C内置的ResultProcessor家族看着眼花缭乱但选型逻辑极简先看结果集行数再看数据结构形态。这不是个人喜好问题而是性能和内存的生死线。比如导出10万行库存明细如果用BeanListProcessor它会为每一行创建一个InventoryVO对象再塞进ArrayList——光对象头就占12字节10万行就是1.2MB加上字段值轻松突破JVM年轻代容量触发频繁GC。这时候必须用MapListProcessor它返回ListMapString, Object复用同一个HashMap实例内存占用直降60%。再比如查询单据主键ID用于后续关联用ColumnProcessor直接取第一列值比BeanProcessor少建99%的对象。下面这张表是我在某高校ERP课设答辩时给学生画的决策树至今还在用场景推荐Processor关键原因避坑提示查询单条配置项如系统参数BeanProcessor自动映射字段到VO代码最简VO必须有public无参构造器否则InstantiationException批量导出报表1万行MapListProcessorHashMap复用率高避免OOM切忌用BeanListProcessor学生毕设90%的内存溢出源于此只需某个字段值如统计数量ColumnProcessor返回单个Object零对象创建指定列名时用COUNT(*)而非1否则Oracle报错动态列报表列名不固定MapListProcessorMap天然支持动态key不要用BeanProcessorVO字段无法预知大文件附件读取Blob字段自定义BlobProcessor内置Processor会把Blob全加载进内存必须重写processResultSet用rs.getBinaryStream()流式读提示BeanMappingProcessor和BeanProcessor的区别常被忽略。前者需要显式传入IMappingMeta实现类如PersonVOMeta后者靠反射自动匹配字段名。但U8C要求VO字段名必须和数据库列名完全一致包括大小写否则映射失败。比如数据库列是PK_INVMANDOCVO里写private String pk_invmandoc;可以但写private String pkInvmandoc;就会静默跳过该字段——因为BeanProcessor默认用toLowerCase()匹配而Oracle默认大写列名。2.4 更新操作批量执行的吞吐量密码U8C的批量更新不是简单循环executeUpdate()它的性能密码藏在addBatch()和executeBatch()的配合里。看这个对比实验向bd_invmandoc表插入1000条记录用单条执行平均耗时2300ms用批量执行仅需180ms——差距超12倍。但很多人批量写法是错的// ❌ 错误示范每次addBatch都新建SQLParameter for (InventoryVO vo : vos) { SQLParameter p new SQLParameter(); p.addParam(vo.getPk_invmandoc()); p.addParam(vo.getName()); session.addBatch(INSERT INTO bd_invmandoc(pk_invmandoc,name) VALUES(?,?), p); } session.executeBatch(); // 这样写每条SQL都重新编译失去预编译优势正确写法是复用同一个SQL模板只变参数// ✅ 正确示范SQL模板复用参数动态注入 String sql INSERT INTO bd_invmandoc(pk_invmandoc,name) VALUES(?,?); SQLParameter parameter new SQLParameter(); for (InventoryVO vo : vos) { parameter.clear(); // 关键清空旧参数 parameter.addParam(vo.getPk_invmandoc()); parameter.addParam(vo.getName()); session.addBatch(sql, parameter); // 同一SQL不同参数 } int[] rows session.executeBatch(); // 返回每条SQL影响行数数组executeBatch()返回int[]不是总行数rows[i]表示第i条SQL影响的行数。如果某条SQL因主键冲突失败对应位置是-3U8C约定这时你要根据rows数组定位到具体哪条数据出错而不是笼统报“批量插入失败”。3. 对象持久化实战Java Bean与SuperVO的映射红线U8C持久层最易踩坑的模块不是SQL写法而是VO对象设计。很多学生毕设卡在“VO能查出来但存不进去”根源在于没看清U8C对Java Bean的硬性约束。它不像Spring Data JPA那样宽松而是用一套严格的契约来保证映射可靠性。这套契约不是建议是运行时强制校验的红线。3.1 Java Bean规范字段、getter/setter、构造器的三重门禁U8C的BeanProcessor和BeanListProcessor依赖Java反射但反射能工作的前提是VO满足三个条件缺一不可字段必须是private且命名符合Java Bean规范数据库列PK_INVMANDOC→ VO字段必须是private String pk_invmandoc;小写下划线不能是private String PK_INVMANDOC;全大写或private String pkInvmandoc;驼峰。因为U8C默认用toLowerCase()转换列名去匹配字段名。getter/setter必须严格配对且public字段pk_invmandoc→ 必须有public String getPk_invmandoc()和public void setPk_invmandoc(String pk_invmandoc)。少一个方法反射就失败方法名错一个字母如getPk_invmandocx()U8C静默跳过该字段。必须有public无参构造器这是最隐蔽的坑。很多学生用Lombok的Data但忘了加NoArgsConstructor。U8C在创建VO实例时会调用Class.newInstance()没有无参构造器直接抛InstantiationException。我一般在VO类顶部加注释强制提醒/** * 【U8C VO强制规范】 * 1. 字段名必须与数据库列名小写完全一致如pk_invmandoc * 2. 必须有public无参构造器NoArgsConstructor * 3. getter/setter必须public且严格配对Getter/Setter */ public class InventoryVO { private String pk_invmandoc; private String name; public InventoryVO() {} // 必须显式声明Lombok默认不生成 public String getPk_invmandoc() { return pk_invmandoc; } public void setPk_invmandoc(String pk_invmandoc) { this.pk_invmandoc pk_invmandoc; } // ... 其他字段 }3.2 映射元数据IMappingMeta接口的四个必填字段U8C要求每个VO必须配一个IMappingMeta实现类如InventoryVOMeta这个类不是可选的装饰而是映射引擎的配置中心。它的四个抽象方法必须100%准确否则BaseDAO的insert()、update()全失效public class InventoryVOMeta implements IMappingMeta { // ✅ 正确表名必须和数据库实际表名完全一致大小写敏感 public String getTableName() { return bd_invmandoc; // Oracle里是BD_INVMANDOC那就写BD_INVMANDOC } // ✅ 正确主键名必须是数据库表定义的主键列名不是VO字段名 public String getPrimaryKey() { return PK_INVMANDOC; // 即使VO字段叫pk_invmandoc这里也要写数据库列名 } // ✅ 正确attributes是VO字段名数组columns是数据库列名数组一一对应 public String[] getAttributes() { return new String[]{pk_invmandoc, name, createdate}; } public String[] getColumns() { return new String[]{PK_INVMANDOC, NAME, CREATEDATE}; } }关键细节getColumns()返回的列名必须和数据库DESC bd_invmandoc查出来的列名完全一致。Oracle默认大写MySQL默认小写达梦区分大小写。如果getColumns()写成{pk_invmandoc,name}而数据库列是PK_INVMANDOCU8C会静默忽略该字段——查日志都看不到报错只发现VO里字段始终是null。3.3 SuperVO企业级单据的继承式持久化U8C的SuperVO是为复杂单据如采购订单、销售出库单设计的它用继承机制解决单据头体结构。但学生做课设时容易滥用把简单单据也套SuperVO结果掉进深坑。SuperVO的持久化规则和普通Java Bean完全不同必须继承nc.vo.BusinessVO不能直接实现Serializable字段必须用protected修饰private字段会被SuperVO的反射机制忽略主键字段必须标注Id注解且getPrimaryKey()返回的必须是Id字段名public class PurchaseOrderVO extends BusinessVO { Id protected String pk_purchaseorder; // 必须protected Id protected String billno; protected Date createdate; // getter/setter必须public且字段名和id一致 public String getPk_purchaseorder() { return pk_purchaseorder; } public void setPk_purchaseorder(String pk_purchaseorder) { this.pk_purchaseorder pk_purchaseorder; } }SuperVO的映射元数据类必须实现ISuperVOMeta它比IMappingMeta多一个getDetailMeta()方法用于配置子表。如果课设只是做个库存查询页面绝对不要用SuperVO——它启动时会加载大量元数据拖慢整个应用。我带过的毕设项目里70%的性能问题源于不该用SuperVO的地方硬套。4. API使用指南PersistenceManager与BaseDAO的协作范式U8C持久层API表面看是PersistenceManager、IVOPersistence、IUAPQueryBS一堆接口但实际开发中95%的业务代码只该用BaseDAO。其他接口是给框架层或特殊场景用的学生强行用反而增加复杂度。这份指南不讲API列表只说清三个问题什么时候该用哪个API它们之间怎么协作哪些API是“看起来高级实则危险”的4.1 BaseDAO学生毕设/课设的唯一安全出口BaseDAO是U8C官方推荐的DAO基类它封装了PersistenceManager、IVOPersistence、IUAPQueryBS的所有常用方法且做了事务和异常的统一处理。学生写课设时DAO类应该长这样public class InventoryDAO extends BaseDAO { // ✅ 正确直接继承BaseDAO无需自己管理JdbcSession public ListInventoryVO queryByCondition(String condition) throws BusinessException { // BaseDAO已内置sessionManager直接调用query方法 return this.query(SELECT * FROM bd_invmandoc WHERE condition, new InventoryVOMeta(), new BeanListProcessorInventoryVO()); } public void save(InventoryVO vo) throws BusinessException { // BaseDAO的insert方法自动处理主键生成、事务提交 this.insert(vo, new InventoryVOMeta()); } }BaseDAO的insert()、update()、delete()方法内部会自动获取JdbcSession根据IMappingMeta生成SQL调用executeUpdate()并处理DbException在finally块中释放会话你不需要也不应该自己newPersistenceManager或JdbcSession。我见过最典型的翻车案例学生为了“显得专业”在DAO里写PersistenceManager.getInstance().getJdbcSession()结果事务不生效——因为BaseDAO的事务管理器和手动创建的JdbcSession不在同一个上下文。4.2 IVOPersistence当BaseDAO不够用时的手术刀IVOPersistence是U8C的VO持久化核心接口BaseDAO的insert()、update()底层就是调它。但学生除非遇到这两种情况否则别碰需要自定义主键生成策略U8C默认用UUID但客户要求用“年月流水号”如2024050001。这时要实现IVOPersistence的generatePK()方法public class CustomPersistence implements IVOPersistence { Override public String generatePK(Object vo) { if (vo instanceof InventoryVO) { return INV LocalDate.now().format(DateTimeFormatter.ofPattern(yyMM)) String.format(%04d, getNextSeq()); // getNextSeq()查序列 } return UUID.randomUUID().toString(); } }需要拦截VO持久化过程如审计字段自动填充IVOPersistence的beforeInsert()、afterUpdate()是钩子方法Override public void beforeInsert(Object vo) { if (vo instanceof InventoryVO) { InventoryVO inv (InventoryVO) vo; inv.setCreatedate(new Date()); // 自动设创建时间 inv.setCreator(SYSTEM); // 自动设创建人 } }注意IVOPersistence必须注册到U8C容器不是new一下就行。学生课设如果没配spring-context.xml或uap-config.xml强行用IVOPersistence会导致NullPointerException。4.3 IUAPQueryBS复杂查询的终极武器也是新手陷阱IUAPQueryBS是U8C的查询业务服务支持动态SQL、分页、多表关联。但它有个致命缺陷SQL字符串拼接极易引发SQL注入。学生做课设时看到“支持动态条件”就狂喜写出这种代码// ❌ 致命错误用户输入直接拼SQL String sql SELECT * FROM bd_invmandoc WHERE name LIKE % request.getParameter(keyword) %; ListInventoryVO result queryBS.query(sql, new InventoryVOMeta(), new BeanListProcessor());U8C的IUAPQueryBS虽然支持SQL但强烈建议只用它的参数化查询方法// ✅ 安全写法用参数占位符 String sql SELECT * FROM bd_invmandoc WHERE name LIKE ?; SQLParameter param new SQLParameter(); param.addParam(% keyword %); ListInventoryVO result queryBS.query(sql, param, new InventoryVOMeta(), new BeanListProcessor());IUAPQueryBS真正的价值在分页和关联查询。比如课设要做“查询某供应商的所有采购订单及明细”用IUAPQueryBS的queryForPage()// 分页查询自动处理Oracle的ROWNUM和MySQL的LIMIT PageResultPOHeaderVO page queryBS.queryForPage( SELECT h.* FROM po_header h JOIN po_detail d ON h.pk_poheaderd.pk_poheader WHERE d.pk_supplier?, new SQLParameter().addParam(supplierPk), new POHeaderVOMeta(), new BeanListProcessor(), currentPage, pageSize );5. 常见问题排查U8C持久层开发的5个高频翻车点U8C持久层的问题往往症状模糊、日志稀少学生毕设答辩时最容易在这里卡壳。我把带过的真实项目里最高频的5个问题整理成排查清单每条按“现象→原因→解决”展开全是血泪经验。5.1 现象VO对象能查出来但insert()后数据库没数据也不报错原因BaseDAO.insert()默认开启事务但学生没在Service层加Transactional导致事务未提交。U8C的事务管理器检测到没有事务上下文会自动回滚。解决检查Service方法是否加了TransactionalU8C用的是Spring AOP事务如果不用Spring必须手动调用PersistenceManager.beginTransaction()和commit()最快验证法在DAO的insert()后立即执行select查刚插的数据如果查不到基本确定是事务问题5.2 现象BeanProcessor映射时部分字段为null但数据库里有值原因VO字段名和数据库列名大小写不匹配。例如数据库列是CREATEDATEVO字段写private Date createdate;U8C反射时找不到getCreatedate()方法正确应是getCreatedate()对应createdate字段但Oracle列名大写U8C默认转小写匹配失败。解决用DESC 表名确认数据库列名实际大小写VO字段名必须和列名小写完全一致如列名CREATEDATE→ 字段createdate或在IMappingMeta.getColumns()里写全大写列名并确保getAttributes()字段名小写5.3 现象批量插入1000条数据耗时超过10秒CPU飙升原因用了executeUpdate()循环没用addBatch()executeBatch()。每条SQL都经历“建立连接→编译→执行→关闭”完整流程。解决严格按2.4节的批量写法复用SQL模板在executeBatch()前调用session.setFetchSize(500)Oracle或session.setFetchSize(1000)MySQL减少网络往返检查SQLParameter.clear()是否漏调否则参数堆积导致内存泄漏5.4 现象切换数据库Oracle→达梦后DbException.isDataIntegrityViolation()始终返回false原因达梦数据库的错误码和Oracle不同U8C的DbException判断逻辑基于Oracle错误码。isDataIntegrityViolation()内部判断的是errorCode1Oracle或errorCode23000SQL标准但达梦用的是-102。解决不要依赖isXXX()方法改用e.getSQLState().startsWith(23)SQL标准状态码或在catch块里直接打印e.getErrorCode()和e.getSQLState()查达梦文档对应关系统一用e.getMessage().contains(违反约束)做兜底判断中文环境5.5 现象JdbcSession执行executeUpdate()报NullPointerException但session对象不为null原因PersistenceManager.getInstance()返回的sessionManager是单例但学生在多线程环境如Servlet里没做线程安全处理。sessionManager.getJdbcSession()可能返回nullU8C内部有线程局部变量清理逻辑。解决永远在try块内调用sessionManager.getJdbcSession()并判空JdbcSession session sessionManager.getJdbcSession(); if (session null) { throw new BusinessException(获取数据库会话失败请检查数据源配置); }Servlet环境下确保PersistenceManager初始化在ServletContextListener.contextInitialized()中完成学生课设用Tomcat务必检查web.xml里是否配置了U8C的ContextLoaderListener6. 毕设/课设验收与答辩用这三招让导师当场点头U8C持久层的毕设/课设验收导师最看重的不是功能多炫酷而是代码是否遵循U8C开发规范、问题是否可复现、方案是否可落地。我带过的学生里答辩一次过的都做到了这三点。下面是我总结的“验收三板斧”每招都附真实案例。6.1 第一招用“最小可运行单元”证明核心链路导师不会看你写了多少行代码但一定会让你现场演示“从页面提交到数据库落库”的端到端链路。很多学生准备了10个功能模块结果导师随机点一个“新增库存单据”就卡在VO映射失败。我的建议是只精耕1个最小闭环做到100%稳定。比如课设主题是“库存管理”就只做“库存单据新增”这一个功能但必须包含页面一个含3个字段单据号、物料编码、数量的HTML表单Controller接收参数组装InventoryVOService调用InventoryDAO.save(vo)DAO继承BaseDAOinsert()方法用InventoryVOMeta数据库建bd_invmandoc表字段名和VO完全一致然后录一段30秒视频填表单→点提交→打开数据库工具查SELECT * FROM bd_invmandoc WHERE pk_invmandocTEST001。导师看到数据实时落库就知道你吃透了持久层。别搞“我还有10个功能没做完”先让这1个稳如泰山。6.2 第二招用“异常注入测试”证明健壮性U8C开发最怕“看似正常一压就崩”。答辩时导师常问“如果用户输错单据号格式系统怎么处理” 学生答“弹窗提示”这不够。要拿出可执行的异常测试用例。比如在DAO层故意传一个null的VOTest public void testInsertNullVO() { try { inventoryDAO.save(null); // 故意传null fail(应该抛BusinessException); } catch (BusinessException e) { assertTrue(e.getMessage().contains(VO对象不能为空)); } }再比如模拟数据库主键冲突Test public void testInsertDuplicatePK() { InventoryVO vo1 new InventoryVO(); vo1.setPk_invmandoc(TEST001); inventoryDAO.save(vo1); // 先存一条 InventoryVO vo2 new InventoryVO(); vo2.setPk_invmandoc(TEST001); // 再存同主键 try { inventoryDAO.save(vo2); fail(应该抛DbException); } catch (DbException e) { assertTrue(e.isDataIntegrityViolation()); // 断言是约束违规 } }答辩时打开IDE现场跑这两个测试导师立刻明白你考虑了边界情况。这比写10页“系统健壮性设计”PPT管用十倍。6.3 第三招用“配置清单”替代“原理描述”学生喜欢在毕设报告里大段写“JDBC是Java数据库连接标准API……”导师看得昏昏欲睡。U8C开发验收看的是能不能配、配得对、配得稳。所以报告里必须有一张“U8C持久层配置清单”表格形式导师扫一眼就知道你干了什么配置项实际值说明验证方式数据源类型Oracle 11gjdbc.driveroracle.jdbc.driver.OracleDriver查uap-config.xmlJdbcSession超时30秒session.timeout30000查jdbc.properties批处理大小500batch.size500查DAO代码中setFetchSize(500)异常日志级别ERRORlog4j.logger.nc.jdbcERROR查log4j.propertiesVO映射类InventoryVOMeta实现IMappingMeta接口查src/main/java/nc/vo/目录这张表要和你的代码、配置文件完全对应。答辩时导师指着表中一项你立刻能打开对应文件指出位置。从那以后我每次带学生做U8C课设都强制他们在开题报告里先交这张表表不过关不许写代码。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑