Browse Source

VIP名单增删改查,导入导出下载模板

master
wanghongjun 2 weeks ago
parent
commit
e1d67476ca
  1. 18
      app/Exports/AdminVipListExport.php
  2. 20
      app/Exports/AdminVipListImportTemplateExport.php
  3. 260
      app/Http/Controllers/Admin/VipListController.php
  4. 24
      app/Imports/AdminVipListImport.php
  5. 9
      app/Models/AdminUsers.php
  6. 25
      app/Models/AdminVipList.php
  7. 131
      app/Services/AdminVipListService.php
  8. 380
      config/excel.php
  9. 8
      resources/lang/zh-CN/exports.php

18
app/Exports/AdminVipListExport.php

@ -0,0 +1,18 @@
<?php
namespace App\Exports;
use App\Models\AdminVipList;
use Illuminate\Database\Eloquent\Collection;
use Maatwebsite\Excel\Concerns\FromCollection;
class AdminVipListExport implements FromCollection
{
/**
* @return Collection
*/
public function collection(): Collection
{
return AdminVipList::all();
}
}

20
app/Exports/AdminVipListImportTemplateExport.php

@ -0,0 +1,20 @@
<?php
namespace App\Exports;
use Maatwebsite\Excel\Concerns\FromCollection;
class AdminVipListImportTemplateExport implements FromCollection
{
/**
* @return array
*/
public function collection(): array
{
return [
[__('exports.vip_list.license')],
['粤B88888']
];
}
}

260
app/Http/Controllers/Admin/VipListController.php

