[RELEASE] 晉升 demo 到 main 正式環境

1. 實作機台貨道類型與商品容量上限的雙向連動同步機制。

2. 整理 .gitea/workflows/deploy-demo.yaml 部署工作流,移除多餘的空白行。
This commit is contained in:
sky121113 2026-06-03 16:25:36 +08:00
commit 7f9958ad7e
6 changed files with 112 additions and 19 deletions

View File

@ -34,7 +34,6 @@ jobs:
docker run --rm -v /workspace/mama/star-cloud:/target busybox mkdir -p /target/docker/emqx
cat docker/emqx/acl.conf | docker run -i --rm -v /workspace/mama/star-cloud:/target busybox sh -c "cat > /target/docker/emqx/acl.conf"
- name: Step 1 - Build & Deploy Containers
run: |
WWWGROUP=1000 WWWUSER=1000 docker compose -f compose.yaml up -d --build --wait

Binary file not shown.

Binary file not shown.

View File

@ -43,6 +43,17 @@ class Machine extends Model
}
}
}
if ($machine->wasChanged([
'is_spring_slot_1_10',
'is_spring_slot_11_20',
'is_spring_slot_21_30',
'is_spring_slot_31_40',
'is_spring_slot_41_50',
'is_spring_slot_51_60',
])) {
app(\App\Services\Machine\MachineService::class)->syncMachineSlotMaxStock($machine);
}
});
}

View File

@ -5,11 +5,21 @@ namespace App\Models\Product;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use App\Services\Machine\MachineService;
use App\Traits\TenantScoped;
class Product extends Model
{
use HasFactory, SoftDeletes, TenantScoped;
protected static function booted()
{
static::updated(function (Product $product) {
if ($product->wasChanged(['track_limit', 'spring_limit'])) {
app(MachineService::class)->syncProductSlotMaxStock($product);
}
});
}
/**
* Scope a query to only include active products.

View File

@ -7,6 +7,7 @@ use App\Models\Machine\MachineLog;
use App\Models\Machine\MachineSlot;
use App\Models\Machine\MachineStockMovement;
use App\Models\Machine\RemoteCommand;
use App\Models\Product\Product;
use App\Models\Transaction\Order;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
@ -14,6 +15,15 @@ use Carbon\Carbon;
class MachineService
{
private const SPRING_SLOT_FLAGS = [
[1, 10, 'is_spring_slot_1_10'],
[11, 20, 'is_spring_slot_11_20'],
[21, 30, 'is_spring_slot_21_30'],
[31, 40, 'is_spring_slot_31_40'],
[41, 50, 'is_spring_slot_41_50'],
[51, 60, 'is_spring_slot_51_60'],
];
/**
* B013: 硬體狀態代碼對照表 (Hardware Status Code Mapping)
*
@ -278,15 +288,12 @@ class MachineService
})?->id;
}
// 根據貨道類型自動決定上限 (Auto-calculate max_stock based on slot type)
// 若未提供 type預設為 '1' (履帶/Track)
$slotType = $slotData['type'] ?? $existingSlot->type ?? '1';
// 根據機台貨道機制設定決定上限與貨道類型 (1: 履帶, 2: 彈簧)
$slotType = $this->resolveSlotType($machine, $slotNo);
if ($actualProductId) {
$product = $products->find($actualProductId);
if ($product) {
// 1: 履帶, 2: 彈簧
$calculatedMaxStock = ($slotType == '1') ? $product->track_limit : $product->spring_limit;
$slotData['capacity'] = $calculatedMaxStock ?? $slotData['capacity'] ?? null;
$slotData['capacity'] = $this->calculateSlotMaxStock($product, $machine, $slotNo);
}
}
@ -307,22 +314,26 @@ class MachineService
// 檢查是否有實質變動 (Check for actual changes)
$isStockChanged = ($oldStock !== $newStock);
$isProductChanged = ($oldProductId != $actualProductId); // 這裡用鬆散比對以處理 null/0 的情況
$isSlotConfigChanged = (string) $existingSlot->type !== (string) $updateData['type']
|| (int) $existingSlot->max_stock !== (int) $updateData['max_stock'];
if ($isStockChanged || $isProductChanged) {
if ($isStockChanged || $isProductChanged || $isSlotConfigChanged) {
$existingSlot->update($updateData);
$existingSlot->refresh();
$delta = $newStock - $oldStock;
$note = $isProductChanged ? "movement.note.product_changed_and_adjusted" : "movement.note.replenishment_correction";
$this->recordStockMovement(
$existingSlot,
$delta,
MachineStockMovement::TYPE_ADJUSTMENT,
null,
$note,
['slot_no' => $existingSlot->slot_no, 'old' => $oldStock, 'new' => $newStock, 'old_product_id' => $oldProductId, 'new_product_id' => $actualProductId]
);
if ($isStockChanged || $isProductChanged) {
$delta = $newStock - $oldStock;
$note = $isProductChanged ? "movement.note.product_changed_and_adjusted" : "movement.note.replenishment_correction";
$this->recordStockMovement(
$existingSlot,
$delta,
MachineStockMovement::TYPE_ADJUSTMENT,
null,
$note,
['slot_no' => $existingSlot->slot_no, 'old' => $oldStock, 'new' => $newStock, 'old_product_id' => $oldProductId, 'new_product_id' => $actualProductId]
);
}
}
} else {
$newSlot = $machine->slots()->create(array_merge($updateData, ['slot_no' => $slotNo]));
@ -378,6 +389,68 @@ class MachineService
});
}
public function syncProductSlotMaxStock(Product $product): void
{
MachineSlot::query()
->where('product_id', $product->id)
->with(['machine' => fn($query) => $query->withoutGlobalScopes()])
->chunkById(500, function ($slots) use ($product) {
foreach ($slots as $slot) {
if (!$slot->machine) {
continue;
}
$slot->update([
'type' => $this->resolveSlotType($slot->machine, $slot->slot_no),
'max_stock' => $this->calculateSlotMaxStock($product, $slot->machine, $slot->slot_no),
]);
}
});
}
public function syncMachineSlotMaxStock(Machine $machine): void
{
$machine->slots()
->with(['product' => fn($query) => $query->withoutGlobalScopes()])
->chunkById(500, function ($slots) use ($machine) {
foreach ($slots as $slot) {
if (!$slot->product) {
continue;
}
$slot->update([
'type' => $this->resolveSlotType($machine, $slot->slot_no),
'max_stock' => $this->calculateSlotMaxStock($slot->product, $machine, $slot->slot_no),
]);
}
});
}
public function calculateSlotMaxStock(Product $product, Machine $machine, string|int|null $slotNo): int
{
return $this->isSpringSlot($machine, $slotNo)
? (int) $product->spring_limit
: (int) $product->track_limit;
}
public function resolveSlotType(Machine $machine, string|int|null $slotNo): string
{
return $this->isSpringSlot($machine, $slotNo) ? '2' : '1';
}
public function isSpringSlot(Machine $machine, string|int|null $slotNo): bool
{
$slotNumber = (int) $slotNo;
foreach (self::SPRING_SLOT_FLAGS as [$start, $end, $field]) {
if ($slotNumber >= $start && $slotNumber <= $end) {
return (bool) $machine->{$field};
}
}
return false;
}
/**
* Update machine slot stock, expiry, and batch.
*