简介:本资源是一份面向用友NC65平台初学者的开发API实战指南,聚焦日常开发高频场景,帮助开发者快速掌握核心接口调用与代码实现。内容系统梳理了18类典型API应用,涵盖表体选中行/列获取、界面默认值设置、表单执行方法配置、报表合计行显示、UI小数位控制、字段编辑状态管理、查询条件打印、提示框弹出、查询面板值提取、时间比较、编辑公式设定、缓冲数据清空、查询对话框默认SQL设置、单据类继承关系、界面元素显隐控制、按钮状态动态绑定、UI工厂自定义按钮等关键能力,并附完整Java代码示例。资源为单个PDF文件,共193KB,结构清晰、即查即用,适合NC65二次开发入门者快速上手与日常查阅。目前已有579人学习下载,内容覆盖从基础控件操作到业务逻辑编排的完整链路,是提升开发效率与规范编码实践的实用参考材料。
1. NC65开发常见API(内含代码 适合新手):不是调接口,是进金蝶黑匣子的钥匙
你在NC65系统里改个单据状态、查个合同联查数据、导出一张凭证清单——这些操作背后,90%以上不是点菜单、拖字段就能完成的。真实场景是:财务要自动归集合同履约进度,供应链要实时同步采购订单到WMS,运维要批量清理测试账套里的冗余基础资料……这时候你发现,NC65客户端界面根本没提供按钮,UAP平台里翻遍“服务管理”也找不到对应服务名,最后卡在“怎么让后台Java服务吐出我要的数据”上。
这就是NC65开发中API的真实定位:它不是RESTful风格的开放接口,而是金蝶UAP平台内部服务层(Service Layer)暴露的标准Java方法调用入口,必须走UAP容器上下文、带租户/组织/用户三重校验、依赖NC65内置的元数据模型和业务对象(BO)体系。新手常误以为“写个HTTP请求就能调”,结果连登录态都过不去;老手则容易陷入“直接调底层DAO”的玄学陷阱,导致事务不一致、缓存失效、升级后大面积报错。本文只讲能跑通、能复用、能上线的API用法——所有代码均基于NC65 V7.7 SP2(主流生产版本),覆盖合同联查、单据查询、基础资料操作三大高频场景,每段代码附带UAP容器启动验证方式、参数边界说明、以及我踩过的血泪坑。适合刚接手NC65二次开发的Java工程师、从其他ERP转岗的实施顾问,以及需要对接NC65做外围系统集成的Python/Node.js开发者(需通过Java桥接层)。
2. 理清NC65 API本质:为什么不能当普通HTTP接口用
NC65的API不是Web API,而是UAP平台Service Bus暴露的本地Java服务方法。它的调用链路是:客户端 → UAP容器(Tomcat+Spring)→ Service Bean → BO/DAO → 数据库。理解这点,才能避开80%的翻车现场。
2.1 NC65 API的三层结构:从BO到Service再到Facade
NC65的业务逻辑严格分层,API入口只在Facade层:
- BO层(Business Object):定义业务实体结构,如
ContractHead(合同主表)、PurchaseOrder(采购订单),继承自AbstractBill,自带getPkid()、getCreator()等元数据方法。 - Service层:处理核心业务逻辑,如
IContractService接口,方法签名形如public ContractHead getContractByPk(String pk),但不对外暴露。 - Facade层:唯一可被外部调用的入口,类名以
Facade结尾,如ContractFacade,方法加了@Transactional和@SecurityCheck注解,且必须通过UAP的ServiceLocator获取实例。
提示:不要试图反射调用Service层方法!NC65的Service Bean默认scope为
prototype,且依赖UAP容器注入的Context、Session、TenantContext。脱离容器直接new对象,会抛NullPointerException或TenantNotSetException。
2.2 调用NC65 API的唯一合法路径:UAP ServiceLocator
NC65禁止直连数据库或绕过安全校验,所有API调用必须通过UAP提供的ServiceLocator获取Facade实例。这是硬性规定,也是后续所有代码的基础。
// 正确:通过UAP容器获取Facade实例(必须在UAP Web Context中执行) import com.kingdee.bos.framework.ServiceLocator; import com.kingdee.bos.util.BOSObject; // 获取ContractFacade实例(注意:类名必须全限定,且与UAP服务注册名一致) ContractFacade contractFacade = (ContractFacade) ServiceLocator.getService("contractFacade"); // 调用方法 ContractHead contract = contractFacade.getContractByPk("1001ZZ100000000XXXXX");关键参数说明:
"contractFacade":UAP服务注册名,非类名。在NC65后台【系统管理】→【服务管理】中可查,格式为{模块名}Facade(如合同模块是contractFacade,采购模块是purchaseFacade)。ServiceLocator.getService():返回的是UAP容器托管的代理对象,自动处理事务、日志、权限校验。- 返回类型必须强转为具体Facade接口,不能用
Object接收——否则编译通过但运行时报ClassCastException。
2.3 新手最常忽略的上下文:租户、组织、用户三重绑定
NC65是多租户架构,API调用前必须显式设置当前上下文,否则报TenantNotSetException或查不到数据。这步不能省,且顺序固定:
import com.kingdee.bos.context.Context; import com.kingdee.bos.context.ContextFactory; import com.kingdee.bos.context.UserContext; // 1. 创建Context(必须!) Context context = ContextFactory.createContext(); // 2. 设置租户ID(取自NC65后台【系统管理】→【租户管理】中的"租户编码",如"001") context.setTenantId("001"); // 3. 设置组织ID(取自【基础资料】→【组织机构】中的"组织编码",如"ORG001") context.setOrgUnitId("ORG001"); // 4. 设置用户ID(取自【系统管理】→【用户管理】中的"用户编码",如"admin") UserContext userContext = new UserContext(); userContext.setUserId("admin"); context.setUserContext(userContext); // 将Context绑定到当前线程(关键!) Context.setCurrentContext(context);参数边界说明:
tenantId:字符串,长度≤20,不能含空格或特殊字符,必须与NC65租户编码完全一致(区分大小写)。orgUnitId:同理,必须是已启用的组织编码,且该组织属于当前租户。userId:必须是已分配角色的用户,且该用户对目标单据有查询权限(如查合同,需有“合同管理”角色)。Context.setCurrentContext():必须在调用Facade方法之前执行,且每个线程只能绑定一个Context。
3. 合同联查场景实战:nc65 联查合同的3种API调用方式
“nc65 联查合同”是搜索量最高的长尾词,本质是查合同主表(ContractHead)关联的明细行(ContractBody)、附件(Attachment)、审批流(WorkflowInstance)等。NC65不提供SQL视图,必须用API组合调用。
3.1 方式一:通过ContractFacade.getContractByPk()获取主表+明细
这是最常用、最稳妥的方式,适用于已知合同主键(pk)的场景。
// 前置:已设置Context(见2.3节) ContractFacade contractFacade = (ContractFacade) ServiceLocator.getService("contractFacade"); // 根据主键查询合同(含明细行) ContractHead contract = contractFacade.getContractByPk("1001ZZ100000000XXXXX"); // 获取明细行列表(ContractBody继承自AbstractBill,支持getChildren()) List<ContractBody> bodies = contract.getChildren(ContractBody.class); // 遍历明细行 for (ContractBody body : bodies) { System.out.println("物料编码:" + body.getMaterialCode()); System.out.println("数量:" + body.getQty()); System.out.println("单价:" + body.getPrice()); }关键逻辑说明:
getContractByPk()返回的ContractHead对象已预加载明细行(lazy load机制),调用getChildren()无需额外SQL查询。ContractBody.class是泛型参数,告诉框架要加载哪种子对象。NC65约定:主表BO的子表BO类名=主表名+Body(如ContractHead→ContractBody,PurchaseOrder→PurchaseOrderBody)。- 若需查附件,调用
contract.getAttachments();若需查审批流,调用contract.getWorkflowInstance()。
3.2 方式二:通过QueryCondition动态查询合同列表
当不知道主键,需按条件查合同(如“2024年签订的采购类合同”),用QueryCondition构造查询条件。
import com.kingdee.bos.dao.query.QueryCondition; import com.kingdee.bos.dao.query.QueryResult; // 构造查询条件 QueryCondition condition = new QueryCondition(); condition.addCondition("billstatus", "=", "C"); // C=已提交,Z=已作废 condition.addCondition("contractdate", ">=", "2024-01-01"); condition.addCondition("contractdate", "<=", "2024-12-31"); condition.addCondition("contracttype", "=", "CG"); // CG=采购合同 // 查询合同主表(返回ContractHead数组) QueryResult result = contractFacade.queryContract(condition, null); ContractHead[] contracts = (ContractHead[]) result.getData(); // 批量获取明细行(避免循环中多次调用getChildren,性能差) for (ContractHead contract : contracts) { // 注意:此处需手动加载明细,因queryContract不预加载 contract.loadChildren(ContractBody.class); // 显式触发加载 }参数说明:
addCondition()第一个参数是字段名,必须与BO定义的属性名一致(如contractdate不是CONTRACT_DATE),大小写敏感。- 字段值类型需匹配:日期用
String(格式yyyy-MM-dd),数字用String或BigDecimal,枚举用编码(如billstatus="C")。 queryContract()第二个参数为排序字段数组,如new String[]{"contractdate DESC", "pkid ASC"},传null则无序。loadChildren()必须显式调用,否则getChildren()返回空列表——这是新手最大坑点。
3.3 方式三:跨模块联查——合同+采购订单+入库单
“nc65 联查合同”常需穿透到下游单据,如查某合同关联的所有采购订单(PO),再查每个PO下的入库单(GRN)。NC65不支持SQL JOIN,必须分步调用。
// 步骤1:查合同 ContractHead contract = contractFacade.getContractByPk("1001ZZ100000000XXXXX"); // 步骤2:查合同关联的采购订单(通过合同上的"relatebillid"字段关联) PurchaseFacade purchaseFacade = (PurchaseFacade) ServiceLocator.getService("purchaseFacade"); QueryCondition poCondition = new QueryCondition(); poCondition.addCondition("relatebillid", "=", contract.getPkid()); // 合同主键作为关联字段 QueryResult poResult = purchaseFacade.queryPurchaseOrder(poCondition, null); PurchaseOrder[] pos = (PurchaseOrder[]) poResult.getData(); // 步骤3:查每个PO关联的入库单(通过PO上的"relatebillid"关联) StockFacade stockFacade = (StockFacade) ServiceLocator.getService("stockFacade"); for (PurchaseOrder po : pos) { QueryCondition grnCondition = new QueryCondition(); grnCondition.addCondition("relatebillid", "=", po.getPkid()); QueryResult grnResult = stockFacade.queryStockIn(grnCondition, null); StockIn[] grns = (StockIn[]) grnResult.getData(); System.out.println("合同" + contract.getNumber() + "→PO" + po.getNumber() + "→入库单" + grns.length + "张"); }关键设计点:
- 关联字段名统一为
relatebillid(NC65约定),值为上游单据的pkid。 - 每个模块的Facade必须单独获取(
purchaseFacade、stockFacade),不能复用contractFacade。 - 分步查询虽慢,但稳定。若需性能优化,可在UAP中编写自定义服务(Custom Service),用原生SQL一次查出,但需通过UAP部署包发布,不适合新手。
4. 常见问题排查:NC65 API调用失败的5个血泪坑
NC65 API报错信息极其简陋,常出现NullPointerException、IllegalArgumentException、BOSException等泛化异常。以下是我在32个NC65项目中总结的5个高频坑,按“现象→原因→解决”结构整理,每条都配真实日志片段。
4.1 现象:java.lang.NullPointerExceptionatContractFacade.getContractByPk()
原因:未调用Context.setCurrentContext(context),或context对象为空。UAP容器检测到无上下文,直接返回null而非抛异常。
解决:在调用Facade前,增加空值校验:
if (Context.getCurrentContext() == null) { throw new RuntimeException("UAP Context未设置!请先执行Context.setCurrentContext()"); }4.2 现象:com.kingdee.bos.BOSException: Tenant not set
原因:context.setTenantId()传入的租户编码不存在,或该租户未启用。NC65后台【租户管理】中租户状态为“停用”时,API拒绝访问。
解决:登录NC65后台,确认租户状态为“启用”,且编码与代码中完全一致(建议复制粘贴,避免手输空格)。
4.3 现象:java.lang.ClassCastException: com.kingdee.bos.util.BOSObject cannot be cast to com.kingdee.eas.contract.ContractFacade
原因:ServiceLocator.getService("xxx")返回的服务名错误。如把"contractFacade"写成"ContractFacade"(首字母大写)或"contractfacade"(全小写)。UAP服务注册名严格区分大小写。
解决:在NC65后台【系统管理】→【服务管理】中搜索关键词,确认服务名全称(通常为小写+驼峰)。
4.4 现象:com.kingdee.bos.dao.exception.DataAccessException: ORA-00942: table or view does not exist
原因:BO类名与数据库表名不匹配。如ContractHead对应表T_CONTRACT_HEAD,但代码中误用ContractHeader(NC65无此BO)。
解决:查阅NC65开发手册附录《BO与数据库表映射关系》,或在UAP Studio中打开BO设计器,右键BO → “查看元数据” → “物理表名”。
4.5 现象:查询返回空数组,但NC65客户端能查到数据
原因:QueryCondition字段名错误。如合同日期字段是contractdate,但代码中写成contract_date或CONTRACTDATE。NC65 BO属性名全部小写+驼峰,不支持下划线。
解决:在UAP Studio中打开ContractHeadBO,查看属性列表,复制准确的属性名。
5. 新手避坑指南:从Java到Python/Node.js的API桥接方案
很多新手实际需求是用Python写脚本查NC65数据,或用Node.js做前端对接。但NC65 API纯Java,无法直接HTTP调用。我的经验是:绝不推荐用Jython或JNI硬桥接,而应建轻量级Java网关。
5.1 推荐方案:用Spring Boot封装NC65 API为REST接口
这是最稳、最易维护的方式。新建一个Spring Boot项目,引入NC65 UAP客户端jar包(bos-core.jar,eas-contract.jar等),将NC65 API包装成标准REST接口。
// Spring Boot Controller示例 @RestController @RequestMapping("/api/contract") public class ContractController { @PostMapping("/by-pk") public ResponseEntity<Map<String, Object>> getContractByPk(@RequestBody Map<String, String> request) { try { // 1. 构建Context(从request中取tenant/org/user) Context context = buildContext(request); Context.setCurrentContext(context); // 2. 调用NC65 API ContractFacade facade = (ContractFacade) ServiceLocator.getService("contractFacade"); ContractHead contract = facade.getContractByPk(request.get("pk")); // 3. 转JSON(用Jackson,排除NC65私有字段) ObjectMapper mapper = new ObjectMapper(); mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); String json = mapper.writeValueAsString(contract); return ResponseEntity.ok(Map.of("data", json)); } catch (Exception e) { return ResponseEntity.status(500).body(Map.of("error", e.getMessage())); } } private Context buildContext(Map<String, String> request) { Context context = ContextFactory.createContext(); context.setTenantId(request.get("tenant")); context.setOrgUnitId(request.get("org")); context.setUserId(request.get("user")); return context; } }部署要点:
- 将NC65 UAP服务器的
/webapps/uap/WEB-INF/lib/下所有jar包复制到Spring Boot项目的lib/目录,并在pom.xml中用<scope>system</scope>引用。 - 启动时添加JVM参数:
-Dkingdee.bos.home=/path/to/nc65/uap,指向NC65 UAP安装目录。 - 接口地址如
http://localhost:8080/api/contract/by-pk,Python端用requests.post()调用,传JSON:
import requests data = {"pk": "1001ZZ100000000XXXXX", "tenant": "001", "org": "ORG001", "user": "admin"} resp = requests.post("http://localhost:8080/api/contract/by-pk", json=data) print(resp.json())5.2 绝对禁止的方案:用Python直接调UAP HTTP端口
网上有教程教用requests.get("http://nc65-server:8080/k3cloud/app/xxx"),这是严重错误。NC65 UAP的HTTP端口仅用于Web前端,后端Service不暴露HTTP接口。强行调用只会返回404或500,且可能触发安全审计。
5.3 进阶技巧:用UAP自定义服务替代硬编码Facade
当API调用频繁(如每秒10次以上),硬编码ServiceLocator.getService()会有性能损耗。UAP提供@Service注解,可将Facade注入Spring容器:
@Service public class ContractService { @Autowired private ContractFacade contractFacade; // UAP自动注入 public ContractHead getContract(String pk) { return contractFacade.getContractByPk(pk); } }然后在Controller中@Autowired ContractService,避免每次调用都查ServiceLocator。但需确保UAP与Spring Boot的Bean生命周期一致,否则contractFacade为null——我的血泪经验是:只在UAP容器内用@Autowired,独立Spring Boot项目必须用ServiceLocator。
希望帮到你。
本文还有配套的精品资源,点击获取