@ -0,0 +1,260 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Exceptions\CustomException;
use App\Exports\AdminVipListExport;
use App\Exports\AdminVipListImportTemplateExport;
use App\Imports\AdminVipListImport;
use App\Models\AdminUsers;
use App\Models\AdminVipList;
use App\Services\AdminVipListService;
use App\Services\ApiResponseService;
use Exception;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;
use Maatwebsite\Excel\Facades\Excel;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
class VipListController extends BaseController
{
/**
* @var ApiResponseService
*/
protected ApiResponseService $responseService;
/**
* @var AdminVipListService
*/
protected AdminVipListService $vipListService;
/**
* @param ApiResponseService $responseService
* @param AdminVipListService $vipListService
*/
public function __construct(
ApiResponseService $responseService,
AdminVipListService $vipListService
) {
$this->responseService = $responseService;
$this->vipListService = $vipListService;
}
/**
* @param Request $request
* @return JsonResponse
*/
public function index(Request $request): JsonResponse
{
try {
$query = AdminVipList::query();
// 分页
$page = $request->input('page', 1);
$perPage = $request->input('per_page', 10);
$total = $query->count();
$items = $query->latest()->forPage($page, $perPage)->get()->each(
function ($item) {
$item['username'] = AdminUsers::getUsername(
$item['user_id']
);
return $item;
}
);
return $this->responseService->success([
'items' => $items,
'total' => $total,
'page' => $page,
'per_page' => $perPage,
'last_page' => ceil($total / $perPage),
]);
} catch (Exception $e) {
$m_prefix = __('exception.get_user_info_list_failed');
return $this->responseService->systemError(
$m_prefix . ':' . $e->getMessage()
);
}
}
/**
* @param Request $request
* @param string $id
* @return JsonResponse
* @throws CustomException
* @throws ValidationException
*/
public function update(Request $request, string $id): JsonResponse
{
try {
$this->saveValidator($request->all(), $id);
$this->vipListService->updateModel($request->all(), $id);
return $this->responseService->success(
null,
__('admin.update_succeeded')
);
} catch (ValidationException|CustomException $e) {
throw $e;
} catch (Exception $e) {
return $this->responseService->systemError(
__('exception.admin_vip_list.update_failed') . ':'
. $e->getMessage()
);
}
}
/**
* @param array $data
* @param int $id
* @return void
* @throws ValidationException
*/
protected function saveValidator(array $data, int $id = 0): void
{
$rules = [
'license' => 'required|max:20'
];
$messages = [
'license.required' => __('validation.admin_list_vip.l_empty'),
'license.max' => __('validation.admin_list_vip.l_max')
];
if ($id) {
$this->validateId($id, AdminVipList::class);
}
$validator = Validator::make($data, $rules, $messages);
if ($validator->fails()) {
throw new ValidationException($validator);
}
}
/**
* @param string $id
* @return JsonResponse
* @throws CustomException
* @throws ValidationException
*/
public function destroy(string $id): JsonResponse
{
try {
$this->validateId($id, AdminVipList::class);
$this->vipListService->deleteModel($id);
return $this->responseService->success(
null,
__('admin.delete_succeeded')
);
} catch (ValidationException|CustomException $e) {
throw $e;
} catch (Exception $e) {
return $this->responseService->systemError(
__('exception.admin_vip_list.destroy_failed') . ':'
. $e->getMessage()
);
}
}
/**
* @param Request $request
* @return JsonResponse
* @throws ValidationException
*/
public function import(Request $request): JsonResponse
{
try {
// 1. 验证上传的文件
$request->validate([
'file' => 'required|mimes:xlsx,xls,csv|max:2048' // 限制文件类型和大小
]);
$validator = Validator::make($request->file('file'), [
'file' => 'required|mimes:xlsx,xls,csv|max:2048'
], [
'file.required' => __('validation.admin_list_vip.file_empty'),
'file.mimes' => __('validation.admin_list_vip.file_mimes'),
'file.max' => __('validation.admin_list_vip.file_max'),
]);
if ($validator->fails()) {
throw new ValidationException($validator);
}
// 2. 获取上传的文件
$file = $request->file('file');
// 3. 正确的做法:先存储文件,再获取绝对路径进行导入
// 将文件存储到 storage/app/imports 目录下
$path = $file->store('imports');
// 4. 执行导入(使用存储后的绝对路径)
// storage_path('app') 获取 storage/app 的绝对路径
Excel::import(
new AdminVipListImport(),
storage_path('app/' . $path)
);
// 5. (可选)导入完成后删除临时文件
Storage::delete($path);
return $this->responseService->systemError(
__('controller.import.success')
);
} catch (ValidationException $e) {
throw $e;
} catch (Exception $e) {
return $this->responseService->systemError(
__('exception.admin_vip_list.import_failed') . ':'
. $e->getMessage()
);
}
}
/**
* @param Request $request
* @return JsonResponse
* @throws CustomException
* @throws ValidationException
*/
public function store(Request $request): JsonResponse
{
try {
$this->saveValidator($request->all());
$this->vipListService->createModel($request->all());
return $this->responseService->success(
null,
__('admin.save_succeeded')
);
} catch (ValidationException|CustomException $e) {
throw $e;
} catch (Exception $e) {
return $this->responseService->systemError(
__('exception.admin_vip_list.create_failed') . ':'
. $e->getMessage()
);
}
}
/**
* @return BinaryFileResponse
*/
public function importTemplate(): BinaryFileResponse
{
return Excel::download(
new AdminVipListImportTemplateExport(),
__('exports.vip_list.import_template') . '.xlsx'
);
}
/**
* @return BinaryFileResponse
*/
public function export(): BinaryFileResponse
{
return Excel::download(
new AdminVipListExport(),
__('exports.vip_list.list') . '.xlsx'
);
}
}

24
app/Imports/AdminVipListImport.php

@ -0,0 +1,24 @@
<?php
namespace App\Imports;
use App\Models\AdminVipList;
use Illuminate\Support\Facades\Auth;
use Maatwebsite\Excel\Concerns\ToModel;
use Maatwebsite\Excel\Concerns\WithHeadingRow;
class AdminVipListImport implements ToModel, WithHeadingRow
{
/**
* @param array $row
* @return AdminVipList
*/
public function model(array $row): AdminVipList
{
return new AdminVipList([
'license' => $row['license'],
'user_id' => Auth::guard('sanctum')->user()['id'],
'created_at' => get_datetime()
]);
}
}

9
app/Models/AdminUsers.php

@ -53,4 +53,13 @@ class AdminUsers extends Model
{ {
return $this->belongsToMany(AdminRoles::class, AdminRoleUsers::class, 'user_id', 'role_id'); return $this->belongsToMany(AdminRoles::class, AdminRoleUsers::class, 'user_id', 'role_id');
} }
/**
* @param $user_id
* @return mixed
*/
public static function getUsername($user_id): mixed
{
return self::query()->where('id', $user_id)->value('username');
}
} }

