HarmonyOS WPS Open SDK 实践:办公审批附件「打开—编辑—回传」代码实现

做鸿蒙办公审批时,附件编辑链路最容易散:有人只写了 OpenFileRequest,忘记先 registerApp;有人加了水印却没关分享;有人配置了回传,测试却只「切回应用」从不关窗,然后报 SDK 没回调。本文按工程视角把 WPS Open SDK 收成三个模块:注册、策略打开、回传落盘。叙述偏短,代码尽量完整,并插入三张关键片段截图,方便联调时对照工程屏幕。读者需已完成 @wps/wps_sdk HAR 集成,下文不再重复依赖声明。我这几天把审批、合同、公文三类附件都在同一套封装里跑通了:差异只在水印文案和是否开修订,调用时序完全一致,后面接手的同事不容易把注册逻辑又抄到页面按钮里。

接口时序(先画清再写代码)

scss 复制代码
Application.onCreate
  → await ensureRegistered(ctx)
Index 按钮
  → copyPickerToSandbox
  → openWithOfficePolicy (WaterMark + extraOptions + TransferType.URI)
  → 用户关闭 WPS 窗口
  → copyCallbackUri → uploadQueue

sendRequest 在未注册时会 reject ;注册后打开失败走 result.code(常见 ERROR=-2、鉴权 1013)。日志建议固定打 requestType/code/msgappKey/appSecretbundleName 绑定,调试包和上架包名不同就要分别申请,否则在预发环境正常、生产突然 1013 的坑会反复出现。

政企审批对「关窗后的文件」语义要求很死:UI 不能在 data 为空时提示「已同步」,否则业务同学会以为服务器已有最新稿。把「打开成功」和「回传成功」拆成两个状态,能省很多扯皮。

模块 1:注册与 Result 断言

把鉴权与「是否注入激活序列号」收敛到 ensureRegistered。对接文档推荐注册成功后用 setWpsFileToken 全局注入;不要每次打开再塞 OpenFileRequest.wpsToken

typescript 复制代码
import { common } from '@kit.AbilityKit';
import {
  WPSApi, RegisterAppRequest, Result, ResultCode,
} from '@wps/wps_sdk';

export let wpsReady = false;

export function logResult(tag: string, r: Result) {
  console.info(`[${tag}] type=${r.requestType} code=${r.code} msg=${r.msg ?? ''}`);
}

export function assertOk(r: Result, stage: string) {
  if (r.code === ResultCode.ERROR_CODE_AUTH_FAILURE) {
    throw new Error(`${stage} 1013: ${r.msg ?? ''}`);
  }
  if (r.code === ResultCode.ERROR) {
    throw new Error(`${stage} ERROR: ${r.msg ?? 'check path/params'}`);
  }
  if (r.code !== ResultCode.OK) {
    throw new Error(`${stage} code=${r.code}`);
  }
}

export async function ensureRegistered(ctx: common.UIAbilityContext) {
  if (wpsReady) return;

  const r = await WPSApi.sendRequest(
    new RegisterAppRequest(ctx, APP_KEY, APP_SECRET)
  );
  logResult('register', r);
  assertOk(r, 'register');

  if (ACTIVATION_SN) {
    WPSApi.setWpsFileToken(ACTIVATION_SN);
  }
  wpsReady = true;
}

激活序列号只在有申请材料时注入。冷启动放在 EntryAbility.onCreate;首页在 wpsReady 前禁用打开按钮,避免冷启动连点触发 reject。可为 assertOk 写单测:Mock 1013、ERROR、OK,减少手测依赖。凭据走 flavor rawfile 或 CI 注入,Release 日志不打 secret。

模块 2:策略打开(可编辑 + 水印 + 关外发 + 回传)

从 DocumentPicker 拿到的路径先拷到沙箱,再构造策略请求。下图是参数赋值的骨架,便于对照本机编辑器:

typescript 复制代码
import fs from '@ohos.file.fs';
import {
  OpenFileRequest, OpenFileExtraOptions, WaterMark,
  TransferType, WPSApi,
} from '@wps/wps_sdk';

export function toSandbox(ctx: common.UIAbilityContext, src: string): string {
  const dir = `${ctx.filesDir}/wps_in`;
  fs.mkdirSync(dir, true);
  const dest = `${dir}/${Date.now()}_${src.split('/').pop() ?? 'file.docx'}`;
  fs.copyFileSync(src, dest);
  return dest;
}

export function buildPolicyRequest(
  ctx: common.UIAbilityContext,
  path: string,
  watermarkText: string
): OpenFileRequest {
  const req = new OpenFileRequest(ctx, path);
  req.enableEdit = true;
  req.enableLocalization = false;
  req.wpsTransferType = TransferType.URI;

  const wm = new WaterMark();
  wm.Enable = true;
  wm.WaterMaskText = watermarkText;
  wm.Angle = -30;
  wm.FontColor = '#19000000';
  wm.FontSize = 16;
  req.wpsWaterMarkParams = wm;

  const opt = new OpenFileExtraOptions();
  opt.enableShare = false;
  opt.enableCloud = false;
  opt.enablePrint = false;
  opt.enableExport = false;
  opt.enableScreenShot = false;
  opt.enableOpenWithExternalApp = false;
  req.extraOptions = opt;
  return req;
}

