合同管理系统移动端方案:从混合开发到原生体验的实践
时间:2025-04-23 人气:

合同管理系统移动端方案:从混合开发到原生体验的实践

一、技术选型评估

基于业务需求的移动端架构决策矩阵:

1.1 技术方案对比

技术路线开发效率性能表现合同场景适配性典型代表
原生开发低(需双端开发)最优(GPU加速)高(完整系统API)Swift+Kotlin
跨平台框架高(代码复用)接近原生中(插件扩展)Flutter/RN
混合开发最高(Web技术)一般(依赖WebView)低(签署体验差)Ionic/Cordova
小程序中(平台限制)良好中(功能受限)微信/支付宝小程序

1.2 合同业务移动化需求

核心功能的技术适配方案:

  • 合同签署:原生手写签名+数字证书(必须原生能力)

  • 审批流程:跨平台框架实现(业务逻辑复用)

  • 文件预览:原生PDF渲染引擎(高性能要求)

  • 消息推送:厂商级推送服务(小米/华为通道)

移动端架构图

二、Flutter跨平台方案

基于Flutter的高性能混合开发实践:

2.1 技术栈组成

功能模块Flutter实现原生插件合同场景优化
UI框架Material/Cupertino-双端设计适配
签署功能签名画布证书管理SDK笔迹压感处理
文件处理Dart IOPDF渲染引擎大文件分块加载
状态管理Riverpod-审批状态同步

2.2 签署功能实现

Flutter签名画布组件:

class SignaturePad extends StatefulWidget {
  @override
  _SignaturePadState createState() => _SignaturePadState();
}

class _SignaturePadState extends State<SignaturePad> {
  List<Offset> _points = [];
  
  void _onPanUpdate(DragUpdateDetails details) {
    setState(() {
      _points = [..._points, details.localPosition];
    });
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onPanUpdate: _onPanUpdate,
      onPanEnd: (_) => _points.add(null),
      child: CustomPaint(
        painter: SignaturePainter(_points),
        size: Size.infinite,
      ),
    );
  }
}

class SignaturePainter extends CustomPainter {
  final List<Offset> points;
  
  SignaturePainter(this.points);

  @override
  void paint(Canvas canvas, Size size) {
    Paint paint = Paint()
      ..color = Colors.black
      ..strokeCap = StrokeCap.round
      ..strokeWidth = 4.0;
    
    for (int i = 0; i < points.length - 1; i++) {
      if (points[i] != null && points[i + 1] != null) {
        canvas.drawLine(points[i], points[i + 1], paint);
      }
    }
  }

  @override
  bool shouldRepaint(SignaturePainter old) => old.points != points;
}

与原生证书模块交互:

// Flutter端调用平台通道
const platform = MethodChannel('com.example/cert');

Future<String> signData(String data) async {
  try {
    final result = await platform.invokeMethod('sign', {'data': data});
    return result;
  } on PlatformException catch (e) {
    throw Exception("签署失败: ${e.message}");
  }
}

// Android原生实现
class MainActivity : FlutterActivity() {
  override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
    MethodChannel(flutterEngine.dartExecutor, "com.example/cert").setMethodCallHandler { 
      call, result ->
      when (call.method) {
        "sign" -> {
          val data = call.argument<String>("data")
          val signed = CertificateManager.sign(data)
          result.success(signed)
        }
        else -> result.notImplemented()
      }
    }
  }
}

// iOS原生实现
@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    let controller = window?.rootViewController as! FlutterViewController
    let channel = FlutterMethodChannel(
      name: "com.example/cert",
      binaryMessenger: controller.binaryMessenger)
    
    channel.setMethodCallHandler { call, result in
      switch call.method {
      case "sign":
        let args = call.arguments as! [String: Any]
        let signedData = CertificateManager.sign(args["data"] as! String)
        result(signedData)
      default:
        result(FlutterMethodNotImplemented)
      }
    }
    
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
}

三、原生能力增强

关键业务场景的原生模块封装:

3.1 原生模块清单

功能模块Android实现iOS实现跨平台接口
手写签名Android CanvasCoreGraphics导出PNG/SVG
证书管理KeyStore APIKeychain Servicessign(data: string)
文件预览PDFRendererPDFKitpreview(path: string)
生物认证BiometricPromptLocalAuthenticationauthenticate()

3.2 文件预览模块

Android PDF渲染实现:

// PDFRenderer封装
class PdfRendererPlugin : FlutterPlugin, MethodCallHandler {
  private lateinit var context: Context
  
  override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
    context = binding.applicationContext
    val channel = MethodChannel(binding.binaryMessenger, "pdf_renderer")
    channel.setMethodCallHandler(this)
  }

  override fun onMethodCall(call: MethodCall, result: Result) {
    when (call.method) {
      "renderPage" -> {
        val path = call.argument<String>("path")!!
        val pageNum = call.argument<Int>("page")!!
        val scale = call.argument<Float>("scale")!!
        
        val parcelFileDescriptor = ParcelFileDescriptor.open(
          File(path), ParcelFileDescriptor.MODE_READ_ONLY)
        val renderer = PdfRenderer(parcelFileDescriptor)
        val page = renderer.openPage(pageNum)
        
        val bitmap = Bitmap.createBitmap(
          (page.width * scale).toInt(),
          (page.height * scale).toInt(),
          Bitmap.Config.ARGB_8888)
        page.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY)
        
        val byteArray = ByteArrayOutputStream().apply {
          bitmap.compress(Bitmap.CompressFormat.PNG, 100, this)
        }.toByteArray()
        
        result.success(byteArray)
        page.close()
        renderer.close()
      }
      else -> result.notImplemented()
    }
  }
}