25
app/Models/AdminVipList.php

@ -0,0 +1,25 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class AdminVipList extends Model
{
use HasFactory, SoftDeletes;
protected $table = 'admin_vip_list';
protected $fillable = [
'license',
'user_id'
];
protected $hidden = [
'deleted_at',
'updated_at',
'created_at'
];
}

131
app/Services/AdminVipListService.php

@ -0,0 +1,131 @@
<?php
namespace App\Services;
use App\Models\AdminVipList;
use Exception;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
class AdminVipListService
{
/**
* @var OperationLogService
*/
private OperationLogService $logService;
/**
* 构造函数
* @param OperationLogService $logService
*/
public function __construct(OperationLogService $logService)
{
$this->logService = $logService;
}
/**
* @param array $data
* @return Model|Builder
* @throws Exception
*/
public function createModel(array $data): Model|Builder
{
try {
DB::beginTransaction();
if (AdminVipList::query()->where('license', $data['license'])
->exists()
) {
throw new Exception(
__('service.admin_vip_list.license_exists')
);
}
$model = AdminVipList::query()->create([
'license' => $data['license'],
'user_id' => Auth::guard('sanctum')->user()['id'],
'created_at' => get_datetime()
]);
$this->logService->logCreated($model, '创建VIP名单');
DB::commit();
return $model;
} catch (Exception $e) {
DB::rollBack();
throw $e;
}
}
/**
* @param array $data
* @param int $id
* @return Model|Builder
* @throws Exception
*/
public function updateModel(array $data, int $id): Model|Builder
{
try {
DB::beginTransaction();
// 验证
$existsWhere = [
['license', '=', $data['license']],
['id', '<>', $id]
];
if (AdminVipList::query()->where($existsWhere)->exists()) {
throw new Exception(__('service.admin_vip_list.license_exists'));
}
// 更新
$model = AdminVipList::query()->findOrFail($id);
$oldValues = $model->toArray();
$model->update([
'license' => $data['license'],
'user_id' => Auth::guard('sanctum')->user()['id'],
'updated_at' => get_datetime()
]);
$this->logService->logUpdated($model, $oldValues, '更新VIP名单');
DB::commit();
return $model;
} catch (Exception $e) {
DB::rollBack();
throw $e;
}
}
/**
* @param $id
* @return bool
* @throws Exception
*/
public function deleteModel($id): bool
{
try {
DB::beginTransaction();
$model = AdminVipList::query()->findOrFail($id);
$this->logService->logDeleted($model, '删除VIP名单');
$model->delete();
DB::commit();
return true;
} catch (Exception $e) {
DB::rollBack();
throw $e;
}
}
}

380
config/excel.php

