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.
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_enabledtenant 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
isWarehouseExitflag, 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:
- The frontend generates a random AES key and IV
- AES-GCM encrypts the actual payload (no size limit)
- RSA public key encrypts only the AES key
- 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.