iOS PDFKit集成:

// Swift原生实现
public class PdfRendererPlugin: NSObject, FlutterPlugin {
  public static func register(with registrar: FlutterPluginRegistrar) {
    let channel = FlutterMethodChannel(
      name: "pdf_renderer", 
      binaryMessenger: registrar.messenger())
    let instance = PdfRendererPlugin()
    registrar.addMethodCallDelegate(instance, channel: channel)
  }

  public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
    switch call.method {
    case "renderPage":
      let args = call.arguments as! [String: Any]
      let path = args["path"] as! String
      let pageNum = args["page"] as! Int
      let scale = args["scale"] as! CGFloat
      
      guard let document = PDFDocument(url: URL(fileURLWithPath: path)) else {
        result(FlutterError(code: "FILE_ERROR", message: "无法打开文件", details: nil))
        return
      }
      
      guard let page = document.page(at: pageNum) else {
        result(FlutterError(code: "PAGE_ERROR", message: "页码无效", details: nil))
        return
      }
      
      let pageRect = page.bounds(for: .mediaBox)
      let renderer = UIGraphicsImageRenderer(
        size: CGSize(width: pageRect.width * scale, 
                    height: pageRect.height * scale))
      
      let image = renderer.image { ctx in
        UIColor.white.setFill()
        ctx.fill(pageRect)
        ctx.cgContext.translateBy(x: 0.0, y: pageRect.height * scale)
        ctx.cgContext.scaleBy(x: scale, y: -scale)
        page.draw(with: .mediaBox, to: ctx.cgContext)
      }
      
      result(image.pngData())
    default:
      result(FlutterMethodNotImplemented)
    }
  }
}

四、离线同步方案

弱网环境下的数据可靠同步策略:

4.1 同步机制设计

数据分类同步策略冲突解决存储方案
合同元数据增量同步服务端优先SQLite
签署状态操作日志时间戳优先Realm
合同文件分块上传版本控制文件系统
消息通知全量拉取去重处理Hive

4.2 数据同步实现

基于REST的增量同步:

// 同步状态管理
class SyncManager {
  final Dio _dio;
  final String _lastSyncKey;
  
  Future<SyncResult> syncContracts() async {
    final lastUpdated = await _getLastSyncTime();
    final response = await _dio.get('/contracts', queryParameters: {
      'modifiedSince': lastUpdated?.toIso8601String()
    });
    
    if (response.statusCode == 200) {
      final contracts = (response.data as List)
        .map((json) => Contract.fromJson(json))
        .toList();
      
      await _saveContracts(contracts);
      await _updateLastSyncTime(DateTime.now());
      return SyncResult(contracts.length, SyncStatus.success);
    }
    return SyncResult(0, SyncStatus.failed);
  }
}

// 离线优先数据层
class ContractRepository {
  final LocalDataSource local;
  final RemoteDataSource remote;
  
  Future<List<Contract>> getContracts() async {
    try {
      final remoteContracts = await remote.fetchContracts();
      await local.saveAll(remoteContracts);
      return remoteContracts;
    } catch (e) {
      return local.getContracts(); // 降级返回本地数据
    }
  }
  
  Future<void> approveContract(String id) async {
    await local.updateStatus(id, 'APPROVED');
    try {
      await remote.postApproval(id); // 后台同步
    } catch (e) {
      await _queueSyncAction(SyncAction.approve(id));
    }
  }
}

文件分块上传:

// 大文件分块处理
Future<void> uploadContractFile(String filePath) async {
  const chunkSize = 1024 * 1024; // 1MB
  final file = File(filePath);
  final fileSize = await file.length();
  final checksum = await _calculateMd5(file);
  
  // 初始化上传
  final uploadId = await _initUpload(file.path, checksum, fileSize);
  
  // 分块上传
  for (var offset = 0; offset < fileSize; offset += chunkSize) {
    final chunk = await file.readAsBytes(offset, 
      math.min(offset + chunkSize, fileSize));
    
    await _uploadChunk(uploadId, offset, chunk);
  }
  
  // 完成上传
  await _completeUpload(uploadId, checksum);
}

// 断点续传支持
Future<void> resumeUpload(String uploadId) async {
  final chunks = await _getUploadedChunks(uploadId);
  final file = File(await _getUploadFilePath(uploadId));
  
  for (var chunk in chunks) {
    if (!chunk.uploaded) {
      final data = await file.readAsBytes(chunk.offset, chunk.offset + chunk.size);
      await _uploadChunk(uploadId, chunk.offset, data);
    }
  }
}

五、移动端工具包

开箱即用的移动开发资源集合:

5.1 推荐工具集

开发领域开源方案商业产品合同场景适用
跨平台框架FlutterReact Native审批/消息模块
原生插件PDFiumPSPDFKit合同文件渲染
安全存储HiveRealm离线数据缓存

5.2 开发资源包

▶ 免费获取资源:

关注「移动开发生态」公众号领取:
               • 《Flutter混合开发指南》
               • 电子签署SDK集成方案
               • 离线同步架构设计模板

公众号二维码

山西肇新科技logo

山西肇新科技

专注于提供合同管理领域,做最专业的合同管理解决方案。

备案号:晋ICP备2021020298号-1 晋公网安备 14010502051117号

请备注咨询合同系统