diff --git a/plugins/Bestseller/Admin/View/DesignBuilders/Bestseller.php b/plugins/Bestseller/Admin/View/DesignBuilders/Bestseller.php
new file mode 100644
index 00000000..fd236855
--- /dev/null
+++ b/plugins/Bestseller/Admin/View/DesignBuilders/Bestseller.php
@@ -0,0 +1,45 @@
+
+ * @created 2022-07-08 17:09:15
+ * @modified 2022-07-08 17:09:15
+ */
+
+namespace Plugin\Bestseller\Admin\View\DesignBuilders;
+
+use Illuminate\Contracts\View\View;
+use Illuminate\View\Component;
+
+class Bestseller extends Component
+{
+ /**
+ * Create a new component instance.
+ *
+ * @return void
+ */
+ public function __construct()
+ {
+ }
+
+ /**
+ * Get the view / contents that represent the component.
+ *
+ * @return View
+ */
+ public function render(): View
+ {
+ $data['register'] = [
+ 'code' => 'bestseller',
+ 'sort' => 0,
+ 'name' => trans('Bestseller::common.module_name'),
+ 'icon' => plugin_asset('Bestseller', 'image/icon.png'),
+ 'view_path' => 'Bestseller::shop/design_module_bestseller',
+ ];
+
+ return view('Bestseller::admin/design_module_bestseller', $data);
+ }
+}
diff --git a/plugins/Bestseller/Bootstrap.php b/plugins/Bestseller/Bootstrap.php
new file mode 100644
index 00000000..4dee1b78
--- /dev/null
+++ b/plugins/Bestseller/Bootstrap.php
@@ -0,0 +1,43 @@
+
+ * @created 2022-07-20 15:35:59
+ * @modified 2022-07-20 15:35:59
+ */
+
+namespace Plugin\Bestseller;
+
+use Plugin\Bestseller\Repositories\ProductRepo;
+
+class Bootstrap
+{
+ public function boot()
+ {
+ /**
+ * Add module for admin design.
+ */
+ add_hook_filter('admin.design.index.data', function ($data) {
+ $data['editors'][] = 'editor-bestseller';
+
+ return $data;
+ });
+
+ /**
+ * Get module content for home page and preview.
+ */
+ add_hook_filter('service.design.module.content', function ($data) {
+ $module = $data['module_code'] ?? '';
+
+ if ($module == 'bestseller') {
+ $data['title'] = $data['title'][locale()] ?? '';
+ $data['products'] = ProductRepo::getBestSellerProducts($data['limit']);
+ }
+
+ return $data;
+ });
+ }
+}
diff --git a/plugins/Bestseller/Lang/en/common.php b/plugins/Bestseller/Lang/en/common.php
new file mode 100644
index 00000000..bcc7a8e9
--- /dev/null
+++ b/plugins/Bestseller/Lang/en/common.php
@@ -0,0 +1,15 @@
+
+ * @created 2022-07-28 16:19:06
+ * @modified 2022-07-28 16:19:06
+ */
+
+return [
+ 'module_name' => 'Bestseller',
+ 'limit' => 'Limit',
+];
diff --git a/plugins/Bestseller/Lang/zh_cn/common.php b/plugins/Bestseller/Lang/zh_cn/common.php
new file mode 100644
index 00000000..e962884f
--- /dev/null
+++ b/plugins/Bestseller/Lang/zh_cn/common.php
@@ -0,0 +1,15 @@
+
+ * @created 2022-07-28 16:19:06
+ * @modified 2022-07-28 16:19:06
+ */
+
+return [
+ 'module_name' => '热卖模块',
+ 'limit' => '数量限制',
+];
diff --git a/plugins/Bestseller/Repositories/ProductRepo.php b/plugins/Bestseller/Repositories/ProductRepo.php
new file mode 100644
index 00000000..525ca388
--- /dev/null
+++ b/plugins/Bestseller/Repositories/ProductRepo.php
@@ -0,0 +1,36 @@
+
+ * @created 2023-03-08 11:56:17
+ * @modified 2023-03-08 11:56:17
+ */
+
+namespace Plugin\Bestseller\Repositories;
+
+use Beike\Shop\Http\Resources\ProductSimple;
+
+class ProductRepo
+{
+ /**
+ * Get best seller
+ *
+ * @param $limit
+ * @return array
+ */
+ public static function getBestSellerProducts($limit): array
+ {
+ $products = \Beike\Repositories\ProductRepo::getBuilder([
+ 'active' => 1,
+ 'sort' => 'products.sales',
+ 'order' => 'desc',
+ ])
+ ->whereHas('masterSku')
+ ->limit($limit)->get();
+
+ return ProductSimple::collection($products)->jsonSerialize();
+ }
+}
diff --git a/plugins/Bestseller/Static/image/icon.png b/plugins/Bestseller/Static/image/icon.png
new file mode 100644
index 00000000..00aafdab
Binary files /dev/null and b/plugins/Bestseller/Static/image/icon.png differ
diff --git a/plugins/Bestseller/Static/image/logo.png b/plugins/Bestseller/Static/image/logo.png
new file mode 100644
index 00000000..cd2f2986
Binary files /dev/null and b/plugins/Bestseller/Static/image/logo.png differ
diff --git a/plugins/Bestseller/Views/admin/design_module_bestseller.blade.php b/plugins/Bestseller/Views/admin/design_module_bestseller.blade.php
new file mode 100644
index 00000000..f0bfb050
--- /dev/null
+++ b/plugins/Bestseller/Views/admin/design_module_bestseller.blade.php
@@ -0,0 +1,54 @@
+
+
+
{{ __('admin/builder.text_set_up') }}
+
+
{{ __('admin/builder.text_module_title') }}
+
+
+
+
{{ __('Bestseller::common.limit') }}
+
+
+
+
+
+
+
+{{-- 定义模块的配置项 --}}
+@push('footer-script')
+
+@endpush
+
diff --git a/plugins/Bestseller/Views/shop/design_module_bestseller.blade.php b/plugins/Bestseller/Views/shop/design_module_bestseller.blade.php
new file mode 100644
index 00000000..9bba7f74
--- /dev/null
+++ b/plugins/Bestseller/Views/shop/design_module_bestseller.blade.php
@@ -0,0 +1,67 @@
+
+ @include('design._partial._module_tool')
+
+
+
{{ $content['title'] }}
+ @if ($content['products'])
+
+
+ @foreach ($content['products'] as $product)
+
+ @include('shared.product')
+
+ @endforeach
+
+
+
+
+
+ @elseif (!$content['products'] and $design)
+
+ @for ($s = 0; $s < 4; $s++)
+
+
+
+
请配置商品
+
+ 66.66
+ 99.99
+
+
+
+ @endfor
+
+ @endif
+
+
+
+
+
diff --git a/plugins/Bestseller/config.json b/plugins/Bestseller/config.json
new file mode 100644
index 00000000..f4ae6961
--- /dev/null
+++ b/plugins/Bestseller/config.json
@@ -0,0 +1,18 @@
+{
+ "code": "bestseller",
+ "name": {
+ "zh_cn": "热卖商品模块",
+ "en": "Hot Items Module"
+ },
+ "description": {
+ "zh_cn": "首页装修热卖商品模块",
+ "en": "Home Decoration Hot Products Module"
+ },
+ "type": "feature",
+ "version": "v1.0.0",
+ "icon": "/image/logo.png",
+ "author": {
+ "name": "成都光大网络科技有限公司",
+ "email": "yangjin@guangda.work"
+ }
+}
diff --git a/plugins/FlatShipping/Bootstrap.php b/plugins/FlatShipping/Bootstrap.php
new file mode 100644
index 00000000..21c5371b
--- /dev/null
+++ b/plugins/FlatShipping/Bootstrap.php
@@ -0,0 +1,65 @@
+
+ * @created 2022-07-20 15:35:59
+ * @modified 2022-07-20 15:35:59
+ */
+
+namespace Plugin\FlatShipping;
+
+use Beike\Admin\Http\Resources\PluginResource;
+use Beike\Plugin\Plugin;
+use Beike\Shop\Services\CheckoutService;
+
+class Bootstrap
+{
+ /**
+ * 获取固定运费方式
+ *
+ * @param CheckoutService $checkout
+ * @param Plugin $plugin
+ * @return array
+ * @throws \Exception
+ */
+ public function getQuotes(CheckoutService $checkout, Plugin $plugin): array
+ {
+ $code = $plugin->code;
+ $pluginResource = (new PluginResource($plugin))->jsonSerialize();
+ $quotes[] = [
+ 'type' => 'shipping',
+ 'code' => "{$code}.0",
+ 'name' => $pluginResource['name'],
+ 'description' => $pluginResource['description'],
+ 'icon' => $pluginResource['icon'],
+ 'cost' => $this->getShippingFee($checkout),
+ ];
+
+ return $quotes;
+ }
+
+ /**
+ * 计算固定运费
+ *
+ * @param CheckoutService $checkout
+ * @return float|int
+ */
+ public function getShippingFee(CheckoutService $checkout): float|int
+ {
+ $totalService = $checkout->totalService;
+ $amount = $totalService->amount;
+ $shippingType = plugin_setting('flat_shipping.type', 'fixed');
+ $shippingValue = plugin_setting('flat_shipping.value', 0);
+ if ($shippingType == 'fixed') {
+ return $shippingValue;
+ } elseif ($shippingType == 'percent') {
+ return $amount * $shippingValue / 100;
+ }
+
+ return 0;
+
+ }
+}
diff --git a/plugins/FlatShipping/Lang/en/common.php b/plugins/FlatShipping/Lang/en/common.php
new file mode 100644
index 00000000..7461686f
--- /dev/null
+++ b/plugins/FlatShipping/Lang/en/common.php
@@ -0,0 +1,17 @@
+
+ * @created 2022-07-28 16:19:06
+ * @modified 2022-07-28 16:19:06
+ */
+
+return [
+ 'way' => 'Way',
+ 'flat_shipping' => 'Flat Shipping',
+ 'percentage' => 'Percentage',
+ 'shipping_value' => 'Shipping Value',
+];
diff --git a/plugins/FlatShipping/Lang/zh_cn/common.php b/plugins/FlatShipping/Lang/zh_cn/common.php
new file mode 100644
index 00000000..a7d395a0
--- /dev/null
+++ b/plugins/FlatShipping/Lang/zh_cn/common.php
@@ -0,0 +1,17 @@
+
+ * @created 2022-07-28 16:19:06
+ * @modified 2022-07-28 16:19:06
+ */
+
+return [
+ 'way' => '方式',
+ 'flat_shipping' => '固定运费',
+ 'percentage' => '百分比',
+ 'shipping_value' => '运费值',
+];
diff --git a/public/plugin/alipay/css/demo.css b/plugins/FlatShipping/Static/css/demo.css
similarity index 100%
rename from public/plugin/alipay/css/demo.css
rename to plugins/FlatShipping/Static/css/demo.css
diff --git a/public/plugin/flat_shipping/image/logo.png b/plugins/FlatShipping/Static/image/logo.png
similarity index 100%
rename from public/plugin/flat_shipping/image/logo.png
rename to plugins/FlatShipping/Static/image/logo.png
diff --git a/public/plugin/alipay/js/demo.js b/plugins/FlatShipping/Static/js/demo.js
similarity index 100%
rename from public/plugin/alipay/js/demo.js
rename to plugins/FlatShipping/Static/js/demo.js
diff --git a/plugins/FlatShipping/columns.php b/plugins/FlatShipping/columns.php
new file mode 100644
index 00000000..ed2e24f7
--- /dev/null
+++ b/plugins/FlatShipping/columns.php
@@ -0,0 +1,29 @@
+
+ * @created 2022-06-29 21:16:23
+ * @modified 2022-06-29 21:16:23
+ */
+
+return [
+ [
+ 'name' => 'type',
+ 'label_key' => 'common.flat_shipping',
+ 'type' => 'select',
+ 'options' => [
+ ['value' => 'fixed', 'label_key' => 'common.flat_shipping'],
+ ['value' => 'percent', 'label_key' => 'common.percentage'],
+ ],
+ 'required' => true,
+ ],
+ [
+ 'name' => 'value',
+ 'label_key' => 'common.shipping_value',
+ 'type' => 'string',
+ 'required' => true,
+ ],
+];
diff --git a/plugins/FlatShipping/config.json b/plugins/FlatShipping/config.json
new file mode 100644
index 00000000..3cd9300c
--- /dev/null
+++ b/plugins/FlatShipping/config.json
@@ -0,0 +1,18 @@
+{
+ "code": "flat_shipping",
+ "name": {
+ "zh_cn": "固定运费",
+ "en": "Flat Rate Shipping"
+ },
+ "description": {
+ "zh_cn": "按订单总额收取固定运费",
+ "en": "Fixed shipping fee by order total"
+ },
+ "type": "shipping",
+ "version": "v1.0.0",
+ "icon": "/image/logo.png",
+ "author": {
+ "name": "成都光大网络科技有限公司",
+ "email": "yangjin@guangda.work"
+ }
+}
diff --git a/plugins/LatestProducts/Bootstrap.php b/plugins/LatestProducts/Bootstrap.php
new file mode 100644
index 00000000..bbfa640d
--- /dev/null
+++ b/plugins/LatestProducts/Bootstrap.php
@@ -0,0 +1,149 @@
+
+ * @created 2022-07-20 15:35:59
+ * @modified 2022-07-20 15:35:59
+ */
+
+namespace Plugin\LatestProducts;
+
+class Bootstrap
+{
+ /**
+ * 去除注释后可以观察网站头部以及产品详情页页面变化
+ */
+ public function boot()
+ {
+ $this->addLatestProducts();
+
+ // $this->modifyHeader();
+ // $this->modifyProductDetail();
+
+ // $this->modifyAdminProductEdit();
+ // $this->modifySetting();
+ // $this->handlePaidOrder();
+ }
+
+ /**
+ * 在前台网页头部添加二级菜单链接
+ */
+ private function addLatestProducts()
+ {
+ add_hook_filter('menu.content', function ($data) {
+ $data[] = [
+ 'name' => trans('LatestProducts::header.latest_products'),
+ 'link' => shop_route('latest_products'),
+ ];
+
+ return $data;
+ }, 0);
+ }
+
+ /**
+ * 修改前台全局 header 模板演示
+ */
+ private function modifyHeader()
+ {
+ add_hook_blade('header.top.currency', function ($callback, $output, $data) {
+ return '货币前' . $output;
+ });
+
+ add_hook_blade('header.top.language', function ($callback, $output, $data) {
+ return $output . '语言后';
+ });
+
+ add_hook_blade('header.top.telephone', function ($callback, $output, $data) {
+ return '电话前' . $output;
+ });
+
+ add_hook_blade('header.menu.logo', function ($callback, $output, $data) {
+ return $output . 'Logo后';
+ });
+
+ add_hook_blade('header.menu.icon', function ($callback, $output, $data) {
+ $view = view('LatestProducts::shop.header_icon')->render();
+
+ return $output . $view;
+ });
+ }
+
+ /**
+ * 修改产品详情页演示
+ * 1. 通过数据 hook 修改产品详情页产品名称
+ * 2. 通过模板 hook 在产品详情页名称上面添加 Hot 标签
+ * 3. 通过模板 hook 在产品详情页品牌下面添加信息
+ * 4. 通过模板 hook 在产品详情页立即购买后添加按钮
+ */
+ private function modifyProductDetail()
+ {
+ // 通过数据 hook 修改产品详情页产品名称
+ add_hook_filter('product.show.data', function ($product) {
+ $product['product']['name'] = '[疯狂热销]' . $product['product']['name'];
+
+ return $product;
+ });
+
+ // 通过模板 hook 在产品详情页名称上面添加 Hot 标签
+ add_hook_blade('product.detail.name', function ($callback, $output, $data) {
+ $badge = 'Hot';
+
+ return $badge . $output;
+ });
+
+ // 通过模板 hook 在产品详情页品牌下面添加信息
+ add_hook_blade('product.detail.brand', function ($callback, $output, $data) {
+ return $output . '
Brand 2:品牌 2
';
+ });
+
+ // 通过模板 hook 在产品详情页立即购买后添加按钮
+ add_hook_blade('product.detail.buy.after', function ($callback, $output, $data) {
+ $view = '';
+
+ return $output . $view;
+ });
+ }
+
+ /**
+ * 后台产品编辑页添加自定义字段演示
+ */
+ private function modifyAdminProductEdit()
+ {
+ add_hook_blade('admin.product.edit.extra', function ($callback, $output, $data) {
+ $view = view('LatestProducts::admin.product.edit_extra_field', $data)->render();
+
+ return $output . $view;
+ }, 1);
+ }
+
+ /**
+ * 系统设置添加新 tab
+ */
+ private function modifySetting()
+ {
+ add_hook_blade('admin.setting.nav.after', function ($callback, $output, $data) {
+ return view('LatestProducts::admin.setting.nav')->render();
+ });
+
+ add_hook_blade('admin.setting.after', function ($callback, $output, $data) {
+ return view('LatestProducts::admin.setting.tab')->render();
+ });
+ }
+
+ /**
+ * 修改订单状态机流程演示
+ */
+ private function handlePaidOrder()
+ {
+ add_hook_filter('service.state_machine.machines', function ($data) {
+ $data['machines']['unpaid']['paid'][] = function () {
+ // 这里写订单由 unpaid 变为 paid 执行的逻辑
+ };
+
+ return $data;
+ }, 0);
+ }
+}
diff --git a/plugins/LatestProducts/Controllers/MenusController.php b/plugins/LatestProducts/Controllers/MenusController.php
new file mode 100644
index 00000000..44fd5e19
--- /dev/null
+++ b/plugins/LatestProducts/Controllers/MenusController.php
@@ -0,0 +1,48 @@
+
+ * @created 2022-07-21 10:00:25
+ * @modified 2022-07-21 10:00:25
+ */
+
+namespace Plugin\LatestProducts\Controllers;
+
+use Beike\Repositories\ProductRepo;
+use Beike\Shop\Http\Controllers\Controller;
+use Beike\Shop\Http\Resources\ProductSimple;
+
+class MenusController extends Controller
+{
+ public function getRoutes(): array
+ {
+ return [
+ 'method' => __METHOD__,
+ 'route_list' => [],
+ ];
+ }
+
+ public function latestProducts()
+ {
+ $products = ProductRepo::getBuilder(
+ [
+ // 'active' => 1,
+ 'sort' => 'created_at',
+ 'order' => 'desc',
+ ])
+ ->whereHas('masterSku')
+ ->with('inCurrentWishlist')
+ ->orderByDesc('created_at')
+ ->paginate(perPage());
+
+ $data = [
+ 'products' => $products,
+ 'items' => ProductSimple::collection($products)->jsonSerialize(),
+ ];
+
+ return view('LatestProducts::shop.latest_products', $data);
+ }
+}
diff --git a/plugins/LatestProducts/Lang/en/header.php b/plugins/LatestProducts/Lang/en/header.php
new file mode 100644
index 00000000..6d49946c
--- /dev/null
+++ b/plugins/LatestProducts/Lang/en/header.php
@@ -0,0 +1,14 @@
+
+ * @created 2022-07-28 16:19:06
+ * @modified 2022-07-28 16:19:06
+ */
+
+return [
+ 'latest_products' => 'Latest Products',
+];
diff --git a/plugins/LatestProducts/Lang/zh_cn/header.php b/plugins/LatestProducts/Lang/zh_cn/header.php
new file mode 100644
index 00000000..5b82feb6
--- /dev/null
+++ b/plugins/LatestProducts/Lang/zh_cn/header.php
@@ -0,0 +1,14 @@
+
+ * @created 2022-07-28 16:19:06
+ * @modified 2022-07-28 16:19:06
+ */
+
+return [
+ 'latest_products' => '最新商品',
+];
diff --git a/plugins/LatestProducts/Routes/admin.php b/plugins/LatestProducts/Routes/admin.php
new file mode 100644
index 00000000..07f85855
--- /dev/null
+++ b/plugins/LatestProducts/Routes/admin.php
@@ -0,0 +1,15 @@
+
+ * @created 2022-08-04 16:17:53
+ * @modified 2022-08-04 16:17:53
+ */
+
+use Illuminate\Support\Facades\Route;
+use Plugin\LatestProducts\Controllers\MenusController;
+
+Route::get('/routes', [MenusController::class, 'getRoutes'])->name('routes');
diff --git a/plugins/LatestProducts/Routes/shop.php b/plugins/LatestProducts/Routes/shop.php
new file mode 100644
index 00000000..9bf997cb
--- /dev/null
+++ b/plugins/LatestProducts/Routes/shop.php
@@ -0,0 +1,15 @@
+
+ * @created 2022-08-04 16:17:44
+ * @modified 2022-08-04 16:17:44
+ */
+
+use Illuminate\Support\Facades\Route;
+use Plugin\LatestProducts\Controllers\MenusController;
+
+Route::get('/latest_products', [MenusController::class, 'latestProducts'])->name('latest_products');
diff --git a/public/plugin/flat_shipping/css/demo.css b/plugins/LatestProducts/Static/css/demo.css
similarity index 100%
rename from public/plugin/flat_shipping/css/demo.css
rename to plugins/LatestProducts/Static/css/demo.css
diff --git a/plugins/LatestProducts/Static/image/logo.png b/plugins/LatestProducts/Static/image/logo.png
new file mode 100644
index 00000000..37ff0126
Binary files /dev/null and b/plugins/LatestProducts/Static/image/logo.png differ
diff --git a/public/plugin/flat_shipping/js/demo.js b/plugins/LatestProducts/Static/js/demo.js
similarity index 100%
rename from public/plugin/flat_shipping/js/demo.js
rename to plugins/LatestProducts/Static/js/demo.js
diff --git a/plugins/LatestProducts/Views/admin/product/edit_extra_field.blade.php b/plugins/LatestProducts/Views/admin/product/edit_extra_field.blade.php
new file mode 100644
index 00000000..f0bad4da
--- /dev/null
+++ b/plugins/LatestProducts/Views/admin/product/edit_extra_field.blade.php
@@ -0,0 +1 @@
+
diff --git a/plugins/LatestProducts/Views/shop/header_icon.blade.php b/plugins/LatestProducts/Views/shop/header_icon.blade.php
new file mode 100644
index 00000000..fa2210f4
--- /dev/null
+++ b/plugins/LatestProducts/Views/shop/header_icon.blade.php
@@ -0,0 +1,3 @@
+
+ 图标后
+
diff --git a/plugins/LatestProducts/Views/shop/latest_products.blade.php b/plugins/LatestProducts/Views/shop/latest_products.blade.php
new file mode 100644
index 00000000..7cec3144
--- /dev/null
+++ b/plugins/LatestProducts/Views/shop/latest_products.blade.php
@@ -0,0 +1,21 @@
+@extends('layout.master')
+@section('content')
+
+
+
+ @foreach ($items as $product)
+
@include('shared.product')
+ @endforeach
+
+ {{ $products->links('shared/pagination/bootstrap-4') }}
+
+@endsection
diff --git a/plugins/LatestProducts/Views/shop/product_button.blade.php b/plugins/LatestProducts/Views/shop/product_button.blade.php
new file mode 100644
index 00000000..f95f97e2
--- /dev/null
+++ b/plugins/LatestProducts/Views/shop/product_button.blade.php
@@ -0,0 +1,3 @@
+
diff --git a/plugins/LatestProducts/config.json b/plugins/LatestProducts/config.json
new file mode 100644
index 00000000..1561cf5b
--- /dev/null
+++ b/plugins/LatestProducts/config.json
@@ -0,0 +1,12 @@
+{
+ "code": "latest_products",
+ "name": "最新商品列表",
+ "description": "首页菜单添加最新商品列表功能",
+ "type": "feature",
+ "version": "v1.0.0",
+ "icon": "/image/logo.png",
+ "author": {
+ "name": "成都光大网络科技有限公司",
+ "email": "yangjin@guangda.work"
+ }
+}
diff --git a/plugins/Openai/Bootstrap.php b/plugins/Openai/Bootstrap.php
new file mode 100644
index 00000000..1e503d20
--- /dev/null
+++ b/plugins/Openai/Bootstrap.php
@@ -0,0 +1,48 @@
+
+ * @created 2023-02-27 13:57:38
+ * @modified 2023-02-27 13:57:38
+ */
+
+namespace Plugin\Openai;
+
+class Bootstrap
+{
+ public function boot()
+ {
+ add_hook_filter('admin.sidebar.setting.prefix', function ($data) {
+ $data[] = 'openai';
+
+ return $data;
+ });
+
+ add_hook_filter('admin.sidebar.setting_routes', function ($data) {
+ $data[] = [
+ 'route' => 'openai.index',
+ 'prefixes' => ['openai'],
+ 'title' => 'ChatGPT',
+ ];
+
+ return $data;
+ });
+
+ add_hook_filter('role.permissions.plugin', function ($data) {
+ $data[] = [
+ 'title' => 'OpenAI',
+ 'permissions' => [
+ [
+ 'code' => 'openai_index',
+ 'name' => 'ChatGPT',
+ ],
+ ],
+ ];
+
+ return $data;
+ });
+ }
+}
diff --git a/plugins/Openai/Controllers/OpenaiController.php b/plugins/Openai/Controllers/OpenaiController.php
new file mode 100644
index 00000000..60ffc3b7
--- /dev/null
+++ b/plugins/Openai/Controllers/OpenaiController.php
@@ -0,0 +1,90 @@
+
+ * @created 2023-02-27 16:13:08
+ * @modified 2023-02-27 16:13:08
+ */
+
+namespace Plugin\Openai\Controllers;
+
+use Beike\Admin\Http\Controllers\Controller;
+use Illuminate\Http\Request;
+use Plugin\Openai\Services\OpenAIService;
+
+class OpenaiController extends Controller
+{
+ /**
+ * OpenAI home page.
+ *
+ * @return mixed
+ */
+ public function index()
+ {
+ $plugin = plugin('openai');
+
+ $error = '';
+ $baseUrl = config('beike.api_url') . '/api/openai';
+ $apiType = plugin_setting('openai.api_type');
+ if ($apiType == 'own') {
+ $apiKey = plugin_setting('openai.api_key');
+ if (empty($apiKey)) {
+ $error = trans('Openai::common.empty_api_key');
+ }
+ $baseUrl = config('app.url') . '/admin/openai';
+ }
+
+ $data = [
+ 'name' => $plugin->getLocaleName(),
+ 'description' => $plugin->getLocaleDescription(),
+ 'type' => $apiType,
+ 'base' => $baseUrl,
+ 'error' => $error,
+ ];
+
+ return view('Openai::admin.openai', $data);
+ }
+
+ /**
+ * Send chat completions with OpenAI API
+ *
+ * @param Request $request
+ * @return array|mixed
+ * @throws \Throwable
+ */
+ public function completions(Request $request)
+ {
+ try {
+ $result = (new OpenAIService())->requestAI($request->all());
+ } catch (\Exception $e) {
+ $result = [
+ 'error' => $e->getMessage(),
+ ];
+ }
+
+ return $result;
+ }
+
+ /**
+ * Get histories
+ *
+ * @param Request $request
+ * @return array|mixed
+ */
+ public function histories(Request $request)
+ {
+ try {
+ $perPage = $request->get('per_page', 10);
+ $result = (new OpenAIService())->getOpenaiLogs($perPage);
+ } catch (\Exception $e) {
+ $result = [
+ 'error' => $e->getMessage(),
+ ];
+ }
+
+ return $result;
+ }
+}
diff --git a/plugins/Openai/Lang/en/common.php b/plugins/Openai/Lang/en/common.php
new file mode 100644
index 00000000..efdcdf17
--- /dev/null
+++ b/plugins/Openai/Lang/en/common.php
@@ -0,0 +1,27 @@
+
+ * @created 2022-08-11 15:26:18
+ * @modified 2022-08-11 15:26:18
+ */
+
+return [
+ // Text
+ 'title' => 'OpenAI intelligent chat assistant',
+ 'sub_title' => 'Based on OpenAI GPT3.0 integrated development If you have any questions, please consult qq group 639108380',
+ 'no_question' => 'Please enter the search content in the box below',
+ 'enter_question' => 'Please enter a question',
+ 'loading' => 'loading...',
+ 'no_more' => 'no more',
+ 'qa_q' => 'ask',
+ 'qa_a' => 'answer',
+ 'number_free' => 'The remaining free times of the day',
+ 'api_type' => 'API Method',
+ 'own' => 'Own Key',
+ 'beikeshop' => 'BeikeShop',
+ 'empty_api_key' => 'API Key is empty, please go to the plugin settings - OpenAI - Edit and fill in the API Key first.',
+];
diff --git a/plugins/Openai/Lang/zh_cn/common.php b/plugins/Openai/Lang/zh_cn/common.php
new file mode 100644
index 00000000..7531ee85
--- /dev/null
+++ b/plugins/Openai/Lang/zh_cn/common.php
@@ -0,0 +1,27 @@
+
+ * @created 2022-08-11 15:26:18
+ * @modified 2022-08-11 15:26:18
+ */
+
+return [
+ // Text
+ 'title' => 'OpenAI 智能聊天助手',
+ 'sub_title' => '基于OpenAI GPT3.0 集成开发 如有疑问详询qq群639108380',
+ 'no_question' => '请在下面输入框搜索内容',
+ 'enter_question' => '请输入问题',
+ 'loading' => '加载中...',
+ 'no_more' => '没有更多了',
+ 'qa_q' => '问',
+ 'qa_a' => '答',
+ 'number_free' => '当日剩余免费次数',
+ 'api_type' => 'API 方式',
+ 'own' => '自有Key',
+ 'beikeshop' => 'BeikeShop平台',
+ 'empty_api_key' => 'API Key 为空, 请先到插件设置 - OpenAI - 编辑 填写API Key',
+];
diff --git a/plugins/Openai/Lang/zh_hk/common.php b/plugins/Openai/Lang/zh_hk/common.php
new file mode 100644
index 00000000..cee4b66b
--- /dev/null
+++ b/plugins/Openai/Lang/zh_hk/common.php
@@ -0,0 +1,27 @@
+
+ * @created 2022-08-11 15:26:18
+ * @modified 2022-08-11 15:26:18
+ */
+
+return [
+ // Text
+ 'title' => 'OpenAI 智能聊天助手',
+ 'sub_title' => '基於OpenAI GPT3.0 集成開發 如有疑問詳詢qq群639108380',
+ 'no_question' => '請在下面輸入框搜索內容',
+ 'enter_question' => '請輸入問題',
+ 'loading' => '加載中...',
+ 'no_more' => '没有更多了',
+ 'qa_q' => '問',
+ 'qa_a' => '答',
+ 'number_free' => '當日剩餘免費次數',
+ 'api_type' => 'API 方式',
+ 'own' => '自有 Key',
+ 'beikeshop' => 'BeikeShop 平台',
+ 'empty_api_key' => 'API Key 為空,請先到插件設置 - OpenAI - 編輯 填寫 API Key',
+];
diff --git a/plugins/Openai/Libraries/OpenAI/Base.php b/plugins/Openai/Libraries/OpenAI/Base.php
new file mode 100644
index 00000000..43a8cbe6
--- /dev/null
+++ b/plugins/Openai/Libraries/OpenAI/Base.php
@@ -0,0 +1,161 @@
+
+ * @created 2023-02-22 20:31:42
+ * @modified 2023-02-22 20:31:42
+ */
+
+namespace Plugin\Openai\Libraries\OpenAI;
+
+use Exception;
+
+class Base
+{
+ /**
+ * @var string|bool|mixed
+ */
+ private string $apiKey = '';
+
+ /**
+ * @var int
+ */
+ protected int $maxTokens = 1000;
+
+ /**
+ * @var float
+ */
+ protected float $temperature = 0.5;
+
+ /**
+ * @var int
+ */
+ protected int $number = 1;
+
+ /**
+ * @var string
+ */
+ protected string $prompt;
+
+ /**
+ * OpenAI constructor.
+ * @param string|null $apiKey
+ */
+ public function __construct(?string $apiKey = '')
+ {
+ if ($apiKey) {
+ $this->apiKey = $apiKey;
+ }
+ if (empty($this->apiKey)) {
+ $this->apiKey = env('OPENAI_API_KEY');
+ }
+ }
+
+ /**
+ * Get OpenAI instance.
+ *
+ * @param string|null $apiKey
+ * @return Base
+ */
+ public static function getInstance(?string $apiKey = ''): static
+ {
+ return new self($apiKey);
+ }
+
+ /**
+ * 设置 max_tokens的值
+ * 一般来说,max_tokens值越大,模型的表现就越好,
+ * 但是需要考虑到计算资源的限制,max_tokens值不宜过大。
+ * 一般来说,max_tokens值可以设置在比较合理的范围内,比如500到1000之间。
+ *
+ * @param int $maxTokens
+ * @return $this
+ */
+ public function setMaxTokens(int $maxTokens): static
+ {
+ $this->maxTokens = $maxTokens;
+
+ return $this;
+ }
+
+ /**
+ * 设置temperature参数值, 参数的范围是0.0到2.0之间。
+ * 在较低的温度下,模型会生成更加安全的文本,
+ * 而在较高的温度下,模型会生成更加创新的文本。
+ *
+ * @param float $temperature
+ * @return $this
+ */
+ public function setTemperature(float $temperature): static
+ {
+ $this->temperature = $temperature;
+
+ return $this;
+ }
+
+ /**
+ * 设置 n 参数值
+ * 指定了返回结果的数量。n参数越大,返回的结果数量也越多。
+ *
+ * @param int $number
+ * @return $this
+ */
+ public function setNumber(int $number): static
+ {
+ $this->number = $number;
+
+ return $this;
+ }
+
+ /**
+ * 设置 prompt 参数值
+ * 用于指定一段文本,用于提供给模型参考,以便于生成更加相关的文本
+ *
+ * @param string $prompt
+ * @return $this
+ * @throws Exception
+ */
+ public function setPrompt(string $prompt): static
+ {
+ $this->prompt = trim($prompt);
+ if (empty($this->prompt)) {
+ throw new Exception('prompt 不能为空!');
+ }
+
+ return $this;
+ }
+
+ /**
+ * 发送请求到 OpenAI
+ *
+ * @param $url
+ * @param $data
+ * @return mixed
+ * @throws Exception
+ */
+ protected function request($url, $data): mixed
+ {
+ $ch = curl_init();
+ curl_setopt($ch, CURLOPT_URL, $url);
+ curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
+ curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
+ curl_setopt($ch, CURLOPT_POST, 1);
+ curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
+ curl_setopt($ch, CURLOPT_HTTPHEADER, [
+ 'Content-Type: application/json',
+ 'Authorization: Bearer ' . $this->apiKey,
+ ]);
+
+ $response = curl_exec($ch);
+ if ($response === false) {
+ throw new \Exception(curl_error($ch));
+ }
+ curl_close($ch);
+
+ return json_decode($response, true);
+ }
+}
diff --git a/plugins/Openai/Libraries/OpenAI/Chat.php b/plugins/Openai/Libraries/OpenAI/Chat.php
new file mode 100644
index 00000000..336af1f2
--- /dev/null
+++ b/plugins/Openai/Libraries/OpenAI/Chat.php
@@ -0,0 +1,66 @@
+
+ * @created 2023-03-02 14:30:10
+ * @modified 2023-03-02 14:30:10
+ */
+
+namespace Plugin\Openai\Libraries\OpenAI;
+
+class Chat extends Base
+{
+ /**
+ * @var array 聊天上下文
+ */
+ private array $messages;
+
+ /**
+ * @param string|null $apiKey
+ * @return static
+ */
+ public static function getInstance(?string $apiKey = ''): static
+ {
+ return new self($apiKey);
+ }
+
+ /**
+ * https://platform.openai.com/docs/guides/chat/introduction
+ *
+ * @param $messages
+ * @param $prompt
+ * @return Chat
+ */
+ public function setMessages($messages, $prompt): self
+ {
+ $messages[] = ['role' => 'user', 'content' => $prompt];
+ $this->messages = $messages;
+
+ return $this;
+ }
+
+ /**
+ * 发送请求到 OpenAI
+ *
+ * @return mixed
+ * @throws \Exception
+ */
+ public function create(): mixed
+ {
+ $model = 'gpt-3.5-turbo';
+ $url = 'https://api.openai.com/v1/chat/completions';
+ $data = [
+ 'messages' => $this->messages,
+ 'max_tokens' => $this->maxTokens,
+ 'temperature' => $this->temperature,
+ 'n' => $this->number,
+ 'stop' => '\n',
+ 'model' => $model,
+ ];
+
+ return $this->request($url, $data);
+ }
+}
diff --git a/plugins/Openai/Libraries/OpenAI/Completion.php b/plugins/Openai/Libraries/OpenAI/Completion.php
new file mode 100644
index 00000000..a30f822a
--- /dev/null
+++ b/plugins/Openai/Libraries/OpenAI/Completion.php
@@ -0,0 +1,33 @@
+
+ * @created 2023-03-02 14:37:15
+ * @modified 2023-03-02 14:37:15
+ */
+
+namespace Plugin\Openai\Libraries\OpenAI;
+
+class Completion extends Base
+{
+ /**
+ * @throws \Exception
+ */
+ public function create()
+ {
+ $model = 'text-davinci-003';
+ $url = 'https://api.openai.com/v1/completions';
+ $data = [
+ 'prompt' => $this->prompt,
+ 'max_tokens' => $this->maxTokens,
+ 'temperature' => $this->temperature,
+ 'n' => $this->number,
+ 'stop' => '\n',
+ 'model' => $model,
+ ];
+ $this->request($url, $data);
+ }
+}
diff --git a/plugins/Openai/Migrations/2023_02_27_173221_add_openai_logs.php b/plugins/Openai/Migrations/2023_02_27_173221_add_openai_logs.php
new file mode 100644
index 00000000..f668eebe
--- /dev/null
+++ b/plugins/Openai/Migrations/2023_02_27_173221_add_openai_logs.php
@@ -0,0 +1,38 @@
+id();
+ $table->integer('user_id')->index('user_id');
+ $table->text('question');
+ $table->text('answer');
+ $table->string('request_ip');
+ $table->text('user_agent');
+ $table->timestamps();
+ });
+ }
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ Schema::dropIfExists('openai_logs');
+ }
+};
diff --git a/plugins/Openai/Models/OpenaiLog.php b/plugins/Openai/Models/OpenaiLog.php
new file mode 100644
index 00000000..e6b8114f
--- /dev/null
+++ b/plugins/Openai/Models/OpenaiLog.php
@@ -0,0 +1,32 @@
+
+ * @created 2023-03-13 16:48:17
+ * @modified 2023-03-13 16:48:17
+ */
+
+namespace Plugin\Openai\Models;
+
+use Illuminate\Database\Eloquent\Model;
+
+class OpenaiLog extends Model
+{
+ public $timestamps = true;
+
+ protected $table = 'openai_logs';
+
+ protected $fillable = [
+ 'user_id', 'question', 'answer', 'request_ip', 'user_agent',
+ ];
+
+ protected $appends = ['created_format'];
+
+ public function getCreatedFormatAttribute()
+ {
+ return $this->created_at->format('Y-m-d H:i:s');
+ }
+}
diff --git a/plugins/Openai/Routes/admin.php b/plugins/Openai/Routes/admin.php
new file mode 100644
index 00000000..184a3ed2
--- /dev/null
+++ b/plugins/Openai/Routes/admin.php
@@ -0,0 +1,18 @@
+
+ * @created 2022-08-04 16:17:53
+ * @modified 2022-08-04 16:17:53
+ */
+
+use Illuminate\Support\Facades\Route;
+use Plugin\Openai\Controllers\OpenaiController;
+
+Route::middleware('can:openai_index')->get('/openai', [OpenaiController::class, 'index'])->name('openai.index');
+
+Route::middleware('can:openai_index')->get('/openai/histories', [OpenaiController::class, 'histories'])->name('openai.histories');
+Route::middleware('can:openai_index')->post('/openai/completions', [OpenaiController::class, 'completions'])->name('openai.completions');
diff --git a/plugins/Openai/Services/OpenAIService.php b/plugins/Openai/Services/OpenAIService.php
new file mode 100644
index 00000000..8eff9252
--- /dev/null
+++ b/plugins/Openai/Services/OpenAIService.php
@@ -0,0 +1,120 @@
+
+ * @created 2023-03-13 16:42:52
+ * @modified 2023-03-13 16:42:52
+ */
+
+namespace Plugin\Openai\Services;
+
+use Plugin\Openai\Libraries\OpenAI\Chat;
+use Plugin\Openai\Models\OpenaiLog;
+
+class OpenAIService
+{
+ /**
+ * 发起 OpenAI 请求
+ *
+ * @param $data
+ * @return mixed
+ * @throws \Throwable
+ */
+ public function requestAI($data)
+ {
+ $prompt = $data['prompt'] ?? '';
+ $apiKey = plugin_setting('openai.api_key');
+
+ $openAI = Chat::getInstance($apiKey);
+ $messages = $this->getChatMessages();
+ $response = $openAI->setMessages($messages, $prompt)->create();
+
+ $result['prompt'] = $prompt;
+
+ $error = trim($response['error']['message'] ?? '');
+ if ($error) {
+ $result['error'] = $error;
+ } else {
+ $content = trim($response['choices'][0]['message']['content'] ?? '');
+
+ $response['choices'][0]['text'] = $content;
+
+ $result['response'] = $response;
+ $newLog = $this->createOpenaiLog($prompt, $content);
+ $result['created_format'] = $newLog->created_format;
+ }
+
+ return $result;
+ }
+
+ /**
+ * @param $question
+ * @param $answer
+ * @return OpenaiLog
+ * @throws \Throwable
+ */
+ private function createOpenaiLog($question, $answer): OpenaiLog
+ {
+ $user = current_user();
+ $newOpenaiLog = new OpenaiLog([
+ 'user_id' => $user->id ?? 0,
+ 'question' => trim($question),
+ 'answer' => trim($answer),
+ 'request_ip' => request()->getClientIp(),
+ 'user_agent' => request()->userAgent(),
+ ]);
+ $newOpenaiLog->saveOrFail();
+
+ return $newOpenaiLog;
+ }
+
+ /**
+ * 获取聊天记录
+ *
+ * @param int $perPage
+ * @return mixed
+ */
+ public function getOpenaiLogs(int $perPage = 10)
+ {
+ $user = current_user();
+
+ return OpenaiLog::query()
+ ->select(['user_id', 'question', 'answer', 'created_at'])
+ ->where('user_id', $user->id)
+ ->orderByDesc('created_at')
+ ->paginate($perPage);
+ }
+
+ /**
+ * https://platform.openai.com/docs/guides/chat/introduction
+ *
+ * messages=[
+ * {"role": "system", "content": "You are a helpful assistant."},
+ * {"role": "user", "content": "Who won the world series in 2020?"},
+ * {"role": "assistant", "content": "The Los Angeles Dodgers won the World Series in 2020."},
+ * {"role": "user", "content": "Where was it played?"}
+ * ]
+ *
+ * @return array
+ */
+ public function getChatMessages()
+ {
+ $logs = OpenaiLog::query()
+ ->select(['user_id', 'question', 'answer', 'created_at'])
+ ->limit(5)
+ ->get();
+
+ $messages[] = [
+ 'role' => 'system', 'content' => 'You are a helpful assistant.',
+ ];
+ foreach ($logs as $log) {
+ $messages[] = ['role' => 'user', 'content' => $log->question];
+ $messages[] = ['role' => 'assistant', 'content' => $log->answer];
+ }
+
+ return $messages;
+ }
+}
diff --git a/plugins/Openai/Static/image/logo.png b/plugins/Openai/Static/image/logo.png
new file mode 100644
index 00000000..d510068d
Binary files /dev/null and b/plugins/Openai/Static/image/logo.png differ
diff --git a/plugins/Openai/Views/admin/openai.blade.php b/plugins/Openai/Views/admin/openai.blade.php
new file mode 100644
index 00000000..17480c6c
--- /dev/null
+++ b/plugins/Openai/Views/admin/openai.blade.php
@@ -0,0 +1,307 @@
+@extends('admin::layouts.master')
+
+@section('title', $name)
+
+@push('header')
+
+
+
+@endpush
+
+@section('content')
+
+
+
+
{{ __('Openai::common.no_question') }}
+
+
+
+
+
+
+
+
+ @if ($type != 'own')
+
{{ __('Openai::common.number_free') }}:
+ {{ __('Openai::common.loading') }}
+
+ @endif
+ @if ($error)
+
+
+ {{ $error }}
+
+ @endif
+
{{ $description }}
+
+
+
+
+
+@endsection
diff --git a/plugins/Openai/columns.php b/plugins/Openai/columns.php
new file mode 100644
index 00000000..6f076006
--- /dev/null
+++ b/plugins/Openai/columns.php
@@ -0,0 +1,31 @@
+
+ * @created 2023-03-13 16:08:41
+ * @modified 2023-03-13 16:08:41
+ */
+
+return [
+ [
+ 'name' => 'api_type',
+ 'label_key' => 'common.api_type',
+ 'type' => 'select',
+ 'options' => [
+ ['value' => 'own', 'label_key' => 'common.own'],
+ ['value' => 'beikeshop', 'label_key' => 'common.beikeshop'],
+ ],
+ 'required' => true,
+ 'description' => '如果选择 BeikeShop 平台, 则 API Key 可以留空',
+ ],
+ [
+ 'name' => 'api_key',
+ 'label' => 'API Key',
+ 'type' => 'string',
+ 'required' => false,
+ 'description' => '获取 API Key',
+ ],
+];
diff --git a/plugins/Openai/config.json b/plugins/Openai/config.json
new file mode 100644
index 00000000..fe658e11
--- /dev/null
+++ b/plugins/Openai/config.json
@@ -0,0 +1,18 @@
+{
+ "code": "openai",
+ "name": {
+ "zh_cn": "ChatGPT 智能聊天助手",
+ "en": "Integration with OpenAI ChatGPT API"
+ },
+ "description": {
+ "zh_cn": "ChatGPT 智能聊天助手, 基于 OpenAI API(gpt-3.5-turbo) 开发, 如有疑问详询QQ群 639108380",
+ "en": "Integration with OpenAI ChatGPT API"
+ },
+ "type": "feature",
+ "version": "v1.1.0",
+ "icon": "/image/logo.png",
+ "author": {
+ "name": "成都光大网络科技有限公司",
+ "email": "yangjin@guangda.work"
+ }
+}
diff --git a/plugins/Paypal/Controllers/PaypalController.php b/plugins/Paypal/Controllers/PaypalController.php
new file mode 100644
index 00000000..2798948b
--- /dev/null
+++ b/plugins/Paypal/Controllers/PaypalController.php
@@ -0,0 +1,118 @@
+
+ * @created 2022-08-10 18:57:56
+ * @modified 2022-08-10 18:57:56
+ *
+ * https://www.zongscan.com/demo333/1311.html
+ * https://clickysoft.com/how-to-integrate-paypal-payment-gateway-in-laravel/
+ * https://www.positronx.io/how-to-integrate-paypal-payment-gateway-in-laravel/
+ */
+
+namespace Plugin\Paypal\Controllers;
+
+use Beike\Repositories\OrderPaymentRepo;
+use Beike\Repositories\OrderRepo;
+use Beike\Services\StateMachineService;
+use Illuminate\Http\JsonResponse;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\DB;
+use Srmklive\PayPal\Services\PayPal;
+
+class PaypalController
+{
+ private PayPal $paypalClient;
+
+ private function initPaypal()
+ {
+ $paypalSetting = plugin_setting('paypal');
+ $config = [
+ 'mode' => $paypalSetting['sandbox_mode'] ? 'sandbox' : 'live',
+ 'sandbox' => [
+ 'client_id' => $paypalSetting['sandbox_client_id'],
+ 'client_secret' => $paypalSetting['sandbox_secret'],
+ ],
+ 'live' => [
+ 'client_id' => $paypalSetting['live_client_id'],
+ 'client_secret' => $paypalSetting['live_secret'],
+ ],
+ 'payment_action' => 'Sale',
+ 'currency' => 'USD',
+ 'notify_url' => '',
+ 'locale' => 'en_US',
+ 'validate_ssl' => false,
+ ];
+ config(['paypal' => null]);
+ $this->paypalClient = new PayPal($config);
+ $token = $this->paypalClient->getAccessToken();
+ $this->paypalClient->setAccessToken($token);
+ }
+
+ /**
+ * 创建 paypal 订单
+ * @param Request $request
+ * @return JsonResponse
+ * @throws \Throwable
+ */
+ public function create(Request $request): JsonResponse
+ {
+ $this->initPaypal();
+ $data = \json_decode($request->getContent(), true);
+ $orderNumber = $data['orderNumber'];
+ $customer = current_customer();
+ $order = OrderRepo::getOrderByNumber($orderNumber, $customer);
+ $total = round($order->total, 2);
+
+ $paypalOrder = $this->paypalClient->createOrder([
+ 'intent' => 'CAPTURE',
+ 'purchase_units' => [
+ [
+ 'amount' => [
+ 'currency_code' => $order->currency_code,
+ 'value' => $total,
+ ],
+ 'description' => 'test',
+ ],
+ ],
+ ]);
+
+ return response()->json($paypalOrder);
+ }
+
+ /**
+ * 客户同意后扣款回调
+ * @param Request $request
+ * @return JsonResponse
+ * @throws \Throwable
+ */
+ public function capture(Request $request): JsonResponse
+ {
+ $this->initPaypal();
+ $data = \json_decode($request->getContent(), true);
+ $orderNumber = $data['orderNumber'];
+ $customer = current_customer();
+ $order = OrderRepo::getOrderByNumber($orderNumber, $customer);
+
+ OrderPaymentRepo::createOrUpdatePayment($order->id, ['request' => $data]);
+ $paypalOrderId = $data['paypalOrderId'];
+ $result = $this->paypalClient->capturePaymentOrder($paypalOrderId);
+ OrderPaymentRepo::createOrUpdatePayment($order->id, ['response' => $result]);
+
+ try {
+ DB::beginTransaction();
+ if ($result['status'] === 'COMPLETED') {
+ StateMachineService::getInstance($order)->changeStatus(StateMachineService::PAID);
+ DB::commit();
+ }
+ } catch (\Exception $e) {
+ DB::rollBack();
+ dd($e);
+ }
+
+ return response()->json($result);
+ }
+}
diff --git a/plugins/Paypal/Lang/en/setting.php b/plugins/Paypal/Lang/en/setting.php
new file mode 100644
index 00000000..e6b4211a
--- /dev/null
+++ b/plugins/Paypal/Lang/en/setting.php
@@ -0,0 +1,15 @@
+
+ * @created 2022-08-11 15:26:18
+ * @modified 2022-08-11 15:26:18
+ */
+
+return [
+ 'sandbox_mode' => 'Sandbox Mode',
+ 'enabled' => 'Enabled',
+];
diff --git a/plugins/Paypal/Lang/zh_cn/setting.php b/plugins/Paypal/Lang/zh_cn/setting.php
new file mode 100644
index 00000000..098ac7db
--- /dev/null
+++ b/plugins/Paypal/Lang/zh_cn/setting.php
@@ -0,0 +1,15 @@
+
+ * @created 2022-08-11 15:26:18
+ * @modified 2022-08-11 15:26:18
+ */
+
+return [
+ 'sandbox_mode' => '沙盒模式',
+ 'enabled' => '开启',
+];
diff --git a/plugins/Paypal/Routes/admin.php b/plugins/Paypal/Routes/admin.php
new file mode 100644
index 00000000..bff13b1f
--- /dev/null
+++ b/plugins/Paypal/Routes/admin.php
@@ -0,0 +1,10 @@
+
+ * @created 2022-08-12 10:33:01
+ * @modified 2022-08-12 10:33:01
+ */
diff --git a/plugins/Paypal/Routes/shop.php b/plugins/Paypal/Routes/shop.php
new file mode 100644
index 00000000..d40b5028
--- /dev/null
+++ b/plugins/Paypal/Routes/shop.php
@@ -0,0 +1,18 @@
+
+ * @created 2022-08-12 10:33:13
+ * @modified 2022-08-12 10:33:13
+ */
+
+use Illuminate\Support\Facades\Route;
+use Plugin\Paypal\Controllers\PaypalController;
+
+Route::group(['prefix' => 'paypal'], function () {
+ Route::post('/create', [PaypalController::class, 'create']);
+ Route::post('/capture', [PaypalController::class, 'capture']);
+});
diff --git a/plugins/Paypal/Static/image/logo.png b/plugins/Paypal/Static/image/logo.png
new file mode 100644
index 00000000..5f64640b
Binary files /dev/null and b/plugins/Paypal/Static/image/logo.png differ
diff --git a/plugins/Paypal/Views/checkout/payment.blade.php b/plugins/Paypal/Views/checkout/payment.blade.php
new file mode 100644
index 00000000..58bb8523
--- /dev/null
+++ b/plugins/Paypal/Views/checkout/payment.blade.php
@@ -0,0 +1,73 @@
+
+
+
+
+@if($payment_setting['sandbox_mode'])
+
+@else
+
+@endif
+
+
diff --git a/plugins/Paypal/columns.php b/plugins/Paypal/columns.php
new file mode 100644
index 00000000..266a4f36
--- /dev/null
+++ b/plugins/Paypal/columns.php
@@ -0,0 +1,56 @@
+
+ * @created 2022-06-29 21:16:23
+ * @modified 2022-06-29 21:16:23
+ */
+
+return [
+ [
+ 'name' => 'sandbox_client_id',
+ 'label' => 'Sandbox Client ID',
+ 'type' => 'string',
+ 'required' => true,
+ 'rules' => 'required|size:80',
+ 'description' => '沙盒模式 Client ID',
+ ],
+ [
+ 'name' => 'sandbox_secret',
+ 'label' => 'Sandbox Secret',
+ 'type' => 'string',
+ 'required' => true,
+ 'rules' => 'required|size:80',
+ 'description' => '沙盒模式 Secret',
+ ],
+ [
+ 'name' => 'live_client_id',
+ 'label' => 'Live Client ID',
+ 'type' => 'string',
+ 'required' => true,
+ 'rules' => 'required|size:80',
+ 'description' => '正式环境 Client ID',
+ ],
+ [
+ 'name' => 'live_secret',
+ 'label' => 'Live Secret',
+ 'type' => 'string',
+ 'required' => true,
+ 'rules' => 'required|size:80',
+ 'description' => '正式环境 Secret',
+ ],
+ [
+ 'name' => 'sandbox_mode',
+ 'label_key' => 'setting.sandbox_mode',
+ 'type' => 'select',
+ 'options' => [
+ ['value' => '1', 'label_key' => 'setting.enabled'],
+ ['value' => '0', 'label' => '关闭'],
+ ],
+ 'required' => true,
+ 'description' => '',
+ ],
+];
diff --git a/plugins/Paypal/config.json b/plugins/Paypal/config.json
new file mode 100644
index 00000000..252c7b0e
--- /dev/null
+++ b/plugins/Paypal/config.json
@@ -0,0 +1,12 @@
+{
+ "code": "paypal",
+ "name": "PayPal",
+ "description": "PayPal 支付 PayPal Developer",
+ "type": "payment",
+ "version": "v1.0.0",
+ "icon": "/image/logo.png",
+ "author": {
+ "name": "成都光大网络科技有限公司",
+ "email": "yangjin@guangda.work"
+ }
+}
diff --git a/plugins/Social/Bootstrap.php b/plugins/Social/Bootstrap.php
new file mode 100644
index 00000000..fa572c7a
--- /dev/null
+++ b/plugins/Social/Bootstrap.php
@@ -0,0 +1,48 @@
+
+ * @created 2022-10-12 17:33:29
+ * @modified 2022-10-12 17:33:29
+ */
+
+namespace Plugin\Social;
+
+class Bootstrap
+{
+ public function boot()
+ {
+ $this->addSocialData();
+
+ add_hook_blade('admin.plugin.form', function ($callback, $output, $data) {
+ $code = $data['plugin']->code;
+ if ($code == 'social') {
+ return view('Social::admin.config_form', $data)->render();
+ }
+
+ return $output;
+ }, 1);
+ }
+
+ /**
+ * 增加第三方登录方式
+ */
+ private function addSocialData()
+ {
+ add_hook_filter('login.social.buttons', function ($buttons) {
+ $providers = plugin_setting('social.setting');
+ if (empty($providers)) {
+ return $buttons;
+ }
+
+ foreach ($providers as $provider) {
+ $buttons[] = view('Social::shop/social_button', ['provider' => $provider])->render();
+ }
+
+ return $buttons;
+ });
+ }
+}
diff --git a/plugins/Social/Controllers/AdminSocialController.php b/plugins/Social/Controllers/AdminSocialController.php
new file mode 100644
index 00000000..dd8c4e56
--- /dev/null
+++ b/plugins/Social/Controllers/AdminSocialController.php
@@ -0,0 +1,29 @@
+
+ * @created 2022-09-30 18:46:38
+ * @modified 2022-09-30 18:46:38
+ */
+
+namespace Plugin\Social\Controllers;
+
+use Beike\Admin\Http\Controllers\Controller;
+use Beike\Repositories\SettingRepo;
+use Illuminate\Http\Request;
+
+class AdminSocialController extends Controller
+{
+ /**
+ * @throws \Throwable
+ */
+ public function saveSetting(Request $request): array
+ {
+ SettingRepo::storeValue('setting', $request->all(), 'social', 'plugin');
+
+ return json_success('保存成功');
+ }
+}
diff --git a/plugins/Social/Controllers/ShopSocialController.php b/plugins/Social/Controllers/ShopSocialController.php
new file mode 100644
index 00000000..e2e012dd
--- /dev/null
+++ b/plugins/Social/Controllers/ShopSocialController.php
@@ -0,0 +1,72 @@
+
+ * @created 2022-09-30 18:46:38
+ * @modified 2022-09-30 18:46:38
+ */
+
+namespace Plugin\Social\Controllers;
+
+use Beike\Admin\Http\Controllers\Controller;
+use Beike\Models\Customer;
+use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\Config;
+use Laravel\Socialite\Facades\Socialite;
+use Plugin\Social\Repositories\CustomerRepo;
+
+class ShopSocialController extends Controller
+{
+ public function initSocial()
+ {
+ $providerSettings = plugin_setting('social.setting', []);
+ foreach ($providerSettings as $providerSetting) {
+ $provider = $providerSetting['provider'];
+ if (empty($provider)) {
+ continue;
+ }
+ $config = [
+ 'client_id' => $providerSetting['key'],
+ 'client_secret' => $providerSetting['secret'],
+ 'redirect' => shop_route('social.callback', $provider),
+ ];
+ Config::set("services.{$provider}", $config);
+ }
+ }
+
+ /**
+ * @param $provider
+ * @return mixed
+ */
+ public function redirect($provider)
+ {
+ try {
+ $this->initSocial();
+
+ return Socialite::driver($provider)->redirect();
+ } catch (\Exception $e) {
+ exit($e->getMessage());
+ }
+ }
+
+ /**
+ * @param $provider
+ * @return mixed
+ */
+ public function callback($provider)
+ {
+ try {
+ $this->initSocial();
+ $userData = Socialite::driver($provider)->user();
+ $customer = CustomerRepo::createCustomer($provider, $userData);
+ Auth::guard(Customer::AUTH_GUARD)->login($customer);
+
+ return view('Social::shop/callback');
+ } catch (\Exception $e) {
+ exit($e->getMessage());
+ }
+ }
+}
diff --git a/plugins/Social/Lang/en/providers.php b/plugins/Social/Lang/en/providers.php
new file mode 100644
index 00000000..18c9d669
--- /dev/null
+++ b/plugins/Social/Lang/en/providers.php
@@ -0,0 +1,36 @@
+
+ * @created 2022-10-13 11:30:33
+ * @modified 2022-10-13 11:30:33
+ */
+
+return [
+ 'alipay' => 'Alipay',
+ 'azure' => 'Azure',
+ 'dingtalk' => 'Dingtalk',
+ 'douyin' => 'Douyin',
+ 'douban' => 'Douban',
+ 'facebook' => 'Facebook',
+ 'feishu' => 'Feishu',
+ 'figma' => 'Figma',
+ 'github' => 'Github',
+ 'gitee' => 'Gitee',
+ 'google' => 'Google',
+ 'line' => 'Line',
+ 'linkedin' => 'Linkedin',
+ 'open-wework' => 'Open-wework',
+ 'outlook' => 'Outlook',
+ 'qcloud' => 'Qcloud',
+ 'qq' => 'QQ',
+ 'taobao' => 'Taobao',
+ 'tapd' => 'Tapd',
+ 'twitter' => 'Twitter',
+ 'wechat' => 'Wechat',
+ 'wework' => 'Wework',
+ 'weibo' => 'Weibo',
+];
diff --git a/plugins/Social/Lang/en/setting.php b/plugins/Social/Lang/en/setting.php
new file mode 100644
index 00000000..7d38c294
--- /dev/null
+++ b/plugins/Social/Lang/en/setting.php
@@ -0,0 +1,60 @@
+
+ * @created 2022-08-11 15:26:18
+ * @modified 2022-08-11 15:26:18
+ */
+
+return [
+ // Text
+ 'text_module' => 'Modules',
+ 'text_success' => 'Success: You have modified module OpenCart OmniAuth!',
+ 'text_copyright' => 'OpenCart.cn OmniAuth © %s',
+ 'text_omni_explain' => 'This plugin supports Facebook, Twitter, Google etc.',
+ 'text_omni_explain_2' => 'To use a third-party login, you need to apply to the corresponding platform, and fill in the obtained ID and key to the corresponding input box.',
+ 'text_wechat_title' => 'WeChat scan code login application address',
+ 'text_wechat_info' => 'WeChat open platform',
+ 'text_qq_title' => 'QQ login application address',
+ 'text_qq_info' => 'QQ interconnection',
+ 'text_weibo_title' => 'Weibo login application address',
+ 'text_weibo_info' => 'Weibo open platform',
+ 'text_facebook_title' => 'Facebook login application address',
+ 'text_google_title' => 'Google login application address',
+ 'text_twitter_title' => 'Twitter login application address',
+ 'text_help_msg' => 'help information',
+
+ 'copy_success' => 'Copy Success',
+
+ // Entry
+ 'entry_status' => 'Status',
+ 'entry_bind' => 'Force Bind',
+ 'entry_debug' => 'Debug Mode',
+
+ 'entry_provider' => 'Provider',
+ 'entry_key' => 'Client ID',
+ 'entry_secret' => 'Client Secret',
+ 'entry_scope' => 'Flags (optional)',
+ 'entry_callback' => 'Callback URL',
+ 'entry_sort_order' => 'Sort Order',
+
+ // Button
+ 'button_add_row' => 'Add Provider',
+
+ // Error
+ 'error_permission' => 'Warning: You do not have permission to modify module OpenCart OmniAuth!',
+
+ // Providers
+ 'wechat' => 'WeChat',
+ 'wechatofficial' => 'WeChatOfficial',
+ 'qq' => 'QQ',
+ 'weibo' => 'Weibo',
+ 'facebook' => 'Facebook',
+ 'google' => 'Google',
+ 'twitter' => 'Twitter',
+
+ 'instagram' => 'Instagram',
+];
diff --git a/plugins/Social/Lang/zh_cn/providers.php b/plugins/Social/Lang/zh_cn/providers.php
new file mode 100644
index 00000000..a81983f4
--- /dev/null
+++ b/plugins/Social/Lang/zh_cn/providers.php
@@ -0,0 +1,36 @@
+
+ * @created 2022-10-13 11:30:33
+ * @modified 2022-10-13 11:30:33
+ */
+
+return [
+ 'alipay' => '支付宝',
+ 'azure' => 'Azure',
+ 'dingtalk' => '钉钉',
+ 'douyin' => '抖音',
+ 'douban' => '豆瓣',
+ 'facebook' => 'Facebook',
+ 'feishu' => '飞书',
+ 'figma' => 'Figma',
+ 'github' => 'GitHub',
+ 'gitee' => 'Gitee',
+ 'google' => 'Google',
+ 'line' => 'Line',
+ 'linkedin' => 'Linkedin',
+ 'open-wework' => 'open-wework',
+ 'outlook' => 'Outlook',
+ 'qcloud' => '腾讯云',
+ 'qq' => 'QQ',
+ 'taobao' => '淘宝',
+ 'tapd' => 'Tapd',
+ 'twitter' => 'Twitter',
+ 'wechat' => '微信',
+ 'wework' => '企业微信',
+ 'weibo' => '微博',
+];
diff --git a/plugins/Social/Lang/zh_cn/setting.php b/plugins/Social/Lang/zh_cn/setting.php
new file mode 100644
index 00000000..99c0be3d
--- /dev/null
+++ b/plugins/Social/Lang/zh_cn/setting.php
@@ -0,0 +1,49 @@
+
+ * @created 2022-08-11 15:26:18
+ * @modified 2022-08-11 15:26:18
+ */
+
+return [
+ // Text
+ 'text_module' => '模块',
+ 'text_success' => '成功: 您成功修改第三方登录配置!',
+ 'text_copyright' => 'OpenCart.cn 获取帮助 © %s',
+ 'text_omni_explain' => '本模块支持微Facebook, Twitter, Google等第三方登录',
+ 'text_omni_explain_2' => '要使用第三方登录, 需要到对应平台申请开通, 并把获取到的 ID 和密钥填写到上面对应的输入框',
+ 'text_wechat_title' => '微信扫码登录申请地址',
+ 'text_wechat_info' => '微信开放平台',
+ 'text_qq_title' => 'QQ登录申请地址',
+ 'text_qq_info' => 'QQ互联',
+ 'text_weibo_title' => '微博登录申请地址',
+ 'text_weibo_info' => '微博开放平台',
+ 'text_facebook_title' => 'Facebook登录申请地址',
+ 'text_google_title' => 'Google登录申请地址',
+ 'text_twitter_title' => 'Twitter登录申请地址',
+ 'text_help_msg' => '帮助信息',
+
+ 'copy_success' => '复制成功',
+
+ // Entry
+ 'entry_status' => '状态',
+ 'entry_bind' => '强制绑定',
+ 'entry_debug' => '调试模式',
+
+ 'entry_provider' => '类型',
+ 'entry_key' => 'Client ID',
+ 'entry_secret' => 'Client Secret',
+ 'entry_scope' => 'Flags (可选项)',
+ 'entry_callback' => '回调地址',
+ 'entry_sort_order' => '排序',
+
+ // Button
+ 'button_add_row' => '添加类型',
+
+ // Error
+ 'error_permission' => '警告: 您没有权限修改此配置!',
+];
diff --git a/plugins/Social/Migrations/2022_10_13_100354_create_customer_socials.php b/plugins/Social/Migrations/2022_10_13_100354_create_customer_socials.php
new file mode 100644
index 00000000..796a242c
--- /dev/null
+++ b/plugins/Social/Migrations/2022_10_13_100354_create_customer_socials.php
@@ -0,0 +1,37 @@
+id();
+ $table->integer('customer_id');
+ $table->string('provider');
+ $table->string('user_id');
+ $table->string('union_id');
+ $table->text('access_token');
+ $table->text('extra');
+ $table->timestamps();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ Schema::dropIfExists('customer_socials');
+ }
+};
diff --git a/plugins/Social/Models/CustomerSocial.php b/plugins/Social/Models/CustomerSocial.php
new file mode 100644
index 00000000..20419eca
--- /dev/null
+++ b/plugins/Social/Models/CustomerSocial.php
@@ -0,0 +1,30 @@
+
+ * @created 2022-10-13 10:35:44
+ * @modified 2022-10-13 10:35:44
+ */
+
+namespace Plugin\Social\Models;
+
+use Beike\Models\Customer;
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
+
+class CustomerSocial extends Model
+{
+ public $table = 'customer_socials';
+
+ public $fillable = [
+ 'customer_id', 'provider', 'user_id', 'union_id', 'access_token', 'extra',
+ ];
+
+ public function customer(): BelongsTo
+ {
+ return $this->belongsTo(Customer::class);
+ }
+}
diff --git a/plugins/Social/Repositories/CustomerRepo.php b/plugins/Social/Repositories/CustomerRepo.php
new file mode 100644
index 00000000..2b7dc358
--- /dev/null
+++ b/plugins/Social/Repositories/CustomerRepo.php
@@ -0,0 +1,120 @@
+
+ * @created 2022-10-13 09:57:13
+ * @modified 2022-10-13 09:57:13
+ */
+
+namespace Plugin\Social\Repositories;
+
+use Beike\Models\Customer;
+use Beike\Shop\Services\AccountService;
+use Illuminate\Database\Eloquent\Builder;
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Support\Str;
+use Laravel\Socialite\Two\User;
+use Plugin\Social\Models\CustomerSocial;
+
+class CustomerRepo
+{
+ /**
+ * 允许的第三方服务
+ */
+ private const PROVIDERS = [
+ 'facebook',
+ 'twitter',
+ 'google',
+ ];
+
+ public static function allProviders(): array
+ {
+ $items = [];
+ foreach (self::PROVIDERS as $provider) {
+ $items[] = [
+ 'code' => $provider,
+ 'label' => trans("Social::providers.{$provider}"),
+ ];
+ }
+
+ return $items;
+ }
+
+ /**
+ * 创建客户, 关联第三方用户数据
+ *
+ * @param $provider
+ * @param User $userData
+ * @return Customer
+ */
+ public static function createCustomer($provider, User $userData): Customer
+ {
+ $social = self::getCustomerByProvider($provider, $userData->getId());
+ $customer = $social->customer ?? null;
+ if ($customer) {
+ return $customer;
+ }
+
+ $email = $userData->getEmail();
+ if (empty($email)) {
+ $email = strtolower(Str::random(8)) . "@{$provider}.com";
+ }
+ $customer = Customer::query()->where('email', $email)->first();
+ if (empty($customer)) {
+ $customerData = [
+ 'from' => $provider,
+ 'email' => $email,
+ 'name' => $userData->getName(),
+ 'avatar' => $userData->getAvatar(),
+ ];
+ $customer = AccountService::register($customerData);
+ }
+
+ self::createSocial($customer, $provider, $userData);
+
+ return $customer;
+ }
+
+ /**
+ * @param $customer
+ * @param $provider
+ * @param User $userData
+ * @return Model|Builder
+ */
+ public static function createSocial($customer, $provider, User $userData): Model|Builder
+ {
+ $social = self::getCustomerByProvider($provider, $userData->getId());
+ if ($social) {
+ return $social;
+ }
+
+ $socialData = [
+ 'customer_id' => $customer->id,
+ 'provider' => $provider,
+ 'user_id' => $userData->getId(),
+ 'union_id' => '',
+ 'access_token' => $userData->token,
+ 'extra' => json_encode($userData->getRaw()),
+ ];
+
+ return CustomerSocial::query()->create($socialData);
+ }
+
+ /**
+ * 通过 provider 和 user_id 获取已存在 social
+ * @param $provider
+ * @param $userId
+ * @return Model|Builder|null
+ */
+ private static function getCustomerByProvider($provider, $userId): Model|Builder|null
+ {
+ return CustomerSocial::query()
+ ->with(['customer'])
+ ->where('provider', $provider)
+ ->where('user_id', $userId)
+ ->first();
+ }
+}
diff --git a/plugins/Social/Routes/admin.php b/plugins/Social/Routes/admin.php
new file mode 100644
index 00000000..0bf948a1
--- /dev/null
+++ b/plugins/Social/Routes/admin.php
@@ -0,0 +1,15 @@
+
+ * @created 2022-08-04 16:17:53
+ * @modified 2022-08-04 16:17:53
+ */
+
+use Illuminate\Support\Facades\Route;
+use Plugin\Social\Controllers\AdminSocialController;
+
+Route::post('/social/setting', [AdminSocialController::class, 'saveSetting'])->name('plugin.social.setting');
diff --git a/plugins/Social/Routes/shop.php b/plugins/Social/Routes/shop.php
new file mode 100644
index 00000000..50f26314
--- /dev/null
+++ b/plugins/Social/Routes/shop.php
@@ -0,0 +1,16 @@
+
+ * @created 2022-09-30 18:52:54
+ * @modified 2022-09-30 18:52:54
+ */
+
+use Illuminate\Support\Facades\Route;
+use Plugin\Social\Controllers\ShopSocialController;
+
+Route::get('/social/redirect/{provider}', [ShopSocialController::class, 'redirect'])->name('social.redirect');
+Route::get('/social/callbacks/{provider}', [ShopSocialController::class, 'callback'])->name('social.callback');
diff --git a/public/plugin/social/image/alipay.png b/plugins/Social/Static/image/alipay.png
similarity index 100%
rename from public/plugin/social/image/alipay.png
rename to plugins/Social/Static/image/alipay.png
diff --git a/public/plugin/social/image/azure.png b/plugins/Social/Static/image/azure.png
similarity index 100%
rename from public/plugin/social/image/azure.png
rename to plugins/Social/Static/image/azure.png
diff --git a/public/plugin/social/image/dingtalk.png b/plugins/Social/Static/image/dingtalk.png
similarity index 100%
rename from public/plugin/social/image/dingtalk.png
rename to plugins/Social/Static/image/dingtalk.png
diff --git a/public/plugin/social/image/douban.png b/plugins/Social/Static/image/douban.png
similarity index 100%
rename from public/plugin/social/image/douban.png
rename to plugins/Social/Static/image/douban.png
diff --git a/public/plugin/social/image/douyin.png b/plugins/Social/Static/image/douyin.png
similarity index 100%
rename from public/plugin/social/image/douyin.png
rename to plugins/Social/Static/image/douyin.png
diff --git a/public/plugin/social/image/facebook.png b/plugins/Social/Static/image/facebook.png
similarity index 100%
rename from public/plugin/social/image/facebook.png
rename to plugins/Social/Static/image/facebook.png
diff --git a/public/plugin/social/image/feishu.png b/plugins/Social/Static/image/feishu.png
similarity index 100%
rename from public/plugin/social/image/feishu.png
rename to plugins/Social/Static/image/feishu.png
diff --git a/public/plugin/social/image/figma.png b/plugins/Social/Static/image/figma.png
similarity index 100%
rename from public/plugin/social/image/figma.png
rename to plugins/Social/Static/image/figma.png
diff --git a/public/plugin/social/image/github.png b/plugins/Social/Static/image/github.png
similarity index 100%
rename from public/plugin/social/image/github.png
rename to plugins/Social/Static/image/github.png
diff --git a/public/plugin/social/image/google.png b/plugins/Social/Static/image/google.png
similarity index 100%
rename from public/plugin/social/image/google.png
rename to plugins/Social/Static/image/google.png
diff --git a/public/plugin/social/image/line.png b/plugins/Social/Static/image/line.png
similarity index 100%
rename from public/plugin/social/image/line.png
rename to plugins/Social/Static/image/line.png
diff --git a/public/plugin/social/image/linkedin.png b/plugins/Social/Static/image/linkedin.png
similarity index 100%
rename from public/plugin/social/image/linkedin.png
rename to plugins/Social/Static/image/linkedin.png
diff --git a/public/plugin/social/image/logo.png b/plugins/Social/Static/image/logo.png
similarity index 100%
rename from public/plugin/social/image/logo.png
rename to plugins/Social/Static/image/logo.png
diff --git a/public/plugin/social/image/qq.png b/plugins/Social/Static/image/qq.png
similarity index 100%
rename from public/plugin/social/image/qq.png
rename to plugins/Social/Static/image/qq.png
diff --git a/public/plugin/social/image/wechat.png b/plugins/Social/Static/image/wechat.png
similarity index 100%
rename from public/plugin/social/image/wechat.png
rename to plugins/Social/Static/image/wechat.png
diff --git a/public/plugin/social/image/weibo.png b/plugins/Social/Static/image/weibo.png
similarity index 100%
rename from public/plugin/social/image/weibo.png
rename to plugins/Social/Static/image/weibo.png
diff --git a/plugins/Social/Views/admin/config_form.blade.php b/plugins/Social/Views/admin/config_form.blade.php
new file mode 100644
index 00000000..1108797f
--- /dev/null
+++ b/plugins/Social/Views/admin/config_form.blade.php
@@ -0,0 +1,187 @@
+@section('page-title-right')
+
+@endsection
+
+
+ {{--
+
{{ $plugin->name }}
+
+ --}}
+ {{--
--}}
+
+
+
+
+ | {{ __('Social::setting.entry_provider') }} |
+ {{ __('Social::setting.entry_status') }} |
+ {{ __('Social::setting.entry_key') }} |
+ {{ __('Social::setting.entry_secret') }} |
+ {{ __('Social::setting.entry_callback') }} |
+ {{ __('Social::setting.entry_sort_order') }} |
+ |
+
+
+
+
+ |
+
+ {providerChange(e, index)}" placeholder="{{ __('Social::setting.provider') }}">
+
+
+
+
+ |
+
+
+
+
+ |
+
+
+
+
+ |
+
+
+
+
+ |
+
+
+
+
+ |
+
+
+
+
+ |
+
+
+ |
+
+
+ |
+ |
+
+
+
+
+
+ {{ __('common.no_data') }}
+
+
+ |
+
+
+
+
+
+ {{ __('Social::setting.text_help_msg') }}
+
+ - {{ __('Social::setting.text_omni_explain') }}
+ - {{ __('Social::setting.text_omni_explain_2') }}
+ - {{ __('Social::setting.text_facebook_title') }}
+ Facebook
+
+ - {{ __('Social::setting.text_twitter_title') }}
+ Twitter
+
+ - {{ __('Social::setting.text_google_title') }}
+ Google
+
+ .......
+
+
+
+
+
+
diff --git a/plugins/Social/Views/shop/callback.blade.php b/plugins/Social/Views/shop/callback.blade.php
new file mode 100644
index 00000000..ba9b029d
--- /dev/null
+++ b/plugins/Social/Views/shop/callback.blade.php
@@ -0,0 +1,9 @@
+
diff --git a/plugins/Social/Views/shop/social_button.blade.php b/plugins/Social/Views/shop/social_button.blade.php
new file mode 100644
index 00000000..dcd250d1
--- /dev/null
+++ b/plugins/Social/Views/shop/social_button.blade.php
@@ -0,0 +1,4 @@
+
diff --git a/plugins/Social/config.json b/plugins/Social/config.json
new file mode 100644
index 00000000..f3907ed0
--- /dev/null
+++ b/plugins/Social/config.json
@@ -0,0 +1,12 @@
+{
+ "code": "social",
+ "name": "Social",
+ "description": "第三方登录(包括Facebook、Twitter、Google)",
+ "type": "social",
+ "version": "v1.0.0",
+ "icon": "/image/logo.png",
+ "author": {
+ "name": "成都光大网络科技有限公司",
+ "email": "yangjin@guangda.work"
+ }
+}
diff --git a/plugins/Stripe/Controllers/StripeController.php b/plugins/Stripe/Controllers/StripeController.php
new file mode 100644
index 00000000..fe865bcd
--- /dev/null
+++ b/plugins/Stripe/Controllers/StripeController.php
@@ -0,0 +1,54 @@
+
+ * @created 2022-08-08 15:58:36
+ * @modified 2022-08-08 15:58:36
+ */
+
+namespace Plugin\Stripe\Controllers;
+
+use Beike\Repositories\OrderPaymentRepo;
+use Beike\Repositories\OrderRepo;
+use Beike\Services\StateMachineService;
+use Beike\Shop\Http\Controllers\Controller;
+use Illuminate\Http\Request;
+use Plugin\Stripe\Services\StripePaymentService;
+
+class StripeController extends Controller
+{
+ /**
+ * 订单支付扣款
+ *
+ * @param Request $request
+ * @return array
+ * @throws \Throwable
+ */
+ public function capture(Request $request)
+ {
+ try {
+ $number = request('order_number');
+ $customer = current_customer();
+ $order = OrderRepo::getOrderByNumber($number, $customer);
+ $creditCardData = $request->all();
+
+ OrderPaymentRepo::createOrUpdatePayment($order->id, ['request' => $creditCardData]);
+ $result = (new StripePaymentService($order))->capture($creditCardData);
+ OrderPaymentRepo::createOrUpdatePayment($order->id, ['response' => $result]);
+
+ if ($result) {
+ StateMachineService::getInstance($order)->setShipment()->changeStatus(StateMachineService::PAID);
+
+ return json_success(trans('Stripe::common.capture_success'));
+ }
+
+ return json_success(trans('Stripe::common.capture_fail'));
+
+ } catch (\Exception $e) {
+ return json_fail($e->getMessage());
+ }
+ }
+}
diff --git a/plugins/Stripe/Lang/en/common.php b/plugins/Stripe/Lang/en/common.php
new file mode 100644
index 00000000..d369e986
--- /dev/null
+++ b/plugins/Stripe/Lang/en/common.php
@@ -0,0 +1,31 @@
+
+ * @created 2022-07-28 16:19:06
+ * @modified 2022-07-28 16:19:06
+ */
+
+return [
+ 'publishable_key' => 'Publishable Key',
+
+ 'title_info' => 'Card information',
+ 'cardnum' => 'Cardnum',
+ 'expiration_date' => 'Expiration Date',
+ 'year' => 'Year',
+ 'month' => 'Month',
+ 'cvv' => 'Cvv',
+ 'remenber' => 'Keep this card in mind for future use',
+ 'btn_submit' => 'Submit',
+
+ 'error_cardnum' => 'Please enter the card number',
+ 'error_cvv' => 'Please enter the security code',
+ 'error_year' => 'Please select the year',
+ 'error_month' => 'Please select a month',
+
+ 'capture_success' => 'Capture Successfully',
+ 'capture_fail' => 'Capture Failed',
+];
diff --git a/plugins/Stripe/Lang/zh_cn/common.php b/plugins/Stripe/Lang/zh_cn/common.php
new file mode 100644
index 00000000..d1fd6e14
--- /dev/null
+++ b/plugins/Stripe/Lang/zh_cn/common.php
@@ -0,0 +1,31 @@
+
+ * @created 2022-07-28 16:19:06
+ * @modified 2022-07-28 16:19:06
+ */
+
+return [
+ 'publishable_key' => '公钥',
+
+ 'title_info' => '卡信息',
+ 'cardnum' => '卡号',
+ 'expiration_date' => '截止日期',
+ 'year' => '选择年',
+ 'month' => '选择月',
+ 'cvv' => '安全码',
+ 'remenber' => '记住这张卡以便将来使用',
+ 'btn_submit' => '提交支付',
+
+ 'error_cardnum' => '请输入卡号',
+ 'error_cvv' => '请输入安全码',
+ 'error_year' => '请选择年',
+ 'error_month' => '请选择月',
+
+ 'capture_success' => '支付成功',
+ 'capture_fail' => '支付失败',
+];
diff --git a/plugins/Stripe/Routes/shop.php b/plugins/Stripe/Routes/shop.php
new file mode 100644
index 00000000..fdf208ca
--- /dev/null
+++ b/plugins/Stripe/Routes/shop.php
@@ -0,0 +1,15 @@
+
+ * @created 2022-08-04 16:17:44
+ * @modified 2022-08-04 16:17:44
+ */
+
+use Illuminate\Support\Facades\Route;
+use Plugin\Stripe\Controllers\StripeController;
+
+Route::post('/stripe/capture', [StripeController::class, 'capture'])->name('stripe_capture');
diff --git a/plugins/Stripe/Services/StripePaymentService.php b/plugins/Stripe/Services/StripePaymentService.php
new file mode 100644
index 00000000..f097009f
--- /dev/null
+++ b/plugins/Stripe/Services/StripePaymentService.php
@@ -0,0 +1,74 @@
+
+ * @created 2022-08-08 16:09:21
+ * @modified 2022-08-08 16:09:21
+ */
+
+namespace Plugin\Stripe\Services;
+
+use Beike\Shop\Services\PaymentService;
+use Stripe\Exception\ApiErrorException;
+use Stripe\Stripe;
+
+class StripePaymentService extends PaymentService
+{
+ // 零位十进制货币 https://stripe.com/docs/currencies#special-cases
+ public const ZERO_DECIMAL = [
+ 'BIF', 'CLP', 'DJF', 'GNF', 'JPY', 'KMF', 'KRW', 'MGA',
+ 'PYG', 'RWF', 'UGX', 'VND', 'VUV', 'XAF', 'XOF', 'XPF',
+ ];
+
+ /**
+ * @throws ApiErrorException
+ * @throws \Exception
+ */
+ public function capture($creditCardData): bool
+ {
+ $apiKey = plugin_setting('stripe.secret_key');
+ Stripe::setApiKey($apiKey);
+ $tokenId = $creditCardData['token'] ?? '';
+ if (empty($tokenId)) {
+ throw new \Exception('Invalid token');
+ }
+ $currency = $this->order->currency_code;
+
+ if (! in_array($currency, self::ZERO_DECIMAL)) {
+ $total = round($this->order->total, 2) * 100;
+ } else {
+ $total = floor($this->order->total);
+ }
+
+ $stripeChargeParameters = [
+ 'amount' => $total,
+ 'currency' => $currency,
+ 'metadata' => [
+ 'orderId' => $this->order->id,
+ ],
+ 'source' => $tokenId,
+ // 'customer' => $this->createCustomer(),
+ ];
+
+ $charge = \Stripe\Charge::create($stripeChargeParameters);
+
+ return $charge['paid'] && $charge['captured'];
+ }
+
+ /**
+ * 创建 stripe customer
+ * @return mixed
+ * @throws ApiErrorException
+ */
+ private function createCustomer(): mixed
+ {
+ $customer = \Stripe\Customer::create([
+ 'email' => $this->order->email,
+ ]);
+
+ return $customer['id'];
+ }
+}
diff --git a/public/plugin/stripe/css/demo.css b/plugins/Stripe/Static/css/demo.css
similarity index 86%
rename from public/plugin/stripe/css/demo.css
rename to plugins/Stripe/Static/css/demo.css
index 30fdbc42..04cd16f6 100644
--- a/public/plugin/stripe/css/demo.css
+++ b/plugins/Stripe/Static/css/demo.css
@@ -4,5 +4,5 @@
*/
#bk-stripe-app .form-wrap {
- max-width: 400px;
+ /* max-width: 500px; */
}
\ No newline at end of file
diff --git a/public/plugin/stripe/image/logo.png b/plugins/Stripe/Static/image/logo.png
similarity index 100%
rename from public/plugin/stripe/image/logo.png
rename to plugins/Stripe/Static/image/logo.png
diff --git a/public/plugin/stripe/image/pay-1.png b/plugins/Stripe/Static/image/pay-1.png
similarity index 100%
rename from public/plugin/stripe/image/pay-1.png
rename to plugins/Stripe/Static/image/pay-1.png
diff --git a/public/plugin/stripe/image/pay-2.png b/plugins/Stripe/Static/image/pay-2.png
similarity index 100%
rename from public/plugin/stripe/image/pay-2.png
rename to plugins/Stripe/Static/image/pay-2.png
diff --git a/public/plugin/stripe/image/pay-3.png b/plugins/Stripe/Static/image/pay-3.png
similarity index 100%
rename from public/plugin/stripe/image/pay-3.png
rename to plugins/Stripe/Static/image/pay-3.png
diff --git a/public/plugin/stripe/image/pay-4.png b/plugins/Stripe/Static/image/pay-4.png
similarity index 100%
rename from public/plugin/stripe/image/pay-4.png
rename to plugins/Stripe/Static/image/pay-4.png
diff --git a/public/plugin/stripe/image/pay-5.png b/plugins/Stripe/Static/image/pay-5.png
similarity index 100%
rename from public/plugin/stripe/image/pay-5.png
rename to plugins/Stripe/Static/image/pay-5.png
diff --git a/public/plugin/stripe/image/pay-image.png b/plugins/Stripe/Static/image/pay-image.png
similarity index 100%
rename from public/plugin/stripe/image/pay-image.png
rename to plugins/Stripe/Static/image/pay-image.png
diff --git a/public/plugin/stripe/js/demo.js b/plugins/Stripe/Static/js/demo.js
similarity index 100%
rename from public/plugin/stripe/js/demo.js
rename to plugins/Stripe/Static/js/demo.js
diff --git a/plugins/Stripe/Views/checkout/payment.blade.php b/plugins/Stripe/Views/checkout/payment.blade.php
new file mode 100644
index 00000000..9a405686
--- /dev/null
+++ b/plugins/Stripe/Views/checkout/payment.blade.php
@@ -0,0 +1,170 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/plugins/Stripe/columns.php b/plugins/Stripe/columns.php
new file mode 100644
index 00000000..ec130ee0
--- /dev/null
+++ b/plugins/Stripe/columns.php
@@ -0,0 +1,40 @@
+
+ * @created 2022-06-29 21:16:23
+ * @modified 2022-06-29 21:16:23
+ */
+
+return [
+ [
+ 'name' => 'publishable_key',
+ 'label_key' => 'common.publishable_key',
+ 'type' => 'string',
+ 'required' => true,
+ 'rules' => 'required|min:32',
+ 'description' => '公钥(Publishable key)',
+ ],
+ [
+ 'name' => 'secret_key',
+ 'label' => '密钥',
+ 'type' => 'string',
+ 'required' => true,
+ 'rules' => 'required|min:32',
+ 'description' => '密钥(Secret key)',
+ ],
+ [
+ 'name' => 'test_mode',
+ 'label' => '测试模式',
+ 'type' => 'select',
+ 'options' => [
+ ['value' => '1', 'label' => '开启'],
+ ['value' => '0', 'label' => '关闭'],
+ ],
+ 'required' => true,
+ 'description' => '如开启测试模式请填写测试公钥和密钥, 关闭测试模式则填写正式公钥和密钥',
+ ],
+];
diff --git a/plugins/Stripe/config.json b/plugins/Stripe/config.json
new file mode 100644
index 00000000..c87af7c3
--- /dev/null
+++ b/plugins/Stripe/config.json
@@ -0,0 +1,12 @@
+{
+ "code": "stripe",
+ "name": "Stripe 支付",
+ "description": "Stripe 支付 Stripe",
+ "type": "payment",
+ "version": "v1.0.0",
+ "icon": "/image/logo.png",
+ "author": {
+ "name": "成都光大网络科技有限公司",
+ "email": "yangjin@guangda.work"
+ }
+}
diff --git a/public/plugin/.gitignore b/public/plugin/.gitignore
new file mode 100644
index 00000000..d6b7ef32
--- /dev/null
+++ b/public/plugin/.gitignore
@@ -0,0 +1,2 @@
+*
+!.gitignore
diff --git a/public/plugin/alipay/image/logo.png b/public/plugin/alipay/image/logo.png
deleted file mode 100644
index 4c21b8dc..00000000
Binary files a/public/plugin/alipay/image/logo.png and /dev/null differ
diff --git a/public/plugin/paypal/image/logo.png b/public/plugin/paypal/image/logo.png
deleted file mode 100644
index e078b425..00000000
Binary files a/public/plugin/paypal/image/logo.png and /dev/null differ