170 lines
6.7 KiB
PHP
170 lines
6.7 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Admin\Product;
|
|
|
|
use App\Repositories\Admin\Product\AdminPinRepository;
|
|
use Illuminate\Support\Facades\Crypt;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
final class AdminPinService
|
|
{
|
|
public function __construct(
|
|
private readonly AdminPinRepository $repo,
|
|
) {}
|
|
|
|
public function getPinsBySku(int $skuId, array $filters): array
|
|
{
|
|
return [
|
|
'pins' => $this->repo->paginatePins($skuId, $filters),
|
|
'stats' => $this->repo->getPinStats($skuId),
|
|
];
|
|
}
|
|
|
|
public function recallPins(int $skuId, string $amountType, int $customAmount, string $zipPassword): array
|
|
{
|
|
// 1. 회수할 수량 결정
|
|
$limit = ($amountType === 'ALL') ? PHP_INT_MAX : $customAmount;
|
|
if ($limit <= 0) return ['ok' => false, 'message' => '회수할 수량을 1 이상 입력해주세요.'];
|
|
|
|
DB::beginTransaction();
|
|
try {
|
|
// 2. 가장 나중에 등록된(ID가 큰) AVAILABLE 핀들 가져오기 (LIFO)
|
|
$pinsToRecall = DB::table('gc_pins')
|
|
->where('sku_id', $skuId)
|
|
->where('status', 'AVAILABLE')
|
|
->orderByDesc('id')
|
|
->limit($limit)
|
|
->get();
|
|
|
|
if ($pinsToRecall->isEmpty()) {
|
|
DB::rollBack();
|
|
return ['ok' => false, 'message' => '회수할 수 있는 판매대기(AVAILABLE) 핀이 없습니다.'];
|
|
}
|
|
|
|
// 3. 상태를 'RECALLED'로 업데이트
|
|
$pinIds = $pinsToRecall->pluck('id')->toArray();
|
|
DB::table('gc_pins')->whereIn('id', $pinIds)->update([
|
|
'status' => 'RECALLED',
|
|
'updated_at' => now()->format('Y-m-d H:i:s')
|
|
]);
|
|
|
|
// 4. 복호화하여 CSV 데이터 텍스트 생성
|
|
// 엑셀에서 한글 깨짐 방지를 위해 BOM(\xEF\xBB\xBF) 추가
|
|
$csvData = "\xEF\xBB\xBF" . "ID,PIN_CODE,FACE_VALUE,BUY_PRICE\n";
|
|
foreach($pinsToRecall as $pin) {
|
|
try {
|
|
$decrypted = \Illuminate\Support\Facades\Crypt::decryptString($pin->pin_code);
|
|
$csvData .= "{$pin->id},{$decrypted},{$pin->face_value},{$pin->buy_price}\n";
|
|
} catch (\Exception $e) {
|
|
$csvData .= "{$pin->id},DECRYPT_ERROR,{$pin->face_value},{$pin->buy_price}\n";
|
|
}
|
|
}
|
|
|
|
// 5. ZIP 파일 생성 (라라벨 storage/app/public 임시 저장)
|
|
$fileName = "recalled_pins_sku_{$skuId}_" . date('YmdHis') . ".zip";
|
|
$zipPath = storage_path('app/public/' . $fileName);
|
|
|
|
$zip = new \ZipArchive();
|
|
if ($zip->open($zipPath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) === true) {
|
|
$zip->addFromString('recalled_pins.csv', $csvData);
|
|
// 비밀번호 암호화 세팅 (PHP 7.2 이상 지원)
|
|
$zip->setPassword($zipPassword);
|
|
$zip->setEncryptionName('recalled_pins.csv', \ZipArchive::EM_AES_256);
|
|
$zip->close();
|
|
} else {
|
|
DB::rollBack();
|
|
return ['ok' => false, 'message' => 'ZIP 파일 생성에 실패했습니다. (서버의 php-zip 확장을 확인하세요)'];
|
|
}
|
|
|
|
DB::commit();
|
|
return ['ok' => true, 'file_path' => $zipPath, 'file_name' => $fileName, 'count' => count($pinIds)];
|
|
|
|
} catch (\Throwable $e) {
|
|
DB::rollBack();
|
|
return ['ok' => false, 'message' => '회수 처리 중 오류가 발생했습니다: ' . $e->getMessage()];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 텍스트 대량 핀 등록 로직
|
|
*/
|
|
public function bulkRegisterPins(int $productId, int $skuId, string $bulkText, string $expiryDate = null): array
|
|
{
|
|
// 1. 텍스트를 줄바꿈 단위로 분리 (빈 줄 제거)
|
|
$lines = array_filter(array_map('trim', explode("\n", $bulkText)));
|
|
|
|
if (empty($lines)) {
|
|
return ['ok' => false, 'message' => '입력된 핀 정보가 없습니다.'];
|
|
}
|
|
|
|
$insertData = [];
|
|
$now = now()->format('Y-m-d H:i:s');
|
|
$successCount = 0;
|
|
$duplicateCount = 0;
|
|
$errorLines = 0;
|
|
|
|
foreach ($lines as $line) {
|
|
// 2. 콤마(,) 또는 탭(\t)으로 데이터 분리 (예: 핀번호, 정액, 원가)
|
|
// 정규식을 사용해 콤마나 탭 여러 개를 하나로 취급
|
|
$parts = preg_split('/[\t,]+/', $line);
|
|
|
|
// 데이터 파싱 (최소한 핀번호는 있어야 함)
|
|
$pinCode = trim($parts[0] ?? '');
|
|
if (empty($pinCode)) continue;
|
|
|
|
$faceValue = isset($parts[1]) ? (int)preg_replace('/[^0-9]/', '', $parts[1]) : 0;
|
|
$buyPrice = isset($parts[2]) ? (int)preg_replace('/[^0-9]/', '', $parts[2]) : 0;
|
|
|
|
// 3. 마진율 계산 (정액금액이 0보다 클 때만 계산)
|
|
$marginRate = 0.00;
|
|
if ($faceValue > 0) {
|
|
$marginRate = round((($faceValue - $buyPrice) / $faceValue) * 100, 2);
|
|
}
|
|
|
|
// 4. 보안 처리 (암호화 및 해시)
|
|
$encryptedPin = Crypt::encryptString($pinCode);
|
|
$pinHash = hash('sha256', $pinCode); // 중복 검사용 해시
|
|
|
|
$insertData[] = [
|
|
'product_id' => $productId,
|
|
'sku_id' => $skuId,
|
|
'pin_code' => $encryptedPin,
|
|
'pin_hash' => $pinHash,
|
|
'status' => 'AVAILABLE',
|
|
'face_value' => $faceValue,
|
|
'buy_price' => $buyPrice,
|
|
'margin_rate' => $marginRate,
|
|
'expiry_date' => $expiryDate ?: null,
|
|
'created_at' => $now,
|
|
'updated_at' => $now,
|
|
];
|
|
}
|
|
|
|
// 5. 500개씩 청크(Chunk)로 나누어 일괄 Insert (서버 과부하 방지)
|
|
DB::beginTransaction();
|
|
try {
|
|
$chunks = array_chunk($insertData, 500);
|
|
$totalInserted = 0;
|
|
|
|
foreach ($chunks as $chunk) {
|
|
// insertOrIgnore 덕분에 중복 핀은 무시되고 성공한 갯수만 반환됨
|
|
$insertedRows = $this->repo->insertPinsBulk($chunk);
|
|
$totalInserted += $insertedRows;
|
|
}
|
|
|
|
DB::commit();
|
|
|
|
$duplicateCount = count($insertData) - $totalInserted;
|
|
|
|
return [
|
|
'ok' => true,
|
|
'message' => "총 {$totalInserted}개의 핀이 등록되었습니다. (중복 제외: {$duplicateCount}개)"
|
|
];
|
|
|
|
} catch (\Throwable $e) {
|
|
DB::rollBack();
|
|
return ['ok' => false, 'message' => '대량 등록 중 DB 오류가 발생했습니다: ' . $e->getMessage()];
|
|
}
|
|
}
|
|
}
|