跳至主要内容

使用 Predicate 建構訂單查詢條件

本文檔展示如何在訂單管理中使用 Predicate 模組建構 RSQL/FIQL 查詢條件。

基本用法

1. 簡單查詢

import { Predicate } from '@appfuse/appfuse-web/utils/predicate';
import { orderService } from '@/services/sales/order-service';

// 查詢待確認的訂單
const result = await orderService.query({
predicate: Predicate.eq('status', 'pending_confirmation').toString(),
page: 0,
size: 20,
sort: 'createdAt,desc',
});

// 計算總頁數(前端衍生欄位)
const totalPages = result.size ? Math.ceil(result.totalElements / result.size) : 1;
console.log(`Total: ${result.totalElements}, Page: ${(result.page ?? 0) + 1}/${totalPages}`);
console.log('Orders:', result.content);

2. 組合查詢(AND)

// 查詢特定日期範圍內的待處理訂單
const predicate = Predicate.eq('status', 'pending_confirmation')
.and(Predicate.ge('deliveryDate', new Date('2025-11-01')))
.and(Predicate.le('deliveryDate', new Date('2025-11-30')))
.toString();

const result = await orderService.query({
predicate,
page: 0,
size: 20,
});

// 生成的 RSQL 查詢:
// (status=="pending_confirmation";deliveryDate=ge=2025-11-01T00:00:00.000Z;deliveryDate=le=2025-11-30T23:59:59.999Z)

3. 多狀態查詢(IN)

// 查詢多個狀態的訂單
const predicate = Predicate.in('status', [
'pending_confirmation',
'confirmed',
'in_production'
]).toString();

const result = await orderService.query({ filter });

// 生成的 RSQL 查詢:
// status=in=("pending_confirmation","confirmed","in_production")

4. 模糊查詢

// 根據訂單編號模糊查詢
const searchTerm = 'ORD-2025';
const predicate = Predicate.like('orderNumber', searchTerm).toString();

const result = await orderService.query({ filter });

// 生成的 RSQL 查詢:
// orderNumber=="*ORD-2025*"

5. 金額範圍查詢

// 查詢金額在 1000-5000 之間的訂單
const predicate = Predicate.ge('total', 1000)
.and(Predicate.le('total', 5000))
.toString();

const result = await orderService.query({ filter });

// 生成的 RSQL 查詢:
// (total=ge=1000;total=le=5000)

完整範例:OrderFinder 組件

import { useEffect, useState } from 'react';
import { Predicate } from '@appfuse/appfuse-web/utils/predicate';
import { orderService } from '@/services/sales/order-service';
import type { Order } from '@/mocks/types';

export function OrderFinder() {
const [orders, setOrders] = useState<Order[]>([]);
const [searchTerm, setSearchTerm] = useState('');
const [statusFilter, setStatusFilter] = useState<string[]>([]);
const [dateFrom, setDateFrom] = useState('');
const [dateTo, setDateTo] = useState('');
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);

useEffect(() => {
const fetchOrders = async () => {
// 動態建構查詢條件
let predicate = Predicate.empty();

// 搜尋條件
if (searchTerm) {
predicate = predicate.and(Predicate.like('orderNumber', searchTerm));
}

// 狀態過濾
if (statusFilter.length > 0) {
predicate = predicate.and(Predicate.in('status', statusFilter));
}

// 日期範圍
if (dateFrom) {
predicate = predicate.and(Predicate.ge('deliveryDate', new Date(dateFrom)));
}
if (dateTo) {
predicate = predicate.and(Predicate.le('deliveryDate', new Date(dateTo)));
}

// 調用 API
const result = await orderService.query({
predicate: predicate.toString(),
page: currentPage - 1, // Spring 從 0 開始
size: 20,
sort: 'createdAt,desc',
});

setOrders(result.content);

// 計算總頁數(UI 元件自己計算)
const actualSize = result.size ?? 20;
const calculatedTotalPages = Math.ceil(result.totalElements / actualSize);
setTotalPages(calculatedTotalPages);
};

void fetchOrders();
}, [searchTerm, statusFilter, dateFrom, dateTo, currentPage]);

return (
<div>
{/* UI components */}
</div>
);
}

後端對應格式(Spring)

前端生成的 RSQL 查詢會傳遞給後端的 Spring Controller:

@GetMapping
public ResponseEntity<List<PropertyMap>> findAll(
@RequestParam(required = false) SearchPredicate predicate,
Pageable pageable,
Authentication authentication) {

Page<PropertyMap> data = orderService.queryAll(predicate, pageable);

HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.set("X-Total-Count", String.valueOf(data.getTotalElements()));

return ResponseEntity.ok().headers(httpHeaders).body(data.getContent());
}

查詢參數格式

參數說明範例
predicateRSQL 查詢字串status=="pending"
page頁碼(從 0 開始)0, 1, 2
size每頁筆數20, 50, 100
sort排序欄位和方向createdAt,desc

回應格式

Headers:

  • X-Total-Count: 總筆數

Body:

[
{
"id": "order-1",
"orderNumber": "ORD-2025-001",
"status": "pending_confirmation",
"total": 2500,
...
}
]

實際 HTTP 請求範例

請求

GET /api/v1/orders?predicate=(status%3D%3D%22pending_confirmation%22%3BdeliveryDate%3Dge%3D2025-11-01)&page=0&size=20&sort=createdAt,desc
Authorization: Bearer <access_token>

回應

HTTP/1.1 200 OK
X-Total-Count: 45
Content-Type: application/json

[
{ "id": "order-1", ... },
{ "id": "order-2", ... },
...
]

支援的操作符

操作符RSQL 格式Predicate 方法說明
等於==eq(field, value)精確匹配
不等於!=notEq(field, value)不相等
大於=gt=gt(field, value)大於
大於等於=ge=ge(field, value)大於等於
小於=lt=lt(field, value)小於
小於等於=le=le(field, value)小於等於
在集合中=in=in(field, array)值在陣列中
不在集合中=out=notIn(field, array)值不在陣列中
模糊查詢==like(field, value)包含子字串(自動添加 *

最佳實踐

1. 空條件處理(核心優勢)

Predicate 會自動忽略空值,無需手動檢查條件是否存在!

// ❌ 傳統做法:需要大量 if 檢查
let predicate = Predicate.empty();

if (searchTerm) {
predicate = predicate.and(Predicate.like('orderNumber', searchTerm));
}

if (filters.status.length > 0) {
predicate = predicate.and(Predicate.in('status', filters.status));
}

if (filters.dateFrom) {
predicate = predicate.and(Predicate.ge('deliveryDate', new Date(filters.dateFrom)));
}

// ✅ 優雅做法:直接串接,空值自動被忽略
const predicate = Predicate.empty()
.and(Predicate.like('orderNumber', searchTerm)) // 空字串 → 忽略
.and(Predicate.in('status', filters.status)) // [] → 忽略
.and(Predicate.ge('deliveryDate',
filters.dateFrom ? new Date(filters.dateFrom) : undefined // undefined → 忽略
));

// 結果:只保留有效條件

自動忽略的值

  • nullundefined
  • 空字串 ''(對於 like()
  • 空陣列 [](對於 in()notIn()
// 這些條件會被自動忽略
Predicate.eq('field', null) // → EmptyNode(忽略)
Predicate.eq('field', undefined) // → EmptyNode(忽略)
Predicate.in('field', []) // → EmptyNode(忽略)
Predicate.like('field', '') // → EmptyNode(忽略)

// 組合後
Predicate.empty()
.and(Predicate.eq('a', null))
.and(Predicate.in('b', []))
.toString() // → null(完全沒有條件)

唯一例外is()isNot() 不會忽略 null(用於明確檢查 null 或 boolean)

Predicate.is('parent', null)      // → "parent=is=null" ✅(不忽略)
Predicate.isNot('deleted', true) // → "deleted=isnt=true" ✅(不忽略)

2. 動態條件建構

function buildOrderQuery(filters: {
search?: string;
status?: string[];
dateFrom?: string;
dateTo?: string;
minAmount?: number;
maxAmount?: number;
}) {
let predicate = Predicate.empty();

if (filters.search) {
predicate = predicate.and(Predicate.like('orderNumber', filters.search));
}

if (filters.status && filters.status.length > 0) {
predicate = predicate.and(Predicate.in('status', filters.status));
}

if (filters.dateFrom) {
predicate = predicate.and(Predicate.ge('deliveryDate', new Date(filters.dateFrom)));
}

if (filters.dateTo) {
predicate = predicate.and(Predicate.le('deliveryDate', new Date(filters.dateTo)));
}

if (filters.minAmount !== undefined) {
predicate = predicate.and(Predicate.ge('total', filters.minAmount));
}

if (filters.maxAmount !== undefined) {
predicate = predicate.and(Predicate.le('total', filters.maxAmount));
}

return predicate.toString();
}

// 使用
const filter = buildOrderQuery({
status: ['pending_confirmation', 'confirmed'],
dateFrom: '2025-11-01',
minAmount: 1000,
});

await orderService.query({ filter, page: 0, size: 20 });

3. 分頁處理

// 前端頁碼從 1 開始,Spring 從 0 開始
const uiPage = 1; // 使用者看到的頁碼
const springPage = uiPage - 1; // 傳給後端的頁碼

const result = await orderService.query({
filter: predicate.toString(),
page: springPage,
size: 20,
});

// 顯示分頁資訊
const totalPages = result.size ? Math.ceil(result.totalElements / result.size) : 1;
console.log(`Page ${(result.page ?? 0) + 1} of ${totalPages}`);
console.log(`Total: ${result.totalElements} items`);

相關資源

  • Predicate API 文檔(參閱 appfuse-web/lib/utils/predicate/)
  • RSQL 標準
  • Service Layer 架構(參閱 appfuse-web/src/services/)