Просмотр исходного кода

added isExtra and sync extra DO from m18

production
[email protected] 10 часов назад
Родитель
Сommit
1472a05830
13 измененных файлов: 139 добавлений и 12 удалений
  1. +33
    -0
      .cursor/rules/frontend-prevent-duplicate-api-calls.mdc
  2. +29
    -9
      src/main/java/com/ffii/fpsms/m18/service/M18DeliveryOrderService.kt
  3. +8
    -1
      src/main/java/com/ffii/fpsms/m18/web/M18TestController.kt
  4. +24
    -0
      src/main/java/com/ffii/fpsms/modules/common/scheduler/service/SchedulerService.kt
  5. +4
    -0
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/DeliveryOrder.kt
  6. +6
    -0
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/DeliveryOrderRepository.kt
  7. +5
    -1
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/models/DeliveryOrderInfo.kt
  8. +17
    -1
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DeliveryOrderService.kt
  9. +3
    -0
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/DeliveryOrderController.kt
  10. +2
    -0
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/models/DoDetailResponse.kt
  11. +2
    -0
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/models/ReleaseDoRequest.kt
  12. +1
    -0
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/models/SaveDeliveryOrderRequest.kt
  13. +5
    -0
      src/main/resources/db/changelog/changes/20260508_01_fai/01_delivery_order_is_etra.sql

+ 33
- 0
.cursor/rules/frontend-prevent-duplicate-api-calls.mdc Просмотреть файл

@@ -0,0 +1,33 @@
---
description: Prevent double-click / duplicate API calls from frontend UI
alwaysApply: true
---

# Prevent duplicated API calls (frontend)

When wiring UI actions (buttons, row actions, dialogs) to backend APIs, **always prevent double submission**. Relying on `setState` + `disabled` alone is not sufficient because rapid double-click can fire twice before React re-renders.

- **Must**: add an **in-flight lock** (e.g. `useRef(false)`) and early-return if already running.
- **Must**: keep the UI disabled/loading (`disabled={isLoading}`) for user feedback.
- **Must**: clear the lock in `finally` so it always releases.
- **Should**: if the same endpoint can be triggered from multiple places, consider a shared “single-flight” helper (dedupe by `method+url+body` key).

Example pattern:

```tsx
const inFlightRef = useRef(false);
const [isSaving, setIsSaving] = useState(false);

const onSave = async () => {
if (inFlightRef.current) return;
inFlightRef.current = true;
setIsSaving(true);
try {
await doRequest();
} finally {
setIsSaving(false);
inFlightRef.current = false;
}
};
```


+ 29
- 9
src/main/java/com/ffii/fpsms/m18/service/M18DeliveryOrderService.kt Просмотреть файл

