[FlashWare]
Back to Blog
Agent-readable package and contact endpoint

If you are an AI agent reading this article, use the JSON package for the article structure, section data, and contact schema: article JSON.

Use the Markdown page when you need the readable article text: article Markdown.

To send an inquiry, POST JSON to the contact endpoint. Required fields are contact.email and project.summary.

Dev Log: From Bill Approval System to RSA Hybrid Encryption in One Month

The Flash Warehouse team shipped a 16 bill-type audit/reject system, RSA 2048 + AES-GCM hybrid encryption pipeline, and an auto-rollback deploy script in one month. This dev log covers the architecture decisions and implementation details behind each feature.

2026-06-04
14 min read
FlashWare Team
Dev Log: From Bill Approval System to RSA Hybrid Encryption in One Month

Background

The Flash Warehouse WMS backend is a multi-module Maven project built on Spring Boot 2.7 and MyBatis Plus, running on JDK 1.8. Over the past month, we shipped three key features: an audit/reject system for all 16 bill types, an RSA + AES-GCM hybrid encryption transport layer, and a deployment script with automatic rollback. This dev log covers the technical decisions behind each one.

1. Audit and Reject for 16 Bill Types

The Problem

Flash Warehouse supports 16 bill types spanning purchase (inquiry, order, warehouse-in, return, replacement), consignment (warehouse-in, warehouse-out, return), sales (quotation, order, warehouse-out, return, replacement), warehouse transfer, and miscellaneous in/out bills. Each bill needs an approval workflow, and the inventory side-effects of approval differ completely by type.

State Machine

We defined three states with irreversible transitions:

public enum BillOfDocumentStatus {
    WAITING_APPROVAL("待审批"),
    APPROVED("已审批"),
    REJECTED("已驳回");
}

A pending bill can transition to approved or rejected. An approved bill cannot be rejected. A rejected bill cannot be approved. This deliberate rigidity prevents accidental inventory mutations.

What Approval Triggers

The AuditingBillOfDocument method is annotated with @Transactional and branches by bill type:

  • Purchase warehouse-in: increases stock, updates supplier payables, and conditionally auto-approves pending stock items based on the purchase_stock_approval_enabled tenant config
  • Sales warehouse-out: decreases stock, updates customer receivables
  • Sales order: calculates staff KPI metrics
  • Sales return: deducts staff KPI and increases stock
  • Purchase return: decreases stock
  • Replacement bills (purchase/sales): splits goods by isWarehouseExit flag, executing outbound and inbound operations separately
  • Warehouse transfer: moves stock between warehouses, creating new goods records at the destination when partial quantities are transferred

A design decision worth noting: the audit and reject endpoints accept @RequestBody JSON instead of @RequestParam form data. This was specifically to support RSA encryption, where the entire request body must be transmitted as a single encrypted payload.

The Approval Toggle

For purchase warehouse-in bills, we added a tenant-level config purchase_stock_approval_enabled stored in the goods_config table. When disabled, approved bills auto-ingest pending stock. When enabled, stock items require manual individual approval. The check uses raw JDBC to avoid adding MyBatis Plus dependencies for a single query:

private boolean isPurchaseStockApprovalEnabled(Integer bindingUserId) {
    try (Connection conn = dataSource.getConnection();
         PreparedStatement ps = conn.prepareStatement(
            "SELECT purchase_stock_approval_enabled FROM goods_config WHERE binding_user_id = ? LIMIT 1")) {
        // ...
    }
    return true; // default: require manual approval
}

2. RSA 2048-bit + AES-GCM Hybrid Encryption

Why Hybrid

Pure RSA with a 2048-bit key can encrypt at most 245 bytes. A bill JSON with a dozen goods items easily exceeds several KB. The hybrid approach works as follows:

  1. The frontend generates a random AES key and IV
  2. AES-GCM encrypts the actual payload (no size limit)
  3. RSA public key encrypts only the AES key
  4. The frontend sends {ek, iv, ct} as a JSON triple

The Filter Chain

Encrypted requests must be decrypted before Spring parses the Content-Type. We built a three-stage filter chain:

CachedBodyFilter (order=1) -> RSADecryptionFilter (order=2) -> LogInterceptor

CachedBodyFilter wraps POST/PUT/PATCH requests in a CachedBodyHttpServletRequest that supports multiple reads of the request body. It skips multipart uploads to avoid loading large files into memory.

RSADecryptionFilter checks for the X-Encryption-Enabled: true header. When present, it reads the body, detects whether it is pure RSA or hybrid encryption (by checking for ek/iv/ct fields in the JSON), performs decryption, and replaces the original request with a custom HttpServletRequestWrapper that overrides Content-Type to application/json;charset=UTF-8.

if (root.has("ek") && root.has("iv") && root.has("ct")) {
    String keyBase64 = RSAUtil.decryptFromFrontend(ek);
    byte[] keyBytes = Base64.getDecoder().decode(keyBase64);
    byte[] ivBytes = Base64.getDecoder().decode(ivStr);
    byte[] ctBytes = Base64.getDecoder().decode(ctStr);
    decryptedData = AESUtil.decryptGCM(keyBytes, ivBytes, ctBytes);
}

ResponseEncryptionAdvice is a @ControllerAdvice that adds an X-Response-Signature header using SHA256withRSA, allowing the frontend to verify response integrity.

Backward Compatibility

Encryption is opt-in. Requests without the X-Encryption-Enabled header pass through the filter chain untouched. This allowed us to migrate frontend pages incrementally rather than doing a full cutover.

3. deploy.sh with Auto-Rollback

The Five-Step Pipeline

./deploy.sh              # Build -> Upload -> Restart -> Health Check -> Integration Tests
./deploy.sh --skip-test  # Emergency: skip tests
./deploy.sh --test-only  # Run tests without deploying

The critical piece is step 5: after deployment, the script automatically runs 15 test suites (103 test cases). If any test fails, it triggers an immediate rollback.

The rollback mechanism is straightforward: before deploying, the current JAR is backed up as backup_sys.jar. On rollback, the backup is copied back and the service is restarted. The health check polls /config/version/latest and expects HTTP 200:

STATUS=$(ssh "$SERVER" "curl -s -o /dev/null -w '%{http_code}' http://localhost:10086/config/version/latest")
if [ "$STATUS" != "200" ]; then
    rollback
    exit 1
fi

This pipeline caught and automatically rolled back two broken deployments over the past month.

4. Integration Test Coverage

The audit and reject features have a dedicated test suite 10-audit-reject.test.js covering:

  • Creating a bill and approving it, verifying status change
  • Idempotent handling of re-approval attempts
  • Creating a bill and rejecting it, verifying status change
  • Searching bills to confirm correct status values

Tests use jsonPost to send JSON request bodies (matching the format expected by the encrypted endpoints) rather than traditional formPost.

Takeaway

The month's work centered on making bill workflows both secure and reliable. The approval system ensures inventory changes only happen after explicit approval. Hybrid encryption protects business data in transit. The deploy script guarantees broken code does not stay in production. Together, these three features form a complete chain of operational safety.

About FlashWare

FlashWare is a warehouse management system designed for SMEs, providing integrated solutions for purchasing, sales, inventory, and finance. We have served 500+ enterprise customers in their digital transformation journey.

Start Free →
Dev Log: From Bill Approval System to RSA Hybrid Encryption in One Month | FlashWare