App版本管理

main
Jing Li 2024-04-30 15:17:51 +08:00
parent 43979c5a67
commit bf648a0a43
10 changed files with 553 additions and 2 deletions

View File

@ -0,0 +1,132 @@
<?php
namespace App\Admin\Controllers;
use App\Admin\Controllers\AdminController;
use App\Admin\Services\AppVersionService;
use App\Enums\AppOs;
use App\Enums\AppUpdateStrategy;
use Slowlyo\OwlAdmin\Admin;
use Slowlyo\OwlAdmin\Renderers\Form;
use Slowlyo\OwlAdmin\Renderers\Operation;
use Slowlyo\OwlAdmin\Renderers\Page;
/**
* @property AppVersionService $service
*/
class AppVersionController extends AdminController
{
protected string $serviceName = AppVersionService::class;
public function list(): Page
{
$crud = $this->baseCRUD()->tableLayout('fixed')
->headerToolbar([
$this->createTypeButton('drawer', 'lg')->visible(Admin::user()->can('admin.app_versions.create')),
amis('reload')->align('right'),
])
->bulkActions([])
->columns([
amis()->TableColumn('name', __('app_version.name')),
amis()->TableColumn('version', __('app_version.version')),
amis()->TableColumn('title', __('app_version.title')),
amis()->TableColumn('update_strategy', __('app_version.update_strategy'))->type('mapping')->map(AppUpdateStrategy::labelMap()),
amis()->TableColumn('is_force', __('app_version.is_force'))->type('static-mapping')->map([
'0' => '<span class="label label-success">否</span>',
'1' => '<span class="label label-danger">是</span>',
]),
amis()->TableColumn('os', __('app_version.os'))->type('mapping')->map(AppOs::labelMap()),
amis()->TableColumn('is_release', __('app_version.is_release'))->type('static-mapping')->map([
'0' => '<span class="label label-default">否</span>',
'1' => '<span class="label label-success">是</span>',
]),
amis()->TableColumn('release_at', __('app_version.release_at')),
amis()->TableColumn('created_at', __('admin.created_at'))->type('datetime'),
Operation::make()->label(__('admin.actions'))->buttons([
$this->rowEditTypeButton('drawer', 'lg')->visible(Admin::user()->can('admin.app_versions.update')),
$this->rowDeleteButton()->visible(Admin::user()->can('admin.app_versions.delete')),
$this->rowShowButton()->visible(Admin::user()->can('admin.app_versions.view')),
]),
]);
return $this->baseList($crud);
}
public function form($isEdit = false): Form
{
return $this->baseForm()->body([
amis()->TextControl('name', __('app_version.name'))
->placeholder(__('app_version.name'))
->required(),
amis()->NumberControl('version', __('app_version.version'))
->placeholder(__('app_version.version'))
->precision(0)
->required(),
amis()->TextControl('title', __('app_version.title'))
->placeholder(__('app_version.title'))
->required(),
amis()->TextareaControl('description', __('app_version.description'))
->placeholder(__('app_version.description'))
->minRows(10)
->required(),
amis()->RadiosControl('update_strategy', __('app_version.update_strategy'))
->options(AppUpdateStrategy::options())
->value(AppUpdateStrategy::Wgt->value)
->required(),
amis()->RadiosControl('is_force', __('app_version.is_force'))
->options([
['label' => '是', 'value' => true],
['label' => '否', 'value' => false],
])
->value(false)
->required(),
amis()->RadiosControl('os', __('app_version.os'))->options(AppOs::options())->value(AppOs::Android->value)->required(),
amis()->FileControl('android[wgt_url]', __('app_version.wgt_url'))
->accept('.wgt')
->useChunk(false)
->visibleOn('${AND(os == "'.AppOs::Android->value.'", update_strategy == "wgt")}'),
amis()->FileControl('android[apk_url]', __('app_version.apk_url'))
->accept('.apk')
->useChunk(false)
->visibleOn('${os == "'.AppOs::Android->value.'"}'),
amis()->FileControl('ios[wgt_url]', __('app_version.wgt_url'))
->accept('.wgt')
->useChunk(false)
->visibleOn('${AND(os == "'.AppOs::Ios->value.'", update_strategy == "wgt")}'),
amis()->TextControl('ios[apk_url]', '苹果应用地址')
->placeholder('苹果应用地址')
->visibleOn('${os == "'.AppOs::Ios->value.'"}'),
amis()->DateTimeControl('release_at', __('app_version.release_at'))
->valueFormat('YYYY-MM-DD HH:mm:ss')
->description('必须设置发布时间,才可更新'),
]);
}
public function detail(): Form
{
return $this->baseDetail()->body([
amis()->Property()->items([
['label' => __('app_version.name'), 'content' => '${name}'],
['label' => __('app_version.version'), 'content' => '${version}'],
['label' => __('app_version.os'), 'content' => amis()->Mapping()->name('os')->map(AppOs::labelMap())],
['label' => __('app_version.title'), 'content' => '${title}', 'span' => 3],
['label' => __('app_version.description'), 'content' => amis()->TextareaControl()->name('description')->static()->labelClassName('hidden'), 'span' => 3],
['label' => __('app_version.update_strategy'), 'content' => amis()->Mapping()->name('update_strategy')->map(AppUpdateStrategy::labelMap())],
[
'label' => __('app_version.is_force'),
'content' => amis()->Mapping()->name('${is_force}')->static()->map([
'0' => '<span class="label label-success">否</span>',
'1' => '<span class="label label-danger">是</span>',
]),
],
['label' => __('app_version.apk_url'), 'content' => amis()->Link()->href('${apk_url}')->body('下载')->blank()->visibleOn('${apk_url != null && apk_url != ""}')],
['label' => __('app_version.wgt_url'), 'content' => amis()->Link()->href('${wgt_url}')->body('下载')->blank()->visibleOn('${wgt_url != null && wgt_url != ""}')],
['label' => __('app_version.release_at'), 'content' => '${release_at}'],
['label' => __('app_version.created_at'), 'content' => '${created_at}'],
]),
]);
}
}