export async function openWithOfficePolicy(
  ctx: common.UIAbilityContext,
  sandboxPath: string,
  watermarkText: string
) {
  await ensureRegistered(ctx);
  const req = buildPolicyRequest(ctx, sandboxPath, watermarkText);
  const r = await WPSApi.sendRequest(req);
  logResult('open', r);
  assertOk(r, 'open');
  return r;
}

enableEdit 默认只读,办公编辑必须显式 true。不落地模式下部分能力会被强制关闭,联调时以真机菜单为准;需要放开打印时先评估 enableLocalization = true,再调 extraOptions。联调建议分层叠加:可编辑 → 不落地 → 功能开关 → 水印 → 回传,避免一次堆满参数后 ERROR 无法归因。

修订场景可同请求配置:

typescript 复制代码
import { Revision } from '@wps/wps_sdk';

const rev = new Revision();
rev.UserName = CURRENT_USER_NAME;
rev.EnterReviseMode = true;
rev.ShowRevisionPanel = true;
req.wpsRevisionParams = rev;

水印文案建议拼「工号|部门|密级」,审计截图时能直接对到人。FontColor 带透明度,避免压字看不清正文。

模块 3:关窗后拿 filePath

typescript 复制代码
import fileuri from '@ohos.file.fileuri';

export function copyWpsUri(ctx: common.UIAbilityContext, wpsUri: string): string {
  const f = fs.openSync(wpsUri, fs.OpenMode.READ_ONLY);
  const dir = `${ctx.filesDir}/wps_callback`;
  fs.mkdirSync(dir, true);
  const dest = `${dir}/${Date.now()}.docx`;
  fs.copyFileSync(f.fd, dest);
  fs.closeSync(f);
  return fileuri.getUriFromPath(dest);
}

export async function editAttachmentAndCollect(
  ctx: common.UIAbilityContext,
  pickerPath: string
): Promise<string | undefined> {
  const sandbox = toSandbox(ctx, pickerPath);
  const stamp = `${USER_ID}|${DEPT}|审批附件`;
  const r = await openWithOfficePolicy(ctx, sandbox, stamp);
  if (!r.data?.fileUri) {
    return undefined;
  }
  return copyWpsUri(ctx, r.data.fileUri);
}

切回应用 ≠ 关窗。OK && !data 是中间态,不是失败。FD 模式可把 wpsTransferType 换成 TransferType.FD,读 transferFd 并校验 transferFileSize。上传应排队:网络成功后再改业务附件版本号,避免关窗瞬间 UI 先报成功。

页面侧拼装与联调

typescript 复制代码
// EntryAbility.onCreate
await ensureRegistered(this.context);

async function onTapEdit() {
  try {
    if (!wpsReady) return;
    const picked = await selectOneDocument();
    const appUri = await editAttachmentAndCollect(hostContext, picked);
    if (appUri) {
      await uploadQueue.push(appUri);
    }
  } catch (e) {
    promptAction.showToast({ message: String(e) });
  }
}

清单:reject(未注册)、1013(凭据)、ERROR(路径)、只能预览(enableEdit)、外发菜单(不落地/extraOptions)、无 data(未关窗)、换 HAR 后 clean、多 flavor 校验 bundleName。三模块测通后,业务增量通常只在水印文案与上传接口。弱网或政企内网下建议单独回归 registerApp 超时;系统或 WPS 客户端升级后,再抽测一小一大两个附件的 URI 回传。单元测试可以对 assertOkcopyWpsUri 用 Mock Result,覆盖空 data、仅 URI、拷贝失败等分支,减少真机连点成本。

若后续要做断点续传,只需替换上传层;注册与策略打开模块可以保持不动,回归范围也会小很多。字段含义仍以官方对接文档为准,文档更新后对照 OpenFileRequest/ResultData 表扫一遍即可。

小结

稳定接入靠时序,不靠在页面里堆 if。把 注册 → 策略打开 → 关窗 copy → 上传 收成模块,审批附件编辑就能在鸿蒙端稳定复用。更多字段以官方对接文档为准。


基于 WPS Open SDK 鸿蒙版实践整理。

官方对接文档:365.kdocs.cn/l/clQl5cek2...

QQ 群:628436767

相关推荐
Flynt2 小时前
TypeScript 5.8 让我少踩了一个大坑,顺手再聊聊 --erasableSyntaxOnly
前端·javascript·typescript
ZZZMMM.zip2 小时前
基于鸿蒙PC与鸿蒙Flutter框架构建AI本地生活服务平台
人工智能·flutter·华为·harmonyos·鸿蒙
解局易否结局2 小时前
鸿蒙多设备形态适配实战:从手表到车机的差异化 UI 设计
ui·华为·harmonyos
不羁的木木2 小时前
HarmonyOS APP实战-基于Image Kit的图像处理APP - 第1篇:环境搭建与项目初始化
图像处理·华为·harmonyos
解局易否结局2 小时前
鸿蒙新特性实战:元服务与卡片开发完全指南
华为·harmonyos
爱吃大芒果4 小时前
商用门店应用开发:AbilityKit Kiosk 锁屏模式拆解
网络·华为·harmonyos
爸爸6194 小时前
30 原始 SQL 查询与聚合统计:querySql 实战
数据库·sql·华为·harmonyos·鸿蒙系统
星释13 小时前
鸿蒙智能体开发实战:23.常见问题
服务器·华为·ai·harmonyos·鸿蒙·智能体
子非鱼942713 小时前
HarmonyOS律愈实战09:HarmonyOS Form播放器卡片实现
华为·harmonyos