@ -0,0 +1,380 @@
<?php
use Maatwebsite\Excel\Excel;
use PhpOffice\PhpSpreadsheet\Reader\Csv;
return [
'exports' => [
/*
|--------------------------------------------------------------------------
| Chunk size
|--------------------------------------------------------------------------
|
| When using FromQuery, the query is automatically chunked.
| Here you can specify how big the chunk should be.
|
*/
'chunk_size' => 1000,
/*
|--------------------------------------------------------------------------
| Pre-calculate formulas during export
|--------------------------------------------------------------------------
*/
'pre_calculate_formulas' => false,
/*
|--------------------------------------------------------------------------
| Enable strict null comparison
|--------------------------------------------------------------------------
|
| When enabling strict null comparison empty cells ('') will
| be added to the sheet.
*/
'strict_null_comparison' => false,
/*
|--------------------------------------------------------------------------
| CSV Settings
|--------------------------------------------------------------------------
|
| Configure e.g. delimiter, enclosure and line ending for CSV exports.
|
*/
'csv' => [
'delimiter' => ',',
'enclosure' => '"',
'line_ending' => PHP_EOL,
'use_bom' => false,
'include_separator_line' => false,
'excel_compatibility' => false,
'output_encoding' => '',
'test_auto_detect' => true,
],
/*
|--------------------------------------------------------------------------
| Worksheet properties
|--------------------------------------------------------------------------
|
| Configure e.g. default title, creator, subject,...
|
*/
'properties' => [
'creator' => '',
'lastModifiedBy' => '',
'title' => '',
'description' => '',
'subject' => '',
'keywords' => '',
'category' => '',
'manager' => '',
'company' => '',
],
],
'imports' => [
/*
|--------------------------------------------------------------------------
| Read Only
|--------------------------------------------------------------------------
|
| When dealing with imports, you might only be interested in the
| data that the sheet exists. By default we ignore all styles,
| however if you want to do some logic based on style data
| you can enable it by setting read_only to false.
|
*/
'read_only' => true,
/*
|--------------------------------------------------------------------------
| Ignore Empty
|--------------------------------------------------------------------------
|
| When dealing with imports, you might be interested in ignoring
| rows that have null values or empty strings. By default rows
| containing empty strings or empty values are not ignored but can be
| ignored by enabling the setting ignore_empty to true.
|
*/
'ignore_empty' => false,
/*
|--------------------------------------------------------------------------
| Heading Row Formatter
|--------------------------------------------------------------------------
|
| Configure the heading row formatter.
| Available options: none|slug|custom
|
*/
'heading_row' => [
'formatter' => 'slug',
],
/*
|--------------------------------------------------------------------------
| CSV Settings
|--------------------------------------------------------------------------
|
| Configure e.g. delimiter, enclosure and line ending for CSV imports.
|
*/
'csv' => [
'delimiter' => null,
'enclosure' => '"',
'escape_character' => '\\',
'contiguous' => false,
'input_encoding' => Csv::GUESS_ENCODING,
],
/*
|--------------------------------------------------------------------------
| Worksheet properties
|--------------------------------------------------------------------------
|
| Configure e.g. default title, creator, subject,...
|
*/
'properties' => [
'creator' => '',
'lastModifiedBy' => '',
'title' => '',
'description' => '',
'subject' => '',
'keywords' => '',
'category' => '',
'manager' => '',
'company' => '',
],
/*
|--------------------------------------------------------------------------
| Cell Middleware
|--------------------------------------------------------------------------
|
| Configure middleware that is executed on getting a cell value
|
*/
'cells' => [
'middleware' => [
//\Maatwebsite\Excel\Middleware\TrimCellValue::class,
//\Maatwebsite\Excel\Middleware\ConvertEmptyCellValuesToNull::class,
],
],
],
/*
|--------------------------------------------------------------------------
| Extension detector
|--------------------------------------------------------------------------
|
| Configure here which writer/reader type should be used when the package
| needs to guess the correct type based on the extension alone.
|
*/
'extension_detector' => [
'xlsx' => Excel::XLSX,
'xlsm' => Excel::XLSX,
'xltx' => Excel::XLSX,
'xltm' => Excel::XLSX,
'xls' => Excel::XLS,
'xlt' => Excel::XLS,
'ods' => Excel::ODS,
'ots' => Excel::ODS,
'slk' => Excel::SLK,
'xml' => Excel::XML,
'gnumeric' => Excel::GNUMERIC,
'htm' => Excel::HTML,
'html' => Excel::HTML,
'csv' => Excel::CSV,
'tsv' => Excel::TSV,
/*
|--------------------------------------------------------------------------
| PDF Extension
|--------------------------------------------------------------------------
|
| Configure here which Pdf driver should be used by default.
| Available options: Excel::MPDF | Excel::TCPDF | Excel::DOMPDF
|
*/
'pdf' => Excel::DOMPDF,
],
/*
|--------------------------------------------------------------------------
| Value Binder
|--------------------------------------------------------------------------
|
| PhpSpreadsheet offers a way to hook into the process of a value being
| written to a cell. In there some assumptions are made on how the
| value should be formatted. If you want to change those defaults,
| you can implement your own default value binder.
|
| Possible value binders:
|
| [x] Maatwebsite\Excel\DefaultValueBinder::class
| [x] PhpOffice\PhpSpreadsheet\Cell\StringValueBinder::class
| [x] PhpOffice\PhpSpreadsheet\Cell\AdvancedValueBinder::class
|
*/
'value_binder' => [
'default' => Maatwebsite\Excel\DefaultValueBinder::class,
],
'cache' => [
/*
|--------------------------------------------------------------------------
| Default cell caching driver
|--------------------------------------------------------------------------
|
| By default PhpSpreadsheet keeps all cell values in memory, however when
| dealing with large files, this might result into memory issues. If you
| want to mitigate that, you can configure a cell caching driver here.
| When using the illuminate driver, it will store each value in the
| cache store. This can slow down the process, because it needs to
| store each value. You can use the "batch" store if you want to
| only persist to the store when the memory limit is reached.
|
| Drivers: memory|illuminate|batch
|
*/
'driver' => 'memory',
/*
|--------------------------------------------------------------------------
| Batch memory caching
|--------------------------------------------------------------------------
|
| When dealing with the "batch" caching driver, it will only
| persist to the store when the memory limit is reached.
| Here you can tweak the memory limit to your liking.
|
*/
'batch' => [
'memory_limit' => 60000,
],
/*
|--------------------------------------------------------------------------
| Illuminate cache
|--------------------------------------------------------------------------
|
| When using the "illuminate" caching driver, it will automatically use
| your default cache store. However if you prefer to have the cell
| cache on a separate store, you can configure the store name here.
| You can use any store defined in your cache config. When leaving
| at "null" it will use the default store.
|
*/
'illuminate' => [
'store' => null,
],
/*
|--------------------------------------------------------------------------
| Cache Time-to-live (TTL)
|--------------------------------------------------------------------------
|
| The TTL of items written to cache. If you want to keep the items cached
| indefinitely, set this to null. Otherwise, set a number of seconds,
| a \DateInterval, or a callable.
|
| Allowable types: callable|\DateInterval|int|null
|
*/
'default_ttl' => 10800,
],
/*
|--------------------------------------------------------------------------
| Transaction Handler
|--------------------------------------------------------------------------
|
| By default the import is wrapped in a transaction. This is useful
| for when an import may fail and you want to retry it. With the
| transactions, the previous import gets rolled-back.
|
| You can disable the transaction handler by setting this to null.
| Or you can choose a custom made transaction handler here.
|
| Supported handlers: null|db
|
*/
'transactions' => [
'handler' => 'db',
'db' => [
'connection' => null,
],
],
'temporary_files' => [
/*
|--------------------------------------------------------------------------
| Local Temporary Path
|--------------------------------------------------------------------------
|
| When exporting and importing files, we use a temporary file, before
| storing reading or downloading. Here you can customize that path.
| permissions is an array with the permission flags for the directory (dir)
| and the create file (file).
|
*/
'local_path' => storage_path('framework/cache/laravel-excel'),
/*
|--------------------------------------------------------------------------
| Local Temporary Path Permissions
|--------------------------------------------------------------------------
|
| Permissions is an array with the permission flags for the directory (dir)
| and the create file (file).
| If omitted the default permissions of the filesystem will be used.
|
*/
'local_permissions' => [
// 'dir' => 0755,
// 'file' => 0644,
],
/*
|--------------------------------------------------------------------------
| Remote Temporary Disk
|--------------------------------------------------------------------------
|
| When dealing with a multi server setup with queues in which you
| cannot rely on having a shared local temporary path, you might
| want to store the temporary file on a shared disk. During the
| queue executing, we'll retrieve the temporary file from that
| location instead. When left to null, it will always use
| the local path. This setting only has effect when using
| in conjunction with queued imports and exports.
|
*/
'remote_disk' => null,
'remote_prefix' => null,
/*
|--------------------------------------------------------------------------
| Force Resync
|--------------------------------------------------------------------------
|
| When dealing with a multi server setup as above, it's possible
| for the clean up that occurs after entire queue has been run to only
| cleanup the server that the last AfterImportJob runs on. The rest of the server
| would still have the local temporary file stored on it. In this case your
| local storage limits can be exceeded and future imports won't be processed.
| To mitigate this you can set this config value to be true, so that after every
| queued chunk is processed the local temporary file is deleted on the server that
| processed it.
|
*/
'force_resync_remote' => null,
],
];

8
resources/lang/zh-CN/exports.php

@ -0,0 +1,8 @@
<?php
return [
'vip_list' => [
'license' => '车牌号码',
'import_template' => 'VIP名单导入模板',
'list' => 'VIP名单'
]
];
Loading…
Cancel
Save