View File

@ -0,0 +1,9 @@
<?php
namespace App\Admin\Filters;
use EloquentFilter\ModelFilter;
class AppVersionFilter extends ModelFilter
{
}

View File

@ -0,0 +1,235 @@
<?php
namespace App\Admin\Services;
use App\Admin\Filters\AppVersionFilter;
use App\Enums\AppOs;
use App\Enums\AppUpdateStrategy;
use App\Models\AppVersion;
use Illuminate\Contracts\Filesystem\FileNotFoundException;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use Slowlyo\OwlAdmin\Admin;
class AppVersionService extends BaseService
{
protected string $modelName = AppVersion::class;
protected string $modelFilterName = AppVersionFilter::class;
public function sortColumn()
{
return 'version';
}
public function getEditData($id)
{
return tap(parent::getEditData($id), function ($instance) {
switch ($instance?->os) {
case AppOs::Android:
$instance->setAttribute('android', [
'apk_url' => $instance->apk_url,
'wgt_url' => $instance->wgt_url,
]);
break;
case AppOs::Ios:
$instance->setAttribute('ios', [
'apk_url' => $instance->apk_url,
'wgt_url' => $instance->wgt_url,
]);
break;
}
});
}
public function store($data): bool
{
Validator::validate(
data: $data,
rules: [
'name' => ['bail', 'required', 'max:255'],
'version' => ['bail', 'required', 'int'],
'title' => ['bail', 'required', 'max:255'],
'description' => ['bail', 'required'],
'update_strategy' => ['bail', 'required', Rule::enum(AppUpdateStrategy::class)],
'is_force' => ['bail', 'required', 'bool'],
'os' => ['bail', 'required', Rule::enum(AppOs::class)],
'release_at' => ['bail', 'nullable', 'date_format:Y-m-d H:i:s'],
],
attributes: [
'name' => __('app_version.name'),
'version' => __('app_version.version'),
'title' => __('app_version.title'),
'description' => __('app_version.description'),
'update_strategy' => __('app_version.update_strategy'),
'is_force' => __('app_version.is_force'),
'os' => __('app_version.os'),
'release_at' => __('app_version.release_at'),
],
);
// 全量包
$apkUrl = '';
// 增量包
$wgtUrl = '';
// 更新策略
$aus = AppUpdateStrategy::from($data['update_strategy']);
switch (AppOs::from($data['os'])) {
case AppOs::Android:
$apkUrl = (string) Arr::get($data, 'android.apk_url');
$wgtUrl = (string) Arr::get($data, 'android.wgt_url');
if ($aus === AppUpdateStrategy::Apk) {
admin_abort_if($apkUrl === '', '请上传全量包');
} else {
admin_abort_if($wgtUrl === '', '请上传增量包');
}
break;
case AppOs::Ios:
$apkUrl = (string) Arr::get($data, 'ios.apk_url');
$wgtUrl = (string) Arr::get($data, 'ios.wgt_url');
if ($aus === AppUpdateStrategy::Apk) {
admin_abort_if($apkUrl === '', '苹果应用地址 不能为空。');
} else {
admin_abort_if($wgtUrl === '', '请上传增量包');
}
break;
default:
admin_abort('未知操作系统');
break;
}
$model = $this->getModel();
$instance = new $model($data);
try {
$filename = $data['name'].'_'.time();
if ($extension = pathinfo($apkUrl, PATHINFO_EXTENSION)) {
$filename .= '.'.$extension;
}
$instance->apk_url = $this->saveUploadedFile($apkUrl, "app-versions/{$filename}");
} catch (FileNotFoundException $e) {
admin_abort('全量包未找到,请重新上传');
}
try {
$filename = $data['name'].'_'.time();
if ($extension = pathinfo($wgtUrl, PATHINFO_EXTENSION)) {
$filename .= '.'.$extension;
}
$instance->wgt_url = $this->saveUploadedFile($wgtUrl, "app-versions/{$filename}");
} catch (FileNotFoundException $e) {
admin_abort('增量包未找到,请重新上传');
}
return $instance->save();
}
public function update($primaryKey, $data): bool
{
Validator::validate(
data: $data,
rules: [
'name' => ['bail', 'required', 'max:255'],
'version' => ['bail', 'required', 'int'],
'title' => ['bail', 'required', 'max:255'],
'description' => ['bail', 'required'],
'update_strategy' => ['bail', 'required', Rule::enum(AppUpdateStrategy::class)],
'is_force' => ['bail', 'required', 'bool'],
'os' => ['bail', 'required', Rule::enum(AppOs::class)],
'release_at' => ['bail', 'nullable', 'date_format:Y-m-d H:i:s'],
],
attributes: [
'name' => __('app_version.name'),
'version' => __('app_version.version'),
'title' => __('app_version.title'),
'description' => __('app_version.description'),
'update_strategy' => __('app_version.update_strategy'),
'is_force' => __('app_version.is_force'),
'os' => __('app_version.os'),
'release_at' => __('app_version.release_at'),
],
);
// 全量包
$apkUrl = '';
// 增量包
$wgtUrl = '';
// 更新策略
$aus = AppUpdateStrategy::from($data['update_strategy']);
switch (AppOs::from($data['os'])) {
case AppOs::Android:
$apkUrl = (string) Arr::get($data, 'android.apk_url');
$wgtUrl = (string) Arr::get($data, 'android.wgt_url');
if ($aus === AppUpdateStrategy::Apk) {
admin_abort_if($apkUrl === '', '请上传全量包');
} else {
admin_abort_if($wgtUrl === '', '请上传增量包');
}
break;
case AppOs::Ios:
$apkUrl = (string) Arr::get($data, 'ios.apk_url');
$wgtUrl = (string) Arr::get($data, 'ios.wgt_url');
if ($aus === AppUpdateStrategy::Apk) {
admin_abort_if($apkUrl === '', '苹果应用地址 不能为空。');
} else {
admin_abort_if($wgtUrl === '', '请上传增量包');
}
break;
default:
admin_abort('未知操作系统');
break;
}
$instance = $this->query()->whereKey($primaryKey)->firstOrFail();
$instance->fill($data);
try {
$filename = $data['name'].'_'.time();
if ($extension = pathinfo($apkUrl, PATHINFO_EXTENSION)) {
$filename .= '.'.$extension;
}
$instance->apk_url = $this->saveUploadedFile($apkUrl, "app-versions/{$filename}");
} catch (FileNotFoundException $e) {
admin_abort('全量包未找到,请重新上传');
}
try {
$filename = $data['name'].'_'.time();
if ($extension = pathinfo($wgtUrl, PATHINFO_EXTENSION)) {
$filename .= '.'.$extension;
}
$instance->wgt_url = $this->saveUploadedFile($wgtUrl, "app-versions/{$filename}");
} catch (FileNotFoundException $e) {
admin_abort('增量包未找到,请重新上传');
}
return $instance->save();
}
protected function saveUploadedFile(string $from, string $to): string
{
if ($from === '' || preg_match('/^https?:\/\//', $from)) {
return $from;
}
$disk = Storage::disk(Admin::config('admin.upload.disk'));
if (! $disk->exists($from)) {
throw new FileNotFoundException();
}
$disk->move($from, $to);
return $disk->url($to);
}
}

View File

@ -1,6 +1,7 @@
<?php
use App\Admin\Controllers\AgreementController;
use App\Admin\Controllers\AppVersionController;
use App\Admin\Controllers\BaseKeywordController;
use App\Admin\Controllers\CockpitController;
use App\Admin\Controllers\Complaint\ComplaintController;
@ -219,6 +220,8 @@ Route::group([
$router->post('agreement/download', [AgreementController::class, 'download'])->name('agreement.download');
$router->resource('agreement', AgreementController::class);
$router->resource('app-versions', AppVersionController::class);
// 修改上传
$router->post('upload_file', [\App\Admin\Controllers\IndexController::class, 'uploadFile']);
$router->post('upload_image', [\App\Admin\Controllers\IndexController::class, 'uploadImage']);

View File

@ -0,0 +1,30 @@
<?php
namespace App\Enums;
enum AppOs: string
{
case Android = 'android';
case Ios = 'ios';
public function text(): string
{
return $this->options()[$this->value];
}
public static function options()
{
return [
self::Android->value => '安卓',
self::Ios->value => '苹果',
];
}
public static function labelMap(): array
{
return [
self::Android->value => '<span class="label bg-primary">'.self::Android->text().'</span>',
self::Ios->value => '<span class="label label-success">'.self::Ios->text().'</span>',
];
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace App\Enums;
enum AppUpdateStrategy: string
{
case Apk = 'apk';
case Wgt = 'wgt';
public function text(): string
{
return $this->options()[$this->value];
}
public static function options(): array
{
return [
self::Apk->value => '全量包',
self::Wgt->value => '热更新',
];
}
public static function labelMap(): array
{
return [
self::Apk->value => '<span class="label bg-danger">'.self::Apk->text().'</span>',
self::Wgt->value => '<span class="label label-primary">'.self::Wgt->text().'</span>',
];
}
}

View File

@ -0,0 +1,53 @@
<?php
namespace App\Models;
use App\Enums\AppOs;
use App\Enums\AppUpdateStrategy;
use App\Traits\HasDateTimeFormatter;
use EloquentFilter\Filterable;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class AppVersion extends Model
{
use Filterable, HasFactory, HasDateTimeFormatter;
protected $appends = [
'is_release',
];
protected $attributes = [
'is_force' => false,
];
protected $casts = [
'os' => AppOs::class,
'update_strategy' => AppUpdateStrategy::class,
'is_force' => 'bool',
'release_at' => 'datetime',
];
protected $fillable = [
'os',
'name',
'version',
'title',
'description',
'update_strategy',
'is_force',
'apk_url',
'wgt_url',
'release_at',
];
protected function isRelease(): Attribute
{
return Attribute::make(
get: function (mixed $value, $attributes) {
return (bool) $attributes['release_at'];
},
);
}
}

View File

@ -0,0 +1,37 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('app_versions', function (Blueprint $table) {
$table->id();
$table->string('os')->comment('操作系统: android, ios');
$table->string('name')->comment('版本名称');
$table->integer('version')->comment('版本号');
$table->string('title')->nullable()->comment('更新标题');
$table->text('description')->nullable()->comment('更新描述');
$table->string('apk_url')->nullable()->comment('全量包地址');
$table->string('wgt_url')->nullable()->comment('增量包地址');
$table->boolean('is_force')->default(false)->comment('是否强制更新');
$table->string('update_strategy')->comment('更新策略: apk 全量包, wgt 热更新');
$table->timestamp('release_at')->nullable()->comment('发布时间');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('app_versions');
}
};

View File

@ -15,8 +15,6 @@ class AdminPermissionSeeder extends Seeder
*/
public function run()
{
// AdminMenu::truncate();
// AdminPermission::truncate();
$data = [
/*
|--------------------------------------------------------------------------
@ -359,6 +357,13 @@ class AdminPermissionSeeder extends Seeder
'download' => '打包下载',
]
],
'app_versions' => [
'name' => 'App版本管理',
'icon' => 'flowbite:inbox-full-outline',
'uri' => '/app-versions',
'resource' => true,
'children' => []
],
/*
|--------------------------------------------------------------------------

View File

@ -0,0 +1,17 @@
<?php
return [
'id' => 'ID',
'name' => '版本名称',
'version' => '版本号',
'title' => '更新标题',
'description' => '更新描述',
'is_force' => '强制更新',
'update_strategy' => '更新策略',
'os' => '操作系统',
'apk_url' => '全量包',
'wgt_url' => '增量包',
'is_release' => '是否发布',
'release_at' => '发布时间',
'created_at' => '创建时间',
];