chore: 重写初始提交(清空历史,整理后全量提交)

This commit is contained in:
ywxapp
2026-08-16 16:54:14 +08:00
commit 6c1a106bc1
1808 changed files with 238144 additions and 0 deletions
+701
View File
@@ -0,0 +1,701 @@
<?php
// +----------------------------------------------------------------------
// | YwxApp AppMarket [ 应用市场中心站插件 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Author: ywxapp<admin@ywxapp.cn>
// +----------------------------------------------------------------------
declare(strict_types=1);
namespace addon\appmall\service;
use addon\appmall\model\AddonDownloadLog;
use addon\appmall\model\AddonLicense;
use addon\appmall\model\AddonOrder;
use think\facade\Cache;
use think\facade\Db;
use think\facade\Log;
use think\Request;
use ywxapp\model\BaseModel;
use Yansongda\Pay\Pay;
/**
* 市场中心站本地成交域服务(会员直购 / 授权 / 收益 / 下载)。
*
* 由核心 app/api/controller/v1/Addon.php 在「本机即中心站」模式下进程内委托调用;
* 所有中心站领域逻辑(订单、授权、收益账本、支付回调)收敛于插件,核心不再持有。
*
* 约定:业务失败抛 MarketException(携带 HTTP 语义 code),由调用方转成 Result 响应。
*/
class MarketService
{
protected static ?MarketService $instance = null;
public static function instance(): static
{
if (static::$instance === null) {
static::$instance = new static();
// 中心站成交域首次被委托时,确保全部 appmall 表存在(覆盖 v1/Addon 等直接调用方)
\addon\appmall\library\AppmallSchema::ensure();
}
return static::$instance;
}
/**
* 运行时自愈建表(仅当表不存在时创建),保证购买闭环在当前库即可跑通。
* 已收敛到 \addon\appmall\library\AppmallSchema::ensure()(建全部表 + 扩展列),此处委托。
*/
public function ensureTables(): void
{
\addon\appmall\library\AppmallSchema::ensure();
}
/**
* 商品查询(appmall_addon_list
*/
public function product(int $id): ?array
{
return Db::name('appmall_addon_list')->where('id', $id)->where('status', 1)->find();
}
/**
* 插件详情(含当前用户已购状态)
* @throws MarketException 404 插件不存在或已下架
*/
public function info(int $id, int $uid = 0): array
{
$addon = $this->product($id);
if (!$addon) {
throw new MarketException('插件不存在或已下架', 404);
}
$purchased = false;
if ($uid > 0) {
$purchased = AddonLicense::where('uid', $uid)->where('aid', $id)->count() > 0;
}
return [
'addon' => [
'id' => $addon['id'],
'name' => $addon['name'],
'title' => $addon['title'],
'version' => $addon['version'],
'price' => $addon['price'],
'logo' => $addon['logo'],
'intro' => $addon['description'] ?? '',
'author' => $addon['author'] ?? '',
],
'purchased' => $purchased,
];
}
/**
* 上架插件分页列表
* @return array{items: array, total: int}
*/
public function lists(int $page = 1, int $perPage = 15): array
{
$perPage = min(max($perPage, 1), 50);
$page = max($page, 1);
$query = Db::name('appmall_addon_list')->where('status', 1);
$total = (clone $query)->count();
$items = $query->field('id,name,title,version,price,logo,description as intro,author')
->order('id', 'desc')
->page($page, $perPage)
->select()
->toArray();
return ['items' => $items, 'total' => $total];
}
/**
* 发起购买:创建/复用待支付订单并拉起支付(或模拟支付直接签发授权)。
* @param string $baseUrl 客户端根地址(用于拼支付回调)
* @return array{data: array, message: string}
* @throws MarketException
*/
public function buy(int $uid, int $addonId, string $method, string $baseUrl): array
{
$addon = $this->product($addonId);
if (!$addon) {
throw new MarketException('插件不存在或已下架', 404);
}
// 免费插件无需购买(服务端 market valid 也直接放行)
if ((float) $addon['price'] <= 0) {
return ['data' => ['free' => true], 'message' => '该插件免费,可直接安装'];
}
// 幂等:已购直接返回授权
$existLicense = AddonLicense::where('uid', $uid)->where('aid', $addonId)->find();
if ($existLicense) {
return ['data' => ['license_key' => $existLicense->license_key], 'message' => '您已购买该插件'];
}
// 防重复下单:复用 15 分钟内待支付订单
$order = AddonOrder::where('uid', $uid)
->where('aid', $addonId)
->where('status', 0)
->where('create_at', '>', time() - 900)
->find();
if (!$order) {
$order = AddonOrder::create([
'uid' => $uid,
'aid' => $addonId,
'amount' => $addon['price'],
'status' => 0,
'trade_no' => date('YmdHis') . $uid . $addonId . random_int(100000, 999999),
]);
}
// 模拟支付:无真实商户号时直接完成并签发授权(仅本地/自托管调试用)
if (config('pay.mock_enable')
&& empty(config('pay.alipay.app_id'))
&& empty(config('pay.wechat.mch_id'))) {
$this->completeOrder($order, $addonId, $uid);
$license = AddonLicense::where('uid', $uid)->where('aid', $addonId)->find();
return [
'data' => ['license_key' => $license?->license_key, 'mock' => true],
'message' => '(模拟支付)购买成功',
];
}
try {
$base = rtrim($baseUrl, '/');
$payParams = [
'out_trade_no' => $order->trade_no,
'total_amount' => $order->amount,
'subject' => '购买插件:' . $addon['title'],
];
if ($method === 'wechat') {
$wconfig = config('pay.wechat');
if (empty($wconfig['mch_id']) || empty($wconfig['app_id'])) {
throw new \Exception('微信支付未配置,请联系管理员');
}
$wconfig['notify_url'] = $base . '/v1/addon/notify';
$result = Pay::wechat($wconfig)->h5($payParams);
return [
'data' => ['order_no' => $order->trade_no, 'pay_params' => $result->getBody()->getContents()],
'message' => '支付发起成功',
];
}
$aconfig = config('pay.alipay');
if (empty($aconfig['app_id']) || empty($aconfig['private_key'])) {
throw new \Exception('支付未配置,请联系管理员');
}
$aconfig['notify_url'] = $base . '/v1/addon/notify';
$aconfig['return_url'] = $base . '/v1/addon/payResult';
$result = Pay::alipay($aconfig)->web($payParams);
return [
'data' => ['order_no' => $order->trade_no, 'pay_params' => $result->getBody()->getContents()],
'message' => '支付发起成功',
];
} catch (\Throwable $e) {
throw new MarketException('支付发起失败: ' . $e->getMessage(), 500);
}
}
/**
* 订单支付成功后:置已付 + 幂等签发授权 + 写入开发者收益账本(分红)
*/
public function completeOrder($order, int $aid, int $uid): void
{
if ($order->status == 0) {
$order->status = 1;
$order->pay_time = time();
$order->save();
}
$exist = AddonLicense::where('uid', $uid)->where('aid', $aid)->find();
if (!$exist) {
try {
AddonLicense::create([
'uid' => $uid,
'aid' => $aid,
'license_key' => $this->generateLicenseKey($uid, $aid),
'expire_time' => 0,
'download_count' => 0,
]);
} catch (\PDOException $e) {
if (stripos($e->getMessage(), 'duplicate') === false
&& stripos($e->getMessage(), '1062') === false) {
throw $e;
}
}
}
// 写入收益账本(仅当该商品归属某开发者)
try {
$product = Db::name('appmall_addon_list')->where('id', $aid)->find();
$devId = (int) ($product['developer_id'] ?? 0);
if ($devId > 0 && !Db::name('appmall_addon_revenues')->where('order_id', $order->id)->find()) {
$rate = (float) config('appmall.commission_rate', 0.2);
$commission = round((float) $order->amount * $rate, 2);
$income = round((float) $order->amount - $commission, 2);
Db::name('appmall_addon_revenues')->insert([
'developer_id' => $devId,
'order_id' => $order->id,
'aid' => $aid,
'uid' => $uid,
'amount' => $order->amount,
'commission' => $commission,
'income' => $income,
'status' => 0,
'create_at' => time(),
]);
// 计入开发者可提现余额(提现申请时再转入冻结待打款)
Db::name('appmall_developers')->where('id', $devId)->inc('balance', $income)->update();
}
} catch (\Exception $e) {
// 收益记账失败不影响已完成的购买
Log::warning('收益记账失败:' . $e->getMessage());
}
// 钩子点:订单支付完成(已签发授权 + 写入分红账本)后触发,
// 插件可在 info.php['events']['listen']['order_paid'] 监听,用于发货 / 通知等联动。
event('order_paid', ['order' => $order, 'aid' => $aid, 'uid' => $uid]);
}
/**
* 支付结果查询(前端轮询用)
* @throws MarketException 404 订单不存在
*/
public function orderStatus(int $uid, string $tradeNo): array
{
$order = AddonOrder::where('trade_no', $tradeNo)->where('uid', $uid)->find();
if (!$order) {
throw new MarketException('订单不存在', 404);
}
$data = ['status' => $order->status];
if ($order->status === 1) {
$license = AddonLicense::where('uid', $uid)->where('aid', $order->aid)->find();
$data['license_key'] = $license?->license_key;
}
return $data;
}
/**
* 支付完成落地页状态(支付宝 return_url 跳转,无需登录,仅展示状态)
* @return array{data: array, message: string}
*/
public function payResult(string $tradeNo): array
{
if ($tradeNo === '') {
return ['data' => [], 'message' => '支付流程结束'];
}
$order = AddonOrder::where('trade_no', $tradeNo)->find();
if (!$order) {
return ['data' => ['status' => 0], 'message' => '订单不存在'];
}
return ['data' => ['status' => $order->status], 'message' => '获取成功'];
}
/**
* 退款 / 吊销授权(买家或运营)
* @return array{data: array, message: string}
* @throws MarketException
*/
public function refund(int $uid, string $tradeNo, int $orderId): array
{
$order = null;
if ($tradeNo !== '') {
$order = AddonOrder::where('trade_no', $tradeNo)->where('uid', $uid)->find();
} elseif ($orderId > 0) {
$order = AddonOrder::where('id', $orderId)->where('uid', $uid)->find();
}
if (!$order) {
throw new MarketException('订单不存在', 404);
}
if ($order->status === 2) {
return ['data' => ['status' => 2], 'message' => '该订单已退款'];
}
if ($order->status !== 1) {
throw new MarketException('仅已支付订单可申请退款', 400);
}
Db::transaction(function () use ($order, $uid) {
$order->status = 2;
$order->save();
// 吊销授权(status=0 且过期,download/valid 双重拦截)
AddonLicense::where('uid', $uid)->where('aid', $order->aid)
->update(['status' => 0, 'expire_time' => time()]);
// 收益冲正
$rev = Db::name('appmall_addon_revenues')->where('order_id', $order->id)->find();
if ($rev) {
if ((int) $rev['status'] === 0) {
Db::name('appmall_developers')->where('id', $rev['developer_id'])
->dec('balance', (float) $rev['income'])->update();
}
Db::name('appmall_addon_revenues')->where('id', $rev['id'])->update(['status' => 2]);
}
});
return ['data' => ['status' => 2], 'message' => '退款成功,授权已吊销'];
}
/**
* 我的已购插件(买家视角):列出已购付费插件 + 授权状态 + 是否可退款
*/
public function mine(int $uid): array
{
$rows = Db::name('appmall_addon_licenses')
->alias('l')
->join('appmall_addon_list a', 'a.id = l.aid')
->where('l.uid', $uid)
->where('a.price', '>', 0)
->field('a.name,a.title,a.version,a.price,l.status as license_status,l.expire_time,l.domain,l.aid')
->order('l.id', 'desc')
->select()
->toArray();
$list = [];
foreach ($rows as $r) {
$order = Db::name('appmall_addon_orders')->where('uid', $uid)->where('aid', $r['aid'])
->order('id', 'desc')->find();
$orderStatus = $order ? (int) $order['status'] : 0;
$refundable = ((int) $r['license_status'] === 1) && $orderStatus === 1;
$list[] = [
'name' => $r['name'],
'title' => $r['title'],
'version' => $r['version'],
'price' => $r['price'],
'license_status' => (int) $r['license_status'],
'expire_time' => (int) $r['expire_time'],
'domain' => $r['domain'] ?? '',
'order_id' => $order ? (int) $order['id'] : 0,
'trade_no' => $order['trade_no'] ?? '',
'refundable' => $refundable,
];
}
return ['list' => $list];
}
/**
* 支付宝/微信异步回调:验签 -> 校验金额 -> 订单置已付 -> 生成授权
* @return \think\Response
*/
public function notify(string $method)
{
try {
if ($method === 'wechat') {
$pay = Pay::wechat(config('pay.wechat'));
$data = $pay->callback();
$tradeNo = $data->get('out_trade_no');
$amount = (float) $data->get('amount') ?: (float) $data->get('total');
} else {
$pay = Pay::alipay(config('pay.alipay'));
$data = $pay->verify();
$tradeNo = $data->get('out_trade_no');
$amount = (float) $data->get('total_amount');
$tradeStatus = (string) $data->get('trade_status');
if (!in_array($tradeStatus, ['TRADE_SUCCESS', 'TRADE_FINISHED'], true)) {
return $pay->success();
}
}
} catch (\Throwable) {
return response('fail');
}
$order = AddonOrder::where('trade_no', $tradeNo)->find();
if ($order && $order->status == 0) {
// 防金额篡改:回调金额必须与订单一致
if (abs((float) $order->amount - $amount) < 0.01) {
Db::transaction(function () use ($order) {
$order->refresh();
if ($order->status != 0) {
return;
}
$this->completeOrder($order, $order->aid, $order->uid);
});
}
}
return $method === 'wechat' ? response('SUCCESS') : Pay::alipay(config('pay.alipay'))->success();
}
/**
* 用户下载已购插件(需有效授权 + 域名/签名/次数校验)。
* @return array{file: string, filename: string} 通过校验后返回文件路径与下载名
* @throws MarketException
*/
public function download(int $uid, int $addonId, Request $request): array
{
$license = AddonLicense::where('uid', $uid)->where('aid', $addonId)->find();
if (!$license) {
throw new MarketException('您尚未购买该插件', 403);
}
// 授权已吊销(退款):拒绝下载
if (isset($license->status) && (int) $license->status !== 1) {
throw new MarketException('授权已被吊销,无法下载', 403);
}
if ($license->expire_time > 0 && $license->expire_time < time()) {
throw new MarketException('授权已过期', 403);
}
// 域名绑定:license.domain 非空时,仅允许绑定的域名下载
if (!empty($license->domain)) {
$host = $request->host();
$hostNoPort = preg_replace('/:\d+$/', '', $host);
$allowed = array_filter(array_map('trim', preg_split('/[,\s]+/', (string) $license->domain)));
if (!in_array($host, $allowed, true) && !in_array($hostNoPort, $allowed, true)) {
throw new MarketException('该授权未绑定当前域名,无法下载', 403);
}
}
// 下载签名校验(开关 addon_download_sign
$signSecret = config('ywxapp.addon_secret');
if (config('ywxapp.addon_download_sign') && $signSecret) {
$ts = (int) $request->get('ts', 0);
$sign = (string) $request->get('sign', '');
if ($ts <= 0 || time() - $ts > 600) {
throw new MarketException('下载授权已过期,请重新获取', 403);
}
$expected = $license->generateSign($license->license_key, $addonId, $ts, $signSecret);
if (!hash_equals($expected, $sign)) {
throw new MarketException('下载授权校验失败', 403);
}
}
$addon = Db::name('appmall_addon_list')->where('id', $addonId)->where('status', 1)->find();
if (!$addon) {
throw new MarketException('插件不存在或已下架', 404);
}
// 下载次数限制:0 表示不限
if ($license->download_count > 0) {
$used = AddonDownloadLog::where('license_id', $license->id)->count();
if ($used >= $license->download_count) {
throw new MarketException('下载次数已用完', 403);
}
}
$file = realpath($addon['file_path']);
$root = realpath(root_path()) ?: root_path();
if ($file === false || !is_file($file) || strpos($file, $root) !== 0) {
throw new MarketException('插件文件暂不可用,请联系管理员', 404);
}
AddonDownloadLog::create([
'license_id' => $license->id,
'uid' => $uid,
'aid' => $addonId,
'ip' => $request->ip(),
]);
return ['file' => $file, 'filename' => $addon['name'] . '-' . $addon['version'] . '.zip'];
}
/**
* 后台市场目录(中心站本机浏览):关键词/分类筛选 + 排序 + 分类下拉数据源。
* 自愈元数据列(category/tags/screenshots/rating),与 api/Market::ensureAddonListColumns 对齐。
* @return array{list: array, categories: array}
*/
public function catalog(string $keyword = '', string $category = '', string $order = 'new', string $type = ''): array
{
// 自愈元数据列:旧库缺 category/tags/screenshots/rating 时自动补齐
$listTable = Db::name('appmall_addon_list')->getTable();
$colDefs = [
'category' => "varchar(50) DEFAULT '' COMMENT '分类'",
'tags' => "varchar(255) DEFAULT '' COMMENT '标签(逗号分隔)'",
'screenshots' => "text COMMENT '截图URL(逗号分隔或JSON'",
'rating' => "decimal(2,1) DEFAULT '0.0' COMMENT '评分(0.0~5.0'",
'changelog' => "text COMMENT '更新日志'",
'require_framework' => "varchar(20) DEFAULT '' COMMENT '最低兼容框架版本,如 1.2.0'",
'type' => "varchar(20) NOT NULL DEFAULT 'addon' COMMENT '商品类型:addon=插件 template=模板'",
];
foreach ($colDefs as $col => $def) {
BaseModel::ensureColumn($listTable, $col, $def);
}
// 探测元数据列是否存在,存在才选,保证兼容
$hasCols = [];
try {
$cols = Db::query("SHOW COLUMNS FROM `{$listTable}`");
$hasCols = array_column($cols, 'Field');
} catch (\Throwable $e) {
}
$has = fn (string $c): bool => in_array($c, $hasCols, true);
$q = Db::name('appmall_addon_list')->where('status', 1);
if ($keyword !== '') {
$q->where(function ($w) use ($keyword) {
$w->where('name', 'like', '%' . $keyword . '%')
->whereOr('title', 'like', '%' . $keyword . '%')
->whereOr('author', 'like', '%' . $keyword . '%')
->whereOr('description', 'like', '%' . $keyword . '%');
});
}
if ($category !== '' && $has('category')) {
$q->where('category', $category);
}
// 商品类型筛选(addon=插件 / template=模板;空=全部)
if ($type !== '' && $has('type')) {
$q->where('type', $type);
}
$field = 'id,name,title,description as intro,author,version,logo,price,download_count,update_at';
foreach (['category', 'tags', 'screenshots', 'rating', 'changelog', 'require_framework', 'type'] as $mf) {
if ($has($mf)) {
$field .= ',' . $mf;
}
}
// 排序:对齐 api/Market::lists 的 orderMapdownload_count 为商品表内置列)
$orderMap = [
'new' => ['id', 'desc'],
'hot' => ['download_count', 'desc'],
'price_asc' => ['price', 'asc'],
'price_desc' => ['price', 'desc'],
];
[$of, $od] = $orderMap[$order] ?? $orderMap['new'];
$list = $q->field($field)->order($of, $od)->select()->toArray();
// 同名插件多版本聚合:只展示最新版,其余版本收进 versions(详情弹窗版本下拉用)
$list = self::aggregateVersions($list);
// 分类下拉数据源:本机全部非空分类
$categories = [];
if ($has('category')) {
$categories = Db::name('appmall_addon_list')
->where('status', 1)->where('category', '<>', '')
->distinct(true)->order('category', 'asc')->column('category');
}
return ['list' => $list, 'categories' => $categories];
}
/**
* 运营退款(后台特权):按插件名退最新「已支付」订单,
* 吊销授权 + 订单置已退款 + 收益冲正。
* @throws MarketException
*/
public function operatorRefund(string $name): void
{
$addon = Db::name('appmall_addon_list')->where('name', $name)->find();
if (empty($addon)) {
throw new MarketException('插件不存在', 404);
}
$order = Db::name('appmall_addon_orders')->where('aid', $addon['id'])
->where('status', 1)->order('id', 'desc')->find();
if (empty($order)) {
throw new MarketException('未找到该插件的已支付订单', 404);
}
Db::transaction(function () use ($order, $addon) {
Db::name('appmall_addon_orders')->where('id', $order['id'])
->update(['status' => 2, 'update_at' => time()]);
Db::name('appmall_addon_licenses')->where('uid', $order['uid'])
->where('aid', $addon['id'])
->update(['status' => 0, 'expire_time' => time(), 'update_at' => time()]);
$rev = Db::name('appmall_addon_revenues')->where('order_id', $order['id'])->find();
if ($rev) {
if ((int) $rev['status'] === 0) {
Db::name('appmall_developers')->where('id', $rev['developer_id'])
->dec('balance', (float) $rev['income'])->update();
}
Db::name('appmall_addon_revenues')->where('id', $rev['id'])->update(['status' => 2]);
}
});
}
/**
* 同名插件多版本聚合:每个插件只保留最新版一条作代表行,
* 其余版本收进代表行的 versions 数组(含最新版自身,按版本号降序),
* 供市场详情页/弹窗做「版本下拉选择」,列表不再平铺展示多个版本。
* 输出顺序 = 各插件在原列表中的首次出现顺序(尽量保持原排序语义)。
*/
public static function aggregateVersions(array $rows): array
{
$groups = [];
$names = [];
foreach ($rows as $r) {
$n = (string) ($r['name'] ?? '');
if (!isset($groups[$n])) {
$groups[$n] = [];
$names[] = $n;
}
$groups[$n][] = $r;
}
$out = [];
foreach ($names as $n) {
$vers = $groups[$n];
usort($vers, fn ($a, $b) => version_compare((string) ($b['version'] ?? '0'), (string) ($a['version'] ?? '0')));
$main = $vers[0];
$main['versions'] = array_map(fn ($v) => [
'id' => $v['id'] ?? 0,
'version' => (string) ($v['version'] ?? ''),
'price' => $v['price'] ?? 0,
'update_at' => (int) ($v['update_at'] ?? 0),
], $vers);
$out[] = $main;
}
return $out;
}
/**
* 修改已上架插件价格(按插件名维度,同步全部版本行 + 提交记录)。
* 后台改价传 developerId=0(不限归属);开发者中心改价须传本人 developerId(仅能改自己的应用)。
* 改价后 bump 目录版本号,市场列表缓存立即失效。
* @return int 受影响的版本行数
* @throws MarketException
*/
public function updatePrice(string $name, float $price, int $developerId = 0): int
{
if ($name === '') {
throw new MarketException('缺少插件标识', 400);
}
if ($price < 0 || $price > 999999) {
throw new MarketException('价格必须在 0 ~ 999999 之间', 400);
}
$q = Db::name('appmall_addon_list')->where('name', $name);
if ($developerId > 0) {
$q->where('developer_id', $developerId);
}
$rows = $q->select()->toArray();
if (empty($rows)) {
// 尚未上架(仅有提交记录)也允许改价:只同步 submissions
$subExist = Db::name('appmall_addon_submissions')->where('name', $name)
->when($developerId > 0, fn ($w) => $w->where('developer_id', $developerId))
->count();
if ($subExist <= 0) {
throw new MarketException($developerId > 0 ? '未找到你名下的该插件' : '插件不存在', 404);
}
}
$ids = array_column($rows, 'id');
if ($ids) {
Db::name('appmall_addon_list')->whereIn('id', $ids)
->update(['price' => $price, 'update_at' => time()]);
}
// 同步提交记录价格(保持审核台/开发者中心口径一致;仅同步同归属记录)
try {
$sq = Db::name('appmall_addon_submissions')->where('name', $name);
if ($developerId > 0) {
$sq->where('developer_id', $developerId);
}
$sq->update(['price' => $price, 'update_at' => time()]);
} catch (\Throwable $e) {
// 提交记录同步失败不影响商品改价
}
self::bumpCatalogVersion();
return count($ids);
}
/**
* 目录版本号 bump:使 api/Market::lists 的 60s 缓存立即失效(审核发布/改价后调用)。
*/
public static function bumpCatalogVersion(): void
{
try {
Cache::set('appmall:catalog:ver', (string) microtime(true));
} catch (\Throwable $e) {
// 缓存不可用时静默(列表最多延迟 60s 更新)
}
}
/**
* 生成唯一授权码
*/
protected function generateLicenseKey(int $uid, int $aid): string
{
do {
$key = strtoupper(sprintf(
'%s-%s-%s',
substr(md5($uid . $aid . random_bytes(8)), 0, 8),
bin2hex(random_bytes(4)),
bin2hex(random_bytes(4))
));
} while (AddonLicense::where('license_key', $key)->find());
return $key;
}
}