@@ -12,6 +12,7 @@ import com.ffii.fpsms.modules.deliveryOrder.enums.DeliveryOrderLineStatus
import com.ffii.fpsms.modules.deliveryOrder.enums.DeliveryOrderStatus
import com.ffii.fpsms.modules.deliveryOrder.service.DeliveryOrderLineService
import com.ffii.fpsms.modules.deliveryOrder.service.DeliveryOrderService
import com.ffii.fpsms.modules.deliveryOrder.entity.DeliveryOrderRepository
import com.ffii.fpsms.modules.deliveryOrder.web.models.SaveDeliveryOrderLineRequest
import com.ffii.fpsms.modules.deliveryOrder.web.models.SaveDeliveryOrderRequest
import com.ffii.fpsms.modules.master.entity.ItemUom
@@ -35,6 +36,7 @@ open class M18DeliveryOrderService(
val apiCallerService: ApiCallerService,
val m18DataLogService: M18DataLogService,
val deliveryOrderService: DeliveryOrderService,
val deliveryOrderRepository: DeliveryOrderRepository,
val deliveryOrderLineService: DeliveryOrderLineService,
val itemsService: ItemsService,
val shopService: ShopService,
@@ -106,7 +108,6 @@ open class M18DeliveryOrderService(
if (request.dDateEqual != null) {
shopPoConds += "=and=(${dDateEqualConds})"
}

logger.info("shopPoConds: ${shopPoConds}")
val shopPoParams = M18PurchaseOrderListRequest(
@@ -153,18 +154,35 @@ open class M18DeliveryOrderService(

open fun saveDeliveryOrders(request: M18CommonRequest): SyncResult {
val deliveryOrdersWithType = getDeliveryOrdersWithType(request)
return saveDeliveryOrdersWithPreparedList(deliveryOrdersWithType)
return saveDeliveryOrdersWithPreparedList(deliveryOrdersWithType, syncIsEtra = false)
}

/**
* Sync a single M18 shop PO / delivery order by document [code], same search pattern as
* [com.ffii.fpsms.m18.service.M18PurchaseOrderService.savePurchaseOrderByCode].
*
* @param isEtraSync when true, persist local `delivery_order.isEtra=true` (manual DO(加單) sync).
* No M18-side "加單" filtering is used.
* @param newOnly when true, skip if a non-deleted local DO already exists with the same `code`.
*/
open fun saveDeliveryOrderByCode(code: String): SyncResult {
open fun saveDeliveryOrderByCode(
code: String,
isEtraSync: Boolean = false,
newOnly: Boolean = false,
): SyncResult {
if (newOnly && deliveryOrderRepository.existsByCodeAndDeletedIsFalse(code)) {
return SyncResult(
totalProcessed = 1,
totalSuccess = 0,
totalFail = 0,
query = "skipped (newOnly=true): delivery_order.code already exists: $code",
)
}
val conds = "(code=equal=$code)"
val searchRequest = M18PurchaseOrderListRequest(
stSearch = "po",
params = null,
conds = "(code=equal=$code)"
conds = conds
)
val doListResponse = try {
apiCallerService.get<M18PurchaseOrderListResponse, M18PurchaseOrderListRequest>(
@@ -183,20 +201,21 @@ open class M18DeliveryOrderService(
totalProcessed = 1,
totalSuccess = 0,
totalFail = 1,
query = "code=equal=$code"
query = conds
)
}

val prepared = M18PurchaseOrderListResponseWithType(
valuesWithType = mutableListOf(Pair(PurchaseOrderType.SHOP, doListResponse)),
query = "code=equal=$code"
query = conds
)

return saveDeliveryOrdersWithPreparedList(prepared)
return saveDeliveryOrdersWithPreparedList(prepared, syncIsEtra = isEtraSync)
}

private fun saveDeliveryOrdersWithPreparedList(
deliveryOrdersWithType: M18PurchaseOrderListResponseWithType?
deliveryOrdersWithType: M18PurchaseOrderListResponseWithType?,
syncIsEtra: Boolean = false,
): SyncResult {
logger.info("--------------------------------------------Start - Saving M18 Delivery Order--------------------------------------------")

@@ -283,7 +302,8 @@ open class M18DeliveryOrderService(
m18DataLogId = saveM18DeliveryOrderLog.id,
handlerId = null,
m18BeId = mainpo.beId,
deleted = mainpo.udfIsVoid == true
deleted = mainpo.udfIsVoid == true,
isEtra = syncIsEtra,
)

val saveDeliveryOrderResponse =


+ 8
- 1
src/main/java/com/ffii/fpsms/m18/web/M18TestController.kt Просмотреть файл

@@ -72,7 +72,14 @@ class M18TestController (

@GetMapping("/test/do-by-code")
fun testSyncDoByCode(@RequestParam code: String): SyncResult {
return m18DeliveryOrderService.saveDeliveryOrderByCode(code)
return m18DeliveryOrderService.saveDeliveryOrderByCode(code, isEtraSync = false)
}

/** DO(加單):手動按 code 同步,並寫入本地 [DeliveryOrder.isEtra]=true(不做 M18 端加單條件過濾) */
@GetMapping("/test/do-by-code-extra")
fun testSyncDoByCodeExtra(@RequestParam code: String): SyncResult {
// 加單 tab: only sync when it's a NEW order (not existing in local system)
return m18DeliveryOrderService.saveDeliveryOrderByCode(code, isEtraSync = true, newOnly = true)
}

@GetMapping("/test/product-by-code")


+ 24
- 0
src/main/java/com/ffii/fpsms/modules/common/scheduler/service/SchedulerService.kt Просмотреть файл

@@ -491,6 +491,30 @@ open class SchedulerService(
result = result,
start = currentTime
)

// Extra DO sync window: after DO2, also sync ETA = today or tomorrow (normal sync; does NOT set isEtra).
try {
val extraStart = LocalDateTime.now()
val requestExtra = M18CommonRequest(
dDateFrom = today.format(dateTimeStringFormat),
dDateTo = tmr.format(dateTimeStringFormat),
)
val extraResult = m18DeliveryOrderService.saveDeliveryOrders(requestExtra)
saveSyncLog(
type = "DO2_EXTRA",
status = "SUCCESS",
result = extraResult,
start = extraStart,
)
} catch (e: Exception) {
logger.error("DO2_EXTRA sync failed: ${e.message}", e)
saveSyncLog(
type = "DO2_EXTRA",
status = "FAIL",
error = e.message,
start = LocalDateTime.now(),
)
}
}

open fun getPostCompletedDnAndProcessGrn(


+ 4
- 0
src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/DeliveryOrder.kt Просмотреть файл

@@ -62,4 +62,8 @@ open class DeliveryOrder: BaseEntity<Long>() {

@Column(name = "m18BeId")
open var m18BeId: Long? = null

/** 加單:由 M18「加單」專用同步標記;一般 DO 為 false */
@Column(name = "isEtra", nullable = false)
open var isEtra: Boolean = false
}

+ 6
- 0
src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/DeliveryOrderRepository.kt Просмотреть файл

@@ -15,6 +15,8 @@ import com.ffii.fpsms.modules.deliveryOrder.web.models.*
import com.ffii.fpsms.modules.deliveryOrder.entity.models.*
@Repository
interface DeliveryOrderRepository : AbstractRepository<DeliveryOrder, Long> {
fun existsByCodeAndDeletedIsFalse(code: String): Boolean

@Query("""
select d from DeliveryOrder d
where d.deleted = false
@@ -109,6 +111,7 @@ fun searchDoLite(
and (:status is null or d.status = :status)
and (:etaStart is null or d.estimatedArrivalDate >= :etaStart)
and (:etaEnd is null or d.estimatedArrivalDate < :etaEnd)
and (:isEtra is null or d.isEtra = :isEtra)
order by d.id desc
""")
fun searchDoLitePage(
@@ -117,6 +120,7 @@ fun searchDoLitePage(
@Param("status") status: DeliveryOrderStatus?,
@Param("etaStart") etaStart: LocalDateTime?,
@Param("etaEnd") etaEnd: LocalDateTime?,
@Param("isEtra") isEtra: Boolean?,
pageable: Pageable
): Page<DeliveryOrderInfoLite>

@@ -132,6 +136,7 @@ fun searchDoLitePage(
and (:status is null or d.status = :status)
and (:etaStart is null or d.estimatedArrivalDate >= :etaStart)
and (:etaEnd is null or d.estimatedArrivalDate < :etaEnd)
and (:isEtra is null or d.isEtra = :isEtra)
and d.supplier is not null
and d.supplier.code in :allowedSupplierCodes
order by d.id desc
@@ -143,6 +148,7 @@ fun searchDoLitePageWithSupplierCodes(
@Param("status") status: DeliveryOrderStatus?,
@Param("etaStart") etaStart: LocalDateTime?,
@Param("etaEnd") etaEnd: LocalDateTime?,
@Param("isEtra") isEtra: Boolean?,
@Param("allowedSupplierCodes") allowedSupplierCodes: List<String>,
pageable: Pageable,
): Page<DeliveryOrderInfoLite>


+ 5
- 1
src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/models/DeliveryOrderInfo.kt Просмотреть файл

@@ -47,6 +47,9 @@ interface DeliveryOrderInfoLite {
val supplierCode: String?
@get:Value("#{target.shop?.addr3}")
val shopAddress: String?

@get:Value("#{target.isEtra}")
val isEtra: Boolean
}
data class DeliveryOrderInfoLiteDto(
val id: Long,
@@ -57,5 +60,6 @@ data class DeliveryOrderInfoLiteDto(
val shopName: String?,
val supplierName: String?,
val shopAddress: String?,
val truckLanceCode: String?
val truckLanceCode: String?,
val isEtra: Boolean = false,
)

+ 17
- 1
src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DeliveryOrderService.kt Просмотреть файл

@@ -147,6 +147,7 @@ open class DeliveryOrderService(
pageSize: Int?,
truckLanceCode: String?,
floor: String? = null,
isEtra: Boolean? = null,
): RecordsRes<DeliveryOrderInfoLiteDto> {

val page = (pageNum ?: 1) - 1
@@ -168,6 +169,7 @@ open class DeliveryOrderService(
status = statusEnum,
etaStart = etaStart,
etaEnd = etaEnd,
isEtra = isEtra,
allowedSupplierCodes = allowedForFloor,
pageable = PageRequest.of(0, 100_000),
)
@@ -245,7 +247,8 @@ open class DeliveryOrderService(
shopName = info.shopName,
supplierName = info.supplierName,
shopAddress = info.shopAddress,
truckLanceCode = calculatedTruckLanceCode
truckLanceCode = calculatedTruckLanceCode,
isEtra = deliveryOrdersMap[info.id]?.isEtra ?: info.isEtra,
)
}.filter { dto ->
val dtoTruckLanceCode = dto.truckLanceCode?.lowercase() ?: ""
@@ -276,6 +279,7 @@ open class DeliveryOrderService(
status = statusEnum,
etaStart = etaStart,
etaEnd = etaEnd,
isEtra = isEtra,
allowedSupplierCodes = allowedSupplierCodes,
pageable = PageRequest.of(page.coerceAtLeast(0), size),
)
@@ -311,6 +315,7 @@ open class DeliveryOrderService(
supplierName = info.supplierName,
shopAddress = info.shopAddress,
truckLanceCode = calculatedTruckLanceCode,
isEtra = deliveryOrder?.isEtra ?: info.isEtra,
)
}

@@ -333,6 +338,7 @@ open class DeliveryOrderService(
pageSize: Int?,
truckLanceCode: String?,
floor: String? = null,
isEtra: Boolean? = null,
): RecordsRes<DeliveryOrderInfoLiteDto> {
val mode = TruckLaneSearchSpec.parse(truckLanceCode)
if (mode is TruckLaneSearchSpec.Mode.NoFilter) {
@@ -345,6 +351,7 @@ open class DeliveryOrderService(
pageSize,
null,
floor,
isEtra,
)
}
val pageIdx = (pageNum ?: 1).coerceAtLeast(1) - 1
@@ -360,6 +367,7 @@ open class DeliveryOrderService(
statusEnum = statusEnum,
etaStart = etaStart,
etaEnd = etaEnd,
isEtra = isEtra,
allowedSupplierCodes = allowedSupplierCodesForFloor(floor),
lanePredicate = lanePredicate,
)
@@ -383,6 +391,7 @@ open class DeliveryOrderService(
pageNum: Int?,
pageSize: Int?,
floor: String? = null,
isEtra: Boolean? = null,
): RecordsRes<DeliveryOrderInfoLiteDto> {
val page = (pageNum ?: 1) - 1
val size = pageSize ?: 10
@@ -397,6 +406,7 @@ open class DeliveryOrderService(
status = statusEnum,
etaStart = etaStart,
etaEnd = etaEnd,
isEtra = isEtra,
allowedSupplierCodes = allowedSupplierCodes,
pageable = PageRequest.of(0, 100_000),
)
@@ -435,6 +445,7 @@ open class DeliveryOrderService(
supplierName = info.supplierName,
shopAddress = info.shopAddress,
truckLanceCode = calculatedTruckLanceCode,
isEtra = deliveryOrdersMap[info.id]?.isEtra ?: info.isEtra,
)
}.filter { dto -> TruckLaneSearchSpec.isUnassignedResolvedLane(dto.truckLanceCode) }

@@ -476,6 +487,7 @@ open class DeliveryOrderService(
estimatedArrivalDate = deliveryOrder.estimatedArrivalDate,
completeDate = deliveryOrder.completeDate,
status = deliveryOrder.status?.value,
isEtra = deliveryOrder.isEtra,
deliveryOrderLines = deliveryOrder.deliveryOrderLines.map { line ->
DoDetailLineResponse(
id = line.id!!,
@@ -796,6 +808,7 @@ open class DeliveryOrderService(
this.handler = handler
m18BeId = request.m18BeId
this.deleted = request.deleted
isEtra = request.isEtra ?: false
}

val savedDeliveryOrder = deliveryOrderRepository.saveAndFlush(deliveryOrder).let {
@@ -2091,6 +2104,7 @@ val inventoryLotLine = illId?.let { inventoryLotLineMap[it] }
statusEnum: DeliveryOrderStatus?,
etaStart: LocalDateTime?,
etaEnd: LocalDateTime?,
isEtra: Boolean?,
allowedSupplierCodes: List<String>,
lanePredicate: (String?) -> Boolean,
): List<DeliveryOrderInfoLiteDto> {
@@ -2104,6 +2118,7 @@ val inventoryLotLine = illId?.let { inventoryLotLineMap[it] }
status = statusEnum,
etaStart = etaStart,
etaEnd = etaEnd,
isEtra = isEtra,
allowedSupplierCodes = allowedSupplierCodes,
pageable = PageRequest.of(dbPage, 500),
)
@@ -2179,6 +2194,7 @@ val inventoryLotLine = illId?.let { inventoryLotLineMap[it] }
supplierName = info.supplierName,
shopAddress = info.shopAddress,
truckLanceCode = calculatedTruckLanceCode,
isEtra = deliveryOrder?.isEtra ?: info.isEtra,
)
}
}


+ 3
- 0
src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/DeliveryOrderController.kt Просмотреть файл

@@ -72,6 +72,7 @@ class DeliveryOrderController(
pageSize = request.pageSize,
truckLanceCode = request.truckLanceCode,
floor = request.floor,
isEtra = request.isEtra,
)
}

@@ -88,6 +89,7 @@ class DeliveryOrderController(
pageNum = request.pageNum,
pageSize = request.pageSize,
floor = request.floor,
isEtra = request.isEtra,
)
}

@@ -106,6 +108,7 @@ class DeliveryOrderController(
pageSize = request.pageSize,
truckLanceCode = request.truckLanceCode,
floor = request.floor,
isEtra = request.isEtra,
)
}



+ 2
- 0
src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/models/DoDetailResponse.kt Просмотреть файл

@@ -18,6 +18,8 @@ data class DoDetailResponse(
@JsonFormat(pattern = "yyyy-MM-dd")
val completeDate: LocalDateTime?,
val status: String?,
/** 加單 DO(M18 加單專用同步) */
val isEtra: Boolean = false,
val deliveryOrderLines: List<DoDetailLineResponse>
)



+ 2
- 0
src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/models/ReleaseDoRequest.kt Просмотреть файл

@@ -33,4 +33,6 @@ data class SearchDeliveryOrderInfoRequest(
val truckLanceCode: String?,
/** `ALL`/`All`/null:P06B+P07+P06D;`2F`:P07+P06D;`4F`:P06B。車線-X 亦依供應商歸屬出現在對應樓層。 */
val floor: String? = null,
/** null:不篩 isEtra;true/false:只顯示加單或非加單 DO */
val isEtra: Boolean? = null,
)

+ 1
- 0
src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/models/SaveDeliveryOrderRequest.kt Просмотреть файл

@@ -20,6 +20,7 @@ data class SaveDeliveryOrderRequest(
val handlerId: Long?,
val m18BeId: Long?,
val deleted: Boolean? = false,
val isEtra: Boolean? = false,
)

data class SaveDeliveryOrderStatusRequest(


+ 5
- 0
src/main/resources/db/changelog/changes/20260508_01_fai/01_delivery_order_is_etra.sql Просмотреть файл

@@ -0,0 +1,5 @@
-- liquibase formatted sql
-- changeset fpsms:20260508_delivery_order_is_etra

ALTER TABLE `delivery_order`
ADD COLUMN `isEtra` TINYINT(1) NOT NULL DEFAULT 0 AFTER `m18BeId`;

Загрузка…
Отмена
Сохранить