Compare commits
3
Commits
ee39010d5e
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5abb8277e3 | ||
|
|
26e42dd2a4 | ||
|
|
dfd308873a |
@@ -26,6 +26,7 @@ class GoodsGroupController extends AdminController
|
|||||||
$grid->column('id')->sortable();
|
$grid->column('id')->sortable();
|
||||||
$grid->column('gp_name')->editable();
|
$grid->column('gp_name')->editable();
|
||||||
$grid->column('is_open')->switch();
|
$grid->column('is_open')->switch();
|
||||||
|
$grid->column('is_open_group_pwd')->switch();
|
||||||
$grid->column('ord')->editable();
|
$grid->column('ord')->editable();
|
||||||
$grid->column('created_at');
|
$grid->column('created_at');
|
||||||
$grid->column('updated_at')->sortable();
|
$grid->column('updated_at')->sortable();
|
||||||
@@ -66,6 +67,16 @@ class GoodsGroupController extends AdminController
|
|||||||
return admin_trans('dujiaoka.status_close');
|
return admin_trans('dujiaoka.status_close');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
$show->field('is_open_group_pwd')->as(function ($isOpenGroupPwd) {
|
||||||
|
if ($isOpenGroupPwd == GoodsGroupModel::STATUS_OPEN) {
|
||||||
|
return admin_trans('dujiaoka.status_open');
|
||||||
|
} else {
|
||||||
|
return admin_trans('dujiaoka.status_close');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$show->field('group_pwd')->as(function ($groupPwd) {
|
||||||
|
return empty($groupPwd) ? '' : '******';
|
||||||
|
});
|
||||||
$show->field('ord');
|
$show->field('ord');
|
||||||
$show->field('created_at');
|
$show->field('created_at');
|
||||||
$show->field('updated_at');
|
$show->field('updated_at');
|
||||||
@@ -81,11 +92,26 @@ class GoodsGroupController extends AdminController
|
|||||||
{
|
{
|
||||||
return Form::make(new GoodsGroup(), function (Form $form) {
|
return Form::make(new GoodsGroup(), function (Form $form) {
|
||||||
$form->display('id');
|
$form->display('id');
|
||||||
$form->text('gp_name');
|
$form->text('gp_name', admin_trans('goods-group.fields.gp_name'));
|
||||||
$form->switch('is_open')->default(GoodsGroupModel::STATUS_OPEN);
|
$form->switch('is_open', admin_trans('goods-group.fields.is_open'))->default(GoodsGroupModel::STATUS_OPEN);
|
||||||
$form->number('ord')->default(1)->help(admin_trans('dujiaoka.ord'));
|
|
||||||
|
$form->divider('分类密码访问');
|
||||||
|
$form->switch('is_open_group_pwd', admin_trans('goods-group.fields.is_open_group_pwd'))->default(GoodsGroupModel::STATUS_CLOSE);
|
||||||
|
$form->text('group_pwd', admin_trans('goods-group.fields.group_pwd'))
|
||||||
|
->help(admin_trans('goods-group.fields.group_pwd_help'));
|
||||||
|
|
||||||
|
$form->number('ord', admin_trans('goods-group.fields.ord'))->default(1)->help(admin_trans('dujiaoka.ord'));
|
||||||
$form->display('created_at');
|
$form->display('created_at');
|
||||||
$form->display('updated_at');
|
$form->display('updated_at');
|
||||||
|
|
||||||
|
$form->saving(function (Form $form) {
|
||||||
|
if ((int) $form->is_open_group_pwd !== GoodsGroupModel::STATUS_OPEN) {
|
||||||
|
$form->group_pwd = null;
|
||||||
|
} elseif ($form->isEditing() && $form->group_pwd === null) {
|
||||||
|
$form->deleteInput('group_pwd');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
$form->disableViewButton();
|
$form->disableViewButton();
|
||||||
$form->footer(function ($footer) {
|
$form->footer(function ($footer) {
|
||||||
// 去掉`查看`checkbox
|
// 去掉`查看`checkbox
|
||||||
|
|||||||
@@ -46,6 +46,9 @@ class HomeController extends BaseController
|
|||||||
*/
|
*/
|
||||||
public function index(Request $request)
|
public function index(Request $request)
|
||||||
{
|
{
|
||||||
|
// 每次打开/刷新首页都重新要求输入分类访问密码
|
||||||
|
session()->forget('dujiaoka_group_pwd_access');
|
||||||
|
|
||||||
$goods = $this->goodsService->withGroup();
|
$goods = $this->goodsService->withGroup();
|
||||||
return $this->render('static_pages/home', ['data' => $goods], __('dujiaoka.page-title.home'));
|
return $this->render('static_pages/home', ['data' => $goods], __('dujiaoka.page-title.home'));
|
||||||
}
|
}
|
||||||
@@ -65,6 +68,7 @@ class HomeController extends BaseController
|
|||||||
try {
|
try {
|
||||||
$goods = $this->goodsService->detail($id);
|
$goods = $this->goodsService->detail($id);
|
||||||
$this->goodsService->validatorGoodsStatus($goods);
|
$this->goodsService->validatorGoodsStatus($goods);
|
||||||
|
$this->goodsService->validatorGoodsGroupAccess($goods);
|
||||||
// 有没有优惠码可以展示
|
// 有没有优惠码可以展示
|
||||||
if (count($goods->coupon)) {
|
if (count($goods->coupon)) {
|
||||||
$goods->open_coupon = 1;
|
$goods->open_coupon = 1;
|
||||||
@@ -83,6 +87,47 @@ class HomeController extends BaseController
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证商品分类访问密码
|
||||||
|
*
|
||||||
|
* @param Request $request
|
||||||
|
* @return \Illuminate\Http\JsonResponse
|
||||||
|
*/
|
||||||
|
public function verifyGroupPassword(Request $request)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$request->validate([
|
||||||
|
'group_id' => 'required|integer',
|
||||||
|
'password' => 'required|string',
|
||||||
|
]);
|
||||||
|
$group = $this->goodsService->verifyGroupPassword((int) $request->input('group_id'), (string) $request->input('password'));
|
||||||
|
return response()->json([
|
||||||
|
'code' => 200,
|
||||||
|
'msg' => __('dujiaoka.prompt.goods_group_password_success'),
|
||||||
|
'data' => $group,
|
||||||
|
]);
|
||||||
|
} catch (RuleValidationException $ruleValidationException) {
|
||||||
|
return response()->json(['code' => 400, 'msg' => $ruleValidationException->getMessage()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消商品分类密码访问授权
|
||||||
|
*
|
||||||
|
* @param Request $request
|
||||||
|
* @return \Illuminate\Http\JsonResponse
|
||||||
|
*/
|
||||||
|
public function forgetGroupPasswordAccess(Request $request)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'group_id' => 'required|integer',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->goodsService->forgetGroupAccess((int) $request->input('group_id'));
|
||||||
|
|
||||||
|
return response()->json(['code' => 200, 'msg' => 'ok']);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 极验行为验证
|
* 极验行为验证
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -2,123 +2,153 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers\Pay;
|
namespace App\Http\Controllers\Pay;
|
||||||
|
|
||||||
|
|
||||||
use AmrShawky\LaravelCurrency\Facade\Currency;
|
use AmrShawky\LaravelCurrency\Facade\Currency;
|
||||||
use App\Exceptions\RuleValidationException;
|
use App\Exceptions\RuleValidationException;
|
||||||
use App\Http\Controllers\PayController;
|
use App\Http\Controllers\PayController;
|
||||||
|
use GuzzleHttp\Client;
|
||||||
|
use GuzzleHttp\Exception\RequestException;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
use PayPal\Api\Amount;
|
|
||||||
use PayPal\Api\Details;
|
|
||||||
use PayPal\Api\Item;
|
|
||||||
use PayPal\Api\ItemList;
|
|
||||||
use PayPal\Api\Payer;
|
|
||||||
use PayPal\Api\Payment;
|
|
||||||
use PayPal\Api\PaymentExecution;
|
|
||||||
use PayPal\Api\RedirectUrls;
|
|
||||||
use PayPal\Api\Transaction;
|
|
||||||
use PayPal\Auth\OAuthTokenCredential;
|
|
||||||
use PayPal\Exception\PayPalConnectionException;
|
|
||||||
use PayPal\Rest\ApiContext;
|
|
||||||
|
|
||||||
class PaypalPayController extends PayController
|
class PaypalPayController extends PayController
|
||||||
{
|
{
|
||||||
|
|
||||||
const Currency = 'USD'; //货币单位
|
const Currency = 'USD'; //货币单位
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PayPal API 地址
|
||||||
|
*
|
||||||
|
* v2 Checkout Orders:
|
||||||
|
* - 创建订单: POST https://api-m.paypal.com/v2/checkout/orders
|
||||||
|
* - 捕获订单: POST https://api-m.paypal.com/v2/checkout/orders/{order_id}/capture
|
||||||
|
*/
|
||||||
|
const PAYPAL_API_BASE = 'https://api-m.paypal.com';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PayPal 沙盒 API 地址
|
||||||
|
*/
|
||||||
|
const PAYPAL_SANDBOX_API_BASE = 'https://api-m.sandbox.paypal.com';
|
||||||
|
|
||||||
public function gateway(string $payway, string $orderSN)
|
public function gateway(string $payway, string $orderSN)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
// 加载网关
|
// 加载网关
|
||||||
$this->loadGateWay($orderSN, $payway);
|
$this->loadGateWay($orderSN, $payway);
|
||||||
$paypal = new ApiContext(
|
|
||||||
new OAuthTokenCredential(
|
|
||||||
$this->payGateway->merchant_key,
|
|
||||||
$this->payGateway->merchant_pem
|
|
||||||
)
|
|
||||||
);
|
|
||||||
$paypal->setConfig(['mode' => 'live']);
|
|
||||||
$product = $this->order->title;
|
|
||||||
// 得到汇率
|
// 得到汇率
|
||||||
$total = Currency::convert()
|
$total = Currency::convert()
|
||||||
->from('CNY')
|
->from('CNY')
|
||||||
->to('USD')
|
->to(self::Currency)
|
||||||
->amount($this->order->actual_price)
|
->amount($this->order->actual_price)
|
||||||
->round(2)
|
->round(2)
|
||||||
->get();
|
->get();
|
||||||
$shipping = 0;
|
|
||||||
$description = $this->order->title;
|
$total = $this->formatPaypalAmount($total, $this->order->actual_price);
|
||||||
$payer = new Payer();
|
$paypalConfig = $this->resolvePaypalConfig($this->payGateway);
|
||||||
$payer->setPaymentMethod('paypal');
|
$accessToken = $this->getAccessToken($paypalConfig['client_id'], $paypalConfig['client_secret'], $paypalConfig['api_base']);
|
||||||
$item = new Item();
|
|
||||||
$item->setName($product)->setCurrency(self::Currency)->setQuantity(1)->setPrice($total);
|
$paypalOrder = $this->createPaypalOrder($accessToken, $paypalConfig['api_base'], [
|
||||||
$itemList = new ItemList();
|
'intent' => 'CAPTURE',
|
||||||
$itemList->setItems([$item]);
|
'purchase_units' => [
|
||||||
$details = new Details();
|
[
|
||||||
$details->setShipping($shipping)->setSubtotal($total);
|
'reference_id' => $this->order->order_sn,
|
||||||
$amount = new Amount();
|
'description' => $this->order->title,
|
||||||
$amount->setCurrency(self::Currency)->setTotal($total)->setDetails($details);
|
'invoice_id' => $this->order->order_sn,
|
||||||
$transaction = new Transaction();
|
'custom_id' => $this->order->order_sn,
|
||||||
$transaction->setAmount($amount)->setItemList($itemList)->setDescription($description)->setInvoiceNumber($this->order->order_sn);
|
'amount' => [
|
||||||
$redirectUrls = new RedirectUrls();
|
'currency_code' => self::Currency,
|
||||||
$redirectUrls->setReturnUrl(route('paypal-return', ['success' => 'ok', 'orderSN' => $this->order->order_sn]))->setCancelUrl(route('paypal-return', ['success' => 'no', 'orderSN' => $this->order->order_sn]));
|
'value' => $total,
|
||||||
$payment = new Payment();
|
'breakdown' => [
|
||||||
$payment->setIntent('sale')->setPayer($payer)->setRedirectUrls($redirectUrls)->setTransactions([$transaction]);
|
'item_total' => [
|
||||||
$payment->create($paypal);
|
'currency_code' => self::Currency,
|
||||||
$approvalUrl = $payment->getApprovalLink();
|
'value' => $total,
|
||||||
return redirect($approvalUrl);
|
],
|
||||||
} catch (PayPalConnectionException $payPalConnectionException) {
|
],
|
||||||
return $this->err($payPalConnectionException->getMessage());
|
],
|
||||||
|
'items' => [
|
||||||
|
[
|
||||||
|
'name' => mb_substr($this->order->title, 0, 127),
|
||||||
|
'unit_amount' => [
|
||||||
|
'currency_code' => self::Currency,
|
||||||
|
'value' => $total,
|
||||||
|
],
|
||||||
|
'quantity' => '1',
|
||||||
|
'category' => 'DIGITAL_GOODS',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'application_context' => [
|
||||||
|
'brand_name' => config('app.name', 'dujiaoka'),
|
||||||
|
'shipping_preference' => 'NO_SHIPPING',
|
||||||
|
'user_action' => 'PAY_NOW',
|
||||||
|
'return_url' => route('paypal-return', ['success' => 'ok', 'orderSN' => $this->order->order_sn]),
|
||||||
|
'cancel_url' => route('paypal-return', ['success' => 'no', 'orderSN' => $this->order->order_sn]),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
foreach ($paypalOrder['links'] ?? [] as $link) {
|
||||||
|
if (($link['rel'] ?? '') === 'approve' && !empty($link['href'])) {
|
||||||
|
return redirect($link['href']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::error('paypal创建订单失败', ['response' => $paypalOrder]);
|
||||||
|
return $this->err('PayPal 创建订单失败:未获取到支付跳转链接');
|
||||||
} catch (RuleValidationException $exception) {
|
} catch (RuleValidationException $exception) {
|
||||||
return $this->err($exception->getMessage());
|
return $this->err($exception->getMessage());
|
||||||
|
} catch (\Exception $exception) {
|
||||||
|
Log::error('paypal创建订单异常', ['message' => $exception->getMessage()]);
|
||||||
|
return $this->err($exception->getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*paypal 同步回调
|
* paypal 同步回调
|
||||||
*/
|
*/
|
||||||
public function returnUrl(Request $request)
|
public function returnUrl(Request $request)
|
||||||
{
|
{
|
||||||
$success = $request->input('success');
|
$success = $request->input('success');
|
||||||
$paymentId = $request->input('paymentId');
|
$paypalOrderId = $request->input('token');
|
||||||
$payerID = $request->input('PayerID');
|
|
||||||
$orderSN = $request->input('orderSN');
|
$orderSN = $request->input('orderSN');
|
||||||
if ($success == 'no' || empty($paymentId) || empty($payerID)) {
|
|
||||||
|
if ($success == 'no' || empty($paypalOrderId)) {
|
||||||
// 取消支付
|
// 取消支付
|
||||||
redirect(url('detail-order-sn', ['orderSN' => $payerID]));
|
return redirect(url('detail-order-sn', ['orderSN' => $orderSN]));
|
||||||
}
|
}
|
||||||
|
|
||||||
$order = $this->orderService->detailOrderSN($orderSN);
|
$order = $this->orderService->detailOrderSN($orderSN);
|
||||||
if (!$order) {
|
if (!$order) {
|
||||||
return 'error';
|
return 'error';
|
||||||
}
|
}
|
||||||
|
|
||||||
$payGateway = $this->payService->detail($order->pay_id);
|
$payGateway = $this->payService->detail($order->pay_id);
|
||||||
if (!$payGateway) {
|
if (!$payGateway) {
|
||||||
return 'error';
|
return 'error';
|
||||||
}
|
}
|
||||||
if($payGateway->pay_handleroute != '/pay/paypal'){
|
|
||||||
|
if ($payGateway->pay_handleroute != '/pay/paypal') {
|
||||||
return 'error';
|
return 'error';
|
||||||
}
|
}
|
||||||
$paypal = new ApiContext(
|
|
||||||
new OAuthTokenCredential(
|
|
||||||
$payGateway->merchant_key,
|
|
||||||
$payGateway->merchant_pem
|
|
||||||
)
|
|
||||||
);
|
|
||||||
$paypal->setConfig(['mode' => 'live']);
|
|
||||||
$payment = Payment::get($paymentId, $paypal);
|
|
||||||
$execute = new PaymentExecution();
|
|
||||||
$execute->setPayerId($payerID);
|
|
||||||
try {
|
try {
|
||||||
$payment->execute($execute, $paypal);
|
$paypalConfig = $this->resolvePaypalConfig($payGateway);
|
||||||
$this->orderProcessService->completedOrder($orderSN, $order->actual_price, $paymentId);
|
$accessToken = $this->getAccessToken($paypalConfig['client_id'], $paypalConfig['client_secret'], $paypalConfig['api_base']);
|
||||||
Log::info("paypal支付成功", ['支付成功,支付ID【' . $paymentId . '】,支付人ID【' . $payerID . '】']);
|
$capture = $this->capturePaypalOrder($accessToken, $paypalConfig['api_base'], $paypalOrderId);
|
||||||
|
|
||||||
|
if (($capture['status'] ?? '') === 'COMPLETED') {
|
||||||
|
$captureId = $capture['purchase_units'][0]['payments']['captures'][0]['id'] ?? $paypalOrderId;
|
||||||
|
$this->orderProcessService->completedOrder($orderSN, $order->actual_price, $captureId);
|
||||||
|
Log::info('paypal支付成功', ['订单号' => $orderSN, 'PayPal订单ID' => $paypalOrderId, '捕获ID' => $captureId]);
|
||||||
|
} else {
|
||||||
|
Log::error('paypal支付未完成', ['订单号' => $orderSN, 'PayPal订单ID' => $paypalOrderId, 'response' => $capture]);
|
||||||
|
}
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
Log::error("paypal支付失败", ['支付失败,支付ID【' . $paymentId . '】,支付人ID【' . $payerID . '】']);
|
Log::error('paypal支付失败', ['订单号' => $orderSN, 'PayPal订单ID' => $paypalOrderId, '错误' => $e->getMessage()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return redirect(url('detail-order-sn', ['orderSN' => $orderSN]));
|
return redirect(url('detail-order-sn', ['orderSN' => $orderSN]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 异步通知
|
* 异步通知
|
||||||
* TODO: 暂未实现,但是好像只实现同步回调即可。这个可以放在后面实现
|
* TODO: 暂未实现,但是好像只实现同步回调即可。这个可以放在后面实现
|
||||||
@@ -127,12 +157,145 @@ class PaypalPayController extends PayController
|
|||||||
{
|
{
|
||||||
//获取回调结果
|
//获取回调结果
|
||||||
$json_data = $this->get_JsonData();
|
$json_data = $this->get_JsonData();
|
||||||
if(!empty($json_data)){
|
if (!empty($json_data)) {
|
||||||
Log::debug("paypal notify info:\r\n" . json_encode($json_data));
|
Log::debug("paypal notify info:\r\n" . json_encode($json_data));
|
||||||
}else{
|
} else {
|
||||||
Log::debug("paypal notify fail:参加为空");
|
Log::debug("paypal notify fail:参加为空");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化 PayPal 金额
|
||||||
|
*
|
||||||
|
* PayPal v2 Orders API 要求金额必须大于 0,且最多保留两位小数。
|
||||||
|
* 如果 CNY 转 USD 后因为订单金额过小或汇率服务异常得到 0.00,则使用 PayPal 最小可支付金额 0.01。
|
||||||
|
*/
|
||||||
|
private function formatPaypalAmount($convertedAmount, $originalAmount): string
|
||||||
|
{
|
||||||
|
$amount = round((float)$convertedAmount, 2);
|
||||||
|
|
||||||
|
if ($amount <= 0) {
|
||||||
|
Log::warning('paypal订单金额转换后小于等于0,已使用最小金额0.01', [
|
||||||
|
'original_amount_cny' => $originalAmount,
|
||||||
|
'converted_amount_usd' => $convertedAmount,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$amount = 0.01;
|
||||||
|
}
|
||||||
|
|
||||||
|
return number_format($amount, 2, '.', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 PayPal OAuth2 Access Token
|
||||||
|
*/
|
||||||
|
private function getAccessToken(string $clientId, string $clientSecret, string $apiBase): string
|
||||||
|
{
|
||||||
|
$response = $this->paypalClient($apiBase)->post('/v1/oauth2/token', [
|
||||||
|
'auth' => [$clientId, $clientSecret],
|
||||||
|
'form_params' => [
|
||||||
|
'grant_type' => 'client_credentials',
|
||||||
|
],
|
||||||
|
'headers' => [
|
||||||
|
'Accept' => 'application/json',
|
||||||
|
'Accept-Language' => 'en_US',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$data = json_decode((string)$response->getBody(), true);
|
||||||
|
|
||||||
|
if (empty($data['access_token'])) {
|
||||||
|
throw new \RuntimeException('PayPal access_token 获取失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $data['access_token'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建 PayPal v2 Checkout Order
|
||||||
|
*/
|
||||||
|
private function createPaypalOrder(string $accessToken, string $apiBase, array $payload): array
|
||||||
|
{
|
||||||
|
return $this->paypalRequest('POST', '/v2/checkout/orders', $accessToken, $apiBase, $payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 捕获 PayPal v2 Checkout Order
|
||||||
|
*/
|
||||||
|
private function capturePaypalOrder(string $accessToken, string $apiBase, string $paypalOrderId): array
|
||||||
|
{
|
||||||
|
return $this->paypalRequest('POST', '/v2/checkout/orders/' . urlencode($paypalOrderId) . '/capture', $accessToken, $apiBase);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 请求 PayPal API
|
||||||
|
*/
|
||||||
|
private function paypalRequest(string $method, string $uri, string $accessToken, string $apiBase, array $payload = null): array
|
||||||
|
{
|
||||||
|
$options = [
|
||||||
|
'headers' => [
|
||||||
|
'Authorization' => 'Bearer ' . $accessToken,
|
||||||
|
'Content-Type' => 'application/json',
|
||||||
|
'Accept' => 'application/json',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($payload !== null) {
|
||||||
|
$options['json'] = $payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$response = $this->paypalClient($apiBase)->request($method, $uri, $options);
|
||||||
|
return json_decode((string)$response->getBody(), true) ?: [];
|
||||||
|
} catch (RequestException $exception) {
|
||||||
|
$responseBody = $exception->hasResponse() ? (string)$exception->getResponse()->getBody() : '';
|
||||||
|
Log::error('paypal api request error', [
|
||||||
|
'method' => $method,
|
||||||
|
'uri' => $uri,
|
||||||
|
'status' => $exception->hasResponse() ? $exception->getResponse()->getStatusCode() : null,
|
||||||
|
'response' => $responseBody,
|
||||||
|
]);
|
||||||
|
|
||||||
|
throw new \RuntimeException($responseBody ?: $exception->getMessage(), $exception->getCode(), $exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析 PayPal 配置
|
||||||
|
*
|
||||||
|
* 后台支付配置建议:
|
||||||
|
* - 商户号 merchant_id:PayPal Client ID
|
||||||
|
* - 商户密钥 merchant_pem:PayPal Secret
|
||||||
|
* - 商户 KEY merchant_key:可选,填 sandbox/test 则使用沙盒;填 live/prod 或留空则使用生产
|
||||||
|
*
|
||||||
|
* 兼容旧配置:如果 merchant_id 为空,则继续使用 merchant_key 作为 Client ID。
|
||||||
|
*/
|
||||||
|
private function resolvePaypalConfig($payGateway): array
|
||||||
|
{
|
||||||
|
$merchantId = trim((string)$payGateway->merchant_id);
|
||||||
|
$merchantKey = trim((string)$payGateway->merchant_key);
|
||||||
|
$merchantPem = trim((string)$payGateway->merchant_pem);
|
||||||
|
|
||||||
|
$mode = strtolower($merchantKey);
|
||||||
|
$apiBase = in_array($mode, ['sandbox', 'test'], true) ? self::PAYPAL_SANDBOX_API_BASE : self::PAYPAL_API_BASE;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'client_id' => $merchantId ?: $merchantKey,
|
||||||
|
'client_secret' => $merchantPem,
|
||||||
|
'api_base' => $apiBase,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PayPal HTTP Client
|
||||||
|
*/
|
||||||
|
private function paypalClient(string $apiBase): Client
|
||||||
|
{
|
||||||
|
return new Client([
|
||||||
|
'base_uri' => $apiBase,
|
||||||
|
'timeout' => 30,
|
||||||
|
'http_errors' => true,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function get_JsonData()
|
private function get_JsonData()
|
||||||
@@ -140,9 +303,8 @@ class PaypalPayController extends PayController
|
|||||||
$json = file_get_contents('php://input');
|
$json = file_get_contents('php://input');
|
||||||
if ($json) {
|
if ($json) {
|
||||||
$json = str_replace("'", '', $json);
|
$json = str_replace("'", '', $json);
|
||||||
$json = json_decode($json,true);
|
$json = json_decode($json, true);
|
||||||
}
|
}
|
||||||
return $json;
|
return $json;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ class GoodsGroup extends BaseModel
|
|||||||
'deleted' => GoodsGroupDeleted::class
|
'deleted' => GoodsGroupDeleted::class
|
||||||
];
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'is_open_group_pwd' => 'integer',
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 关联商品
|
* 关联商品
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ class GoodsService
|
|||||||
public function withGroup(): ?array
|
public function withGroup(): ?array
|
||||||
{
|
{
|
||||||
$goods = GoodsGroup::query()
|
$goods = GoodsGroup::query()
|
||||||
|
->select(['id', 'gp_name', 'is_open', 'is_open_group_pwd', 'group_pwd', 'ord', 'created_at', 'updated_at', 'deleted_at'])
|
||||||
->with(['goods' => function($query) {
|
->with(['goods' => function($query) {
|
||||||
$query->withCount(['carmis' => function($query) {
|
$query->withCount(['carmis' => function($query) {
|
||||||
$query->where('status', Carmis::STATUS_UNSOLD);
|
$query->where('status', Carmis::STATUS_UNSOLD);
|
||||||
@@ -48,6 +49,15 @@ class GoodsService
|
|||||||
->where('is_open', GoodsGroup::STATUS_OPEN)
|
->where('is_open', GoodsGroup::STATUS_OPEN)
|
||||||
->orderBy('ord', 'DESC')
|
->orderBy('ord', 'DESC')
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
|
$goods->each(function (GoodsGroup $group) {
|
||||||
|
$isLocked = $this->isGroupPasswordProtected($group) && !$this->hasGroupAccess($group->id);
|
||||||
|
$group->setAttribute('is_group_locked', $isLocked ? GoodsGroup::STATUS_OPEN : GoodsGroup::STATUS_CLOSE);
|
||||||
|
if ($isLocked) {
|
||||||
|
$group->setRelation('goods', collect());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// 将自动
|
// 将自动
|
||||||
return $goods ? $goods->toArray() : null;
|
return $goods ? $goods->toArray() : null;
|
||||||
}
|
}
|
||||||
@@ -65,13 +75,136 @@ class GoodsService
|
|||||||
public function detail(int $id)
|
public function detail(int $id)
|
||||||
{
|
{
|
||||||
$goods = Goods::query()
|
$goods = Goods::query()
|
||||||
->with(['coupon'])
|
->with(['coupon', 'group'])
|
||||||
->withCount(['carmis' => function($query) {
|
->withCount(['carmis' => function($query) {
|
||||||
$query->where('status', Carmis::STATUS_UNSOLD);
|
$query->where('status', Carmis::STATUS_UNSOLD);
|
||||||
}])->where('id', $id)->first();
|
}])->where('id', $id)->first();
|
||||||
return $goods;
|
return $goods;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分类是否开启密码访问
|
||||||
|
*
|
||||||
|
* @param GoodsGroup|null $group
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isGroupPasswordProtected(?GoodsGroup $group): bool
|
||||||
|
{
|
||||||
|
return !empty($group)
|
||||||
|
&& $group->is_open_group_pwd == GoodsGroup::STATUS_OPEN
|
||||||
|
&& !empty($group->group_pwd);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前会话是否已解锁分类
|
||||||
|
*
|
||||||
|
* @param int $groupID 分类id
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function hasGroupAccess(int $groupID): bool
|
||||||
|
{
|
||||||
|
$groupIDs = session('dujiaoka_group_pwd_access', []);
|
||||||
|
return in_array($groupID, $groupIDs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证分类访问密码
|
||||||
|
*
|
||||||
|
* @param int $groupID 分类id
|
||||||
|
* @param string $password 访问密码
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function verifyGroupPassword(int $groupID, string $password): array
|
||||||
|
{
|
||||||
|
$group = GoodsGroup::query()
|
||||||
|
->with(['goods' => function($query) {
|
||||||
|
$query->withCount(['carmis' => function($query) {
|
||||||
|
$query->where('status', Carmis::STATUS_UNSOLD);
|
||||||
|
}])->where('is_open', Goods::STATUS_OPEN)->orderBy('ord', 'DESC');
|
||||||
|
}])
|
||||||
|
->where('is_open', GoodsGroup::STATUS_OPEN)
|
||||||
|
->where('id', $groupID)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (empty($group)) {
|
||||||
|
throw new RuleValidationException(__('dujiaoka.prompt.goods_group_does_not_exist'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->isGroupPasswordProtected($group)) {
|
||||||
|
$this->grantGroupAccess($group->id);
|
||||||
|
return $this->formatGroupForResponse($group);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hash_equals((string) $group->group_pwd, (string) $password)) {
|
||||||
|
throw new RuleValidationException(__('dujiaoka.prompt.goods_group_password_error'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->grantGroupAccess($group->id);
|
||||||
|
return $this->formatGroupForResponse($group);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化分类接口返回数据
|
||||||
|
*
|
||||||
|
* @param GoodsGroup $group 分类模型
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private function formatGroupForResponse(GoodsGroup $group): array
|
||||||
|
{
|
||||||
|
$data = $group->toArray();
|
||||||
|
foreach ($data['goods'] as &$goods) {
|
||||||
|
$goods['picture_url'] = picture_ulr($goods['picture']);
|
||||||
|
}
|
||||||
|
unset($goods);
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证商品所属分类访问权限
|
||||||
|
*
|
||||||
|
* @param Goods $goods 商品模型
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function validatorGoodsGroupAccess(Goods $goods): void
|
||||||
|
{
|
||||||
|
$group = $goods->group;
|
||||||
|
if ($this->isGroupPasswordProtected($group) && !$this->hasGroupAccess($group->id)) {
|
||||||
|
throw new RuleValidationException(__('dujiaoka.prompt.goods_group_password_required'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消当前会话访问指定分类的授权
|
||||||
|
*
|
||||||
|
* @param int $groupID 分类id
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function forgetGroupAccess(int $groupID): void
|
||||||
|
{
|
||||||
|
$groupIDs = session('dujiaoka_group_pwd_access', []);
|
||||||
|
$groupIDs = array_values(array_filter($groupIDs, function ($id) use ($groupID) {
|
||||||
|
return (int) $id !== $groupID;
|
||||||
|
}));
|
||||||
|
|
||||||
|
session(['dujiaoka_group_pwd_access' => $groupIDs]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 授权当前会话访问分类
|
||||||
|
*
|
||||||
|
* @param int $groupID 分类id
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function grantGroupAccess(int $groupID): void
|
||||||
|
{
|
||||||
|
$groupIDs = session('dujiaoka_group_pwd_access', []);
|
||||||
|
if (!in_array($groupID, $groupIDs)) {
|
||||||
|
$groupIDs[] = $groupID;
|
||||||
|
session(['dujiaoka_group_pwd_access' => $groupIDs]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 格式化商品信息
|
* 格式化商品信息
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -106,6 +106,8 @@ class OrderService
|
|||||||
$goods = $this->goodsService->detail($request->input('gid'));
|
$goods = $this->goodsService->detail($request->input('gid'));
|
||||||
// 商品状态验证
|
// 商品状态验证
|
||||||
$this->goodsService->validatorGoodsStatus($goods);
|
$this->goodsService->validatorGoodsStatus($goods);
|
||||||
|
// 商品所属分类访问权限验证
|
||||||
|
$this->goodsService->validatorGoodsGroupAccess($goods);
|
||||||
// 如果有限购
|
// 如果有限购
|
||||||
if ($goods->buy_limit_num > 0 && $request->input('by_amount') > $goods->buy_limit_num) {
|
if ($goods->buy_limit_num > 0 && $request->input('by_amount') > $goods->buy_limit_num) {
|
||||||
throw new RuleValidationException(__('dujiaoka.prompt.purchase_limit_exceeded'));
|
throw new RuleValidationException(__('dujiaoka.prompt.purchase_limit_exceeded'));
|
||||||
|
|||||||
Vendored
+1
-1
@@ -63,7 +63,7 @@ $(function() {
|
|||||||
localStorage.setItem("announcement",setTime);
|
localStorage.setItem("announcement",setTime);
|
||||||
}
|
}
|
||||||
// 版权
|
// 版权
|
||||||
console.group("Faka");console.log("Name: 云泽数卡");console.log("Github: https://github.com/assimon/dujiaoka");console.groupEnd();
|
console.group("Faka");console.log("Name: 独角数卡");console.log("Github: https://github.com/assimon/dujiaoka");console.groupEnd();
|
||||||
console.group("Theme");console.log("Name: Hyper Theme");console.log("Author: Bimoes");console.groupEnd();
|
console.group("Theme");console.log("Name: Hyper Theme");console.log("Author: Bimoes");console.groupEnd();
|
||||||
});
|
});
|
||||||
// 图片懒加载
|
// 图片懒加载
|
||||||
|
|||||||
Vendored
+39
-3
@@ -52,9 +52,45 @@
|
|||||||
if (typeof goodsMsg !== 'undefined' && goodsMsg !== '') {
|
if (typeof goodsMsg !== 'undefined' && goodsMsg !== '') {
|
||||||
let cateTpl = document.getElementById('cateTpl').innerHTML, cateHtml = '';
|
let cateTpl = document.getElementById('cateTpl').innerHTML, cateHtml = '';
|
||||||
let goodsTpl = document.getElementById('goodsTpl').innerHTML, goodsHtml;
|
let goodsTpl = document.getElementById('goodsTpl').innerHTML, goodsHtml;
|
||||||
let changeCate = function (key) {
|
let showGroupPassword = function (group) {
|
||||||
|
layer.prompt({
|
||||||
|
title : (typeof groupPasswordTitle !== 'undefined' ? groupPasswordTitle : '请输入分类访问密码') + ':' + group.gp_name,
|
||||||
|
formType: 1
|
||||||
|
}, function (value, index) {
|
||||||
|
$.post(groupPasswordVerifyUrl, {
|
||||||
|
_token : groupPasswordCsrfToken,
|
||||||
|
group_id: group.id,
|
||||||
|
password: value
|
||||||
|
}, function (res) {
|
||||||
|
if (res.code === 200) {
|
||||||
|
layer.close(index);
|
||||||
|
|
||||||
|
res.data.key = group.key;
|
||||||
|
res.data.is_group_locked = 0;
|
||||||
|
goodsMsg[group.key] = res.data;
|
||||||
|
|
||||||
|
laytpl(cateTpl).render(res.data, function (html) {
|
||||||
|
$('.cate-box').eq(group.key).replaceWith(html);
|
||||||
|
});
|
||||||
|
changeCate(group.key, true);
|
||||||
|
} else {
|
||||||
|
layer.msg(res.msg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
let changeCate = function (key, silent) {
|
||||||
|
let group = goodsMsg[key];
|
||||||
|
if (group.is_group_locked) {
|
||||||
|
$('.goods-list').empty();
|
||||||
|
$('.cate-box').removeClass('cate-box-select').eq(key).addClass('cate-box-select');
|
||||||
|
if (!silent) {
|
||||||
|
showGroupPassword(group);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
goodsHtml = '';
|
goodsHtml = '';
|
||||||
goodsMsg[key].goods.forEach(function (i) {
|
group.goods.forEach(function (i) {
|
||||||
if (i.wholesale_price_cnf != "" && i.wholesale_price_cnf != null) {
|
if (i.wholesale_price_cnf != "" && i.wholesale_price_cnf != null) {
|
||||||
i.wholesale_price_arr = i.wholesale_price_cnf.split("\r\n");
|
i.wholesale_price_arr = i.wholesale_price_cnf.split("\r\n");
|
||||||
i.wholesale_price_arr.forEach(function (ii, k) {
|
i.wholesale_price_arr.forEach(function (ii, k) {
|
||||||
@@ -79,7 +115,7 @@
|
|||||||
$('.cate').empty().append(cateHtml).on('click', '.cate-box', function () {
|
$('.cate').empty().append(cateHtml).on('click', '.cate-box', function () {
|
||||||
changeCate($(this).data('key'));
|
changeCate($(this).data('key'));
|
||||||
});
|
});
|
||||||
changeCate(0);
|
changeCate(0, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
@@ -124,7 +124,15 @@ return [
|
|||||||
'no_related_order_found_for_cache' => '未找到相关订单缓存!',
|
'no_related_order_found_for_cache' => '未找到相关订单缓存!',
|
||||||
'no_related_order_found' => '未找到相关订单!',
|
'no_related_order_found' => '未找到相关订单!',
|
||||||
'new_order_push' => '新订单通知',
|
'new_order_push' => '新订单通知',
|
||||||
'loop_carmis_limit' => '此商品最多购买一件!'
|
'loop_carmis_limit' => '此商品最多购买一件!',
|
||||||
|
'goods_group_does_not_exist' => '商品分类不存在',
|
||||||
|
'goods_group_password_error' => '分类访问密码错误',
|
||||||
|
'goods_group_password_required' => '请先输入分类访问密码',
|
||||||
|
'goods_group_password_required_short' => '密码访问',
|
||||||
|
'goods_group_password_title' => '分类密码访问',
|
||||||
|
'goods_group_password_placeholder' => '请输入分类访问密码',
|
||||||
|
'goods_group_password_submit' => '确认进入',
|
||||||
|
'goods_group_password_success' => '验证成功'
|
||||||
],
|
],
|
||||||
|
|
||||||
'equipment' => [
|
'equipment' => [
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ return [
|
|||||||
'fields' => [
|
'fields' => [
|
||||||
'gp_name' => '分类名称',
|
'gp_name' => '分类名称',
|
||||||
'is_open' => '是否启用',
|
'is_open' => '是否启用',
|
||||||
|
'is_open_group_pwd' => '开启密码访问',
|
||||||
|
'group_pwd' => '访问密码',
|
||||||
|
'group_pwd_help' => '开启密码访问后,前台需要输入此密码才可查看该分类商品',
|
||||||
'ord' => '排序权重 越大越靠前',
|
'ord' => '排序权重 越大越靠前',
|
||||||
],
|
],
|
||||||
'options' => [
|
'options' => [
|
||||||
|
|||||||
@@ -123,7 +123,16 @@ return [
|
|||||||
'search_order_browser_tips' => '最多只能查詢最近 5 筆訂單',
|
'search_order_browser_tips' => '最多只能查詢最近 5 筆訂單',
|
||||||
'no_related_order_found_for_cache' => '未找到相關訂單快取!',
|
'no_related_order_found_for_cache' => '未找到相關訂單快取!',
|
||||||
'no_related_order_found' => '未找到相關訂單!',
|
'no_related_order_found' => '未找到相關訂單!',
|
||||||
'new_order_push' => '新訂單通知'
|
'new_order_push' => '新訂單通知',
|
||||||
|
'loop_carmis_limit' => '此商品最多購買一件!',
|
||||||
|
'goods_group_does_not_exist' => '商品分類不存在',
|
||||||
|
'goods_group_password_error' => '分類訪問密碼錯誤',
|
||||||
|
'goods_group_password_required' => '請先輸入分類訪問密碼',
|
||||||
|
'goods_group_password_required_short' => '密碼訪問',
|
||||||
|
'goods_group_password_title' => '分類密碼訪問',
|
||||||
|
'goods_group_password_placeholder' => '請輸入分類訪問密碼',
|
||||||
|
'goods_group_password_submit' => '確認進入',
|
||||||
|
'goods_group_password_success' => '驗證成功'
|
||||||
],
|
],
|
||||||
|
|
||||||
'equipment' => [
|
'equipment' => [
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ return [
|
|||||||
'fields' => [
|
'fields' => [
|
||||||
'gp_name' => '分類名稱',
|
'gp_name' => '分類名稱',
|
||||||
'is_open' => '是否啟用',
|
'is_open' => '是否啟用',
|
||||||
|
'is_open_group_pwd' => '開啟密碼訪問',
|
||||||
|
'group_pwd' => '訪問密碼',
|
||||||
|
'group_pwd_help' => '開啟密碼訪問後,前台需要輸入此密碼才可查看該分類商品',
|
||||||
'ord' => '排序權重 越大越靠前',
|
'ord' => '排序權重 越大越靠前',
|
||||||
],
|
],
|
||||||
'options' => [
|
'options' => [
|
||||||
|
|||||||
@@ -14,11 +14,11 @@
|
|||||||
<div class="text-error mt-4">error</div>
|
<div class="text-error mt-4">error</div>
|
||||||
<h1 class="text-uppercase text-danger mt-3">{{ $content }}</h1>
|
<h1 class="text-uppercase text-danger mt-3">{{ $content }}</h1>
|
||||||
@if(!$url)
|
@if(!$url)
|
||||||
<a class="btn btn-info mt-3" href="javascript:history.back(-1);"><i class="mdi mdi-reply"></i> {{ __('hyper.error_back_btn') }}</a>
|
<a class="btn btn-info mt-3" href="{{ url('/') }}"><i class="mdi mdi-reply"></i> {{ __('hyper.error_back_btn') }}</a>
|
||||||
@else
|
@else
|
||||||
<a class="btn btn-info mt-3" href="{{ $url }}"><i class="mdi mdi-reply"></i> {{ __('hyper.error_back_btn') }}</a>
|
<a class="btn btn-info mt-3" href="{{ $url }}"><i class="mdi mdi-reply"></i> {{ __('hyper.error_back_btn') }}</a>
|
||||||
@endif
|
@endif
|
||||||
</div> <!-- end /.text-center-->
|
</div> <!-- end /.text-center-->
|
||||||
</div> <!-- end col-->
|
</div> <!-- end col-->
|
||||||
</div>
|
</div>
|
||||||
@stop
|
@stop
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<div class="page-title-right">
|
<div class="page-title-right">
|
||||||
<div class="app-search">
|
<div class="app-search">
|
||||||
<div class="position-relative">
|
<div class="position-relative">
|
||||||
<input type="text" class="form-control" id="search" placeholder="{{ __('hyper.home_search_box') }}">
|
<input type="text" class="form-control" id="search" name="goods_search_{{ time() }}" value="" autocomplete="off" placeholder="{{ __('hyper.home_search_box') }}">
|
||||||
<span class="uil-search"></span>
|
<span class="uil-search"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -32,9 +32,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
@foreach($data as $index => $group)
|
@foreach($data as $index => $group)
|
||||||
|
@if(!empty($group['is_group_locked']))
|
||||||
|
<a href="javascript:void(0);" class="tab-link group-password-link" data-group-id="{{ $group['id'] }}" data-group-name="{{ $group['gp_name'] }}" aria-expanded="false" role="tab">
|
||||||
|
@else
|
||||||
<a href="#group-{{ $group['id'] }}" class="tab-link" data-bs-toggle="tab" aria-expanded="false" role="tab" data-toggle="tab">
|
<a href="#group-{{ $group['id'] }}" class="tab-link" data-bs-toggle="tab" aria-expanded="false" role="tab" data-toggle="tab">
|
||||||
|
@endif
|
||||||
<span class="tab-title">
|
<span class="tab-title">
|
||||||
{{ $group['gp_name'] }}
|
{{ $group['gp_name'] }}
|
||||||
|
@if(!empty($group['is_group_locked']))
|
||||||
|
<i class="uil-lock-alt"></i>
|
||||||
|
@endif
|
||||||
</span>
|
</span>
|
||||||
<div class="img-checkmark">
|
<div class="img-checkmark">
|
||||||
<img src="/assets/hyper/images/check.png">
|
<img src="/assets/hyper/images/check.png">
|
||||||
@@ -98,6 +105,24 @@
|
|||||||
</div>
|
</div>
|
||||||
@endforeach
|
@endforeach
|
||||||
</div>
|
</div>
|
||||||
|
<div class="modal fade" id="group-password-modal" tabindex="-1" role="dialog" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">{{ __('dujiaoka.prompt.goods_group_password_title') }}</h5>
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<input type="hidden" id="group-password-id">
|
||||||
|
<div class="mb-2" id="group-password-name"></div>
|
||||||
|
<input type="password" class="form-control" id="group-password-input" placeholder="{{ __('dujiaoka.prompt.goods_group_password_placeholder') }}">
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-primary" id="group-password-submit">{{ __('dujiaoka.prompt.goods_group_password_submit') }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="modal fade" id="notice-modal" tabindex="-1" role="dialog" aria-hidden="true">
|
<div class="modal fade" id="notice-modal" tabindex="-1" role="dialog" aria-hidden="true">
|
||||||
<div class="modal-dialog modal-dialog-centered">
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
@@ -117,6 +142,11 @@
|
|||||||
$('#notice-open').click(function() {
|
$('#notice-open').click(function() {
|
||||||
$('#notice-modal').modal();
|
$('#notice-modal').modal();
|
||||||
});
|
});
|
||||||
|
var groupPasswordPassed = false;
|
||||||
|
|
||||||
|
clearSearchInput();
|
||||||
|
setTimeout(clearSearchInput, 300);
|
||||||
|
|
||||||
$("#search").on("input",function(e){
|
$("#search").on("input",function(e){
|
||||||
var txt = $("#search").val();
|
var txt = $("#search").val();
|
||||||
if($.trim(txt)!="") {
|
if($.trim(txt)!="") {
|
||||||
@@ -125,8 +155,112 @@
|
|||||||
$(".category").show();
|
$(".category").show();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$('.tab-link:not(.group-password-link)').click(function() {
|
||||||
|
resetPasswordGroups();
|
||||||
|
forgetAllPasswordGroupAccess();
|
||||||
|
clearSearchInput();
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).on('click', '.group-password-link', function() {
|
||||||
|
$('#group-password-id').val($(this).data('group-id'));
|
||||||
|
$('#group-password-name').text($(this).data('group-name'));
|
||||||
|
$('#group-password-input').val('');
|
||||||
|
$('#group-password-modal').modal();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#group-password-modal').on('hidden.bs.modal', function() {
|
||||||
|
if (!groupPasswordPassed) {
|
||||||
|
resetPasswordGroups();
|
||||||
|
forgetAllPasswordGroupAccess();
|
||||||
|
clearSearchInput();
|
||||||
|
showAllGroup();
|
||||||
|
}
|
||||||
|
groupPasswordPassed = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#group-password-submit').click(function() {
|
||||||
|
var groupId = $('#group-password-id').val();
|
||||||
|
|
||||||
|
$.post("{{ url('verify-group-password') }}", {
|
||||||
|
_token: "{{ csrf_token() }}",
|
||||||
|
group_id: groupId,
|
||||||
|
password: $('#group-password-input').val()
|
||||||
|
}, function(res) {
|
||||||
|
if (res.code === 200) {
|
||||||
|
groupPasswordPassed = true;
|
||||||
|
unlockGroup(groupId, res.data);
|
||||||
|
clearSearchInput();
|
||||||
|
$('#group-password-modal').modal('hide');
|
||||||
|
} else {
|
||||||
|
$.NotificationApp.send("{{ __('hyper.home_tip') }}", res.msg, "top-center", "rgba(0,0,0,0.2)", "error");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function unlockGroup(groupId, group) {
|
||||||
|
resetPasswordGroups(groupId);
|
||||||
|
|
||||||
|
var $link = $('.group-password-link[data-group-id="' + groupId + '"]');
|
||||||
|
$('#group-' + groupId + ' .hyper-wrapper').html(renderGroupGoods(group.goods || []));
|
||||||
|
|
||||||
|
$('.tab-link').removeClass('active');
|
||||||
|
$('.tab-pane').removeClass('active show');
|
||||||
|
$link.addClass('active');
|
||||||
|
$('#group-' + groupId).addClass('active show');
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetPasswordGroups(exceptGroupId) {
|
||||||
|
$('.group-password-link').each(function() {
|
||||||
|
var groupId = String($(this).data('group-id'));
|
||||||
|
if (exceptGroupId && groupId === String(exceptGroupId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#group-' + groupId + ' .hyper-wrapper').empty();
|
||||||
|
$(this).removeClass('active');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function forgetAllPasswordGroupAccess() {
|
||||||
|
$('.group-password-link').each(function() {
|
||||||
|
$.post("{{ url('forget-group-password-access') }}", {
|
||||||
|
_token: "{{ csrf_token() }}",
|
||||||
|
group_id: $(this).data('group-id')
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function showAllGroup() {
|
||||||
|
$('.tab-link').removeClass('active');
|
||||||
|
$('.tab-pane').removeClass('active show');
|
||||||
|
$('.tab-link[href="#group-all"]').addClass('active');
|
||||||
|
$('#group-all').addClass('active show');
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearSearchInput() {
|
||||||
|
$('#search').val('');
|
||||||
|
$('.category').show();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderGroupGoods(goodsList) {
|
||||||
|
var html = '';
|
||||||
|
$.each(goodsList, function(index, goods) {
|
||||||
|
if (parseInt(goods.in_stock) > 0) {
|
||||||
|
html += '<a href="/buy/' + goods.id + '" class="home-card category">';
|
||||||
|
} else {
|
||||||
|
html += '<a href="javascript:void(0);" onclick="sell_out_tip()" class="home-card category ribbon-box">';
|
||||||
|
html += '<div class="ribbon-two ribbon-two-danger"><span>{{ __('hyper.home_out_of_stock') }}</span></div>';
|
||||||
|
}
|
||||||
|
html += '<img class="home-img" src="' + goods.picture_url + '">';
|
||||||
|
html += '<div class="flex"><p class="name">' + goods.gd_name + '</p>';
|
||||||
|
html += '<div class="price">{{ __('hyper.global_currency') }}<b>' + goods.actual_price + '</b></div></div></a>';
|
||||||
|
});
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
function sell_out_tip() {
|
function sell_out_tip() {
|
||||||
$.NotificationApp.send("{{ __('hyper.home_tip') }}","{{ __('hyper.home_sell_out_tip') }}","top-center","rgba(0,0,0,0.2)","info");
|
$.NotificationApp.send("{{ __('hyper.home_tip') }}","{{ __('hyper.home_sell_out_tip') }}","top-center","rgba(0,0,0,0.2)","info");
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@stop
|
@stop
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
## 云泽数卡 - Luna模板
|
## 独角数卡 - Luna模板
|
||||||
|
|
||||||
一套简洁的云泽数卡模板
|
一套简洁的独角数卡模板
|
||||||
|
|
||||||
## 特殊用法
|
## 特殊用法
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
<div class="err_content">{{ $content }}</div>
|
<div class="err_content">{{ $content }}</div>
|
||||||
@if(!$url)
|
@if(!$url)
|
||||||
<div class="btn">
|
<div class="btn">
|
||||||
<a href="javascript:history.back(-1);">
|
<a href="{{ url('/') }}">
|
||||||
<span>{{ __('dujiaoka.callback') }}</span>
|
<span>{{ __('dujiaoka.callback') }}</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -48,5 +48,3 @@
|
|||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
@endsection
|
@endsection
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -6,3 +6,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -73,8 +73,8 @@
|
|||||||
</body>
|
</body>
|
||||||
<script id="cateTpl" type="text/html">
|
<script id="cateTpl" type="text/html">
|
||||||
<div class="cate-box" data-key="<< d.key >>">
|
<div class="cate-box" data-key="<< d.key >>">
|
||||||
<p><< d.gp_name >></p>
|
<p><< d.gp_name >><<# if(d.is_group_locked){ >> 🔒<<# }; >></p>
|
||||||
<div>{{ __('luna.goods_num') }}:<< d.goods.length >></div>
|
<div><<# if(d.is_group_locked){ >>{{ __('dujiaoka.prompt.goods_group_password_required_short') }}<<# } else { >>{{ __('luna.goods_num') }}:<< d.goods.length >><<# }; >></div>
|
||||||
</div>
|
</div>
|
||||||
</script>
|
</script>
|
||||||
<script id="goodsTpl" type="text/html">
|
<script id="goodsTpl" type="text/html">
|
||||||
@@ -101,8 +101,12 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
let title = "{{ __('dujiaoka.site_announcement') }}",
|
let title = "{{ __('dujiaoka.site_announcement') }}",
|
||||||
goodsMsg = {!! json_encode($data) !!};
|
goodsMsg = {!! json_encode($data) !!},
|
||||||
|
groupPasswordVerifyUrl = "{{ url('verify-group-password') }}",
|
||||||
|
groupPasswordCsrfToken = "{{ csrf_token() }}",
|
||||||
|
groupPasswordTitle = "{{ __('dujiaoka.prompt.goods_group_password_title') }}",
|
||||||
|
groupPasswordPlaceholder = "{{ __('dujiaoka.prompt.goods_group_password_placeholder') }}",
|
||||||
|
groupPasswordSubmit = "{{ __('dujiaoka.prompt.goods_group_password_submit') }}";
|
||||||
</script>
|
</script>
|
||||||
@endsection
|
@endsection
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-12 mt-3 text-center">
|
<div class="col-12 mt-3 text-center">
|
||||||
@if(!$url)
|
@if(!$url)
|
||||||
<a href="javascript:history.back(-1);" class="btn btn-outline-dark">{{ __('dujiaoka.callback') }}</a>
|
<a href="{{ url('/') }}" class="btn btn-outline-dark">{{ __('dujiaoka.callback') }}</a>
|
||||||
@else
|
@else
|
||||||
<a href="{{ $url }}" class="btn btn-outline-dark">{{ __('dujiaoka.callback') }}</a>
|
<a href="{{ $url }}" class="btn btn-outline-dark">{{ __('dujiaoka.callback') }}</a>
|
||||||
@endif
|
@endif
|
||||||
|
|||||||
@@ -48,7 +48,11 @@
|
|||||||
</li>
|
</li>
|
||||||
@foreach($data as $index => $group)
|
@foreach($data as $index => $group)
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a href="#group-{{ $group['id'] }}" data-bs-toggle="tab" class="btn btn-outline-secondary">{{ $group['gp_name'] }}</a>
|
@if(!empty($group['is_group_locked']))
|
||||||
|
<a href="javascript:void(0);" class="btn btn-outline-secondary group-password-link" data-group-id="{{ $group['id'] }}" data-group-name="{{ $group['gp_name'] }}">{{ $group['gp_name'] }} <i class="ali-icon"></i></a>
|
||||||
|
@else
|
||||||
|
<a href="#group-{{ $group['id'] }}" data-bs-toggle="tab" class="btn btn-outline-secondary">{{ $group['gp_name'] }}</a>
|
||||||
|
@endif
|
||||||
</li>
|
</li>
|
||||||
@endforeach
|
@endforeach
|
||||||
</ul>
|
</ul>
|
||||||
@@ -171,6 +175,25 @@
|
|||||||
</section>
|
</section>
|
||||||
<!-- main end -->
|
<!-- main end -->
|
||||||
|
|
||||||
|
<div class="modal fade" id="group-password-modal" tabindex="-1" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">{{ __('dujiaoka.prompt.goods_group_password_title') }}</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<input type="hidden" id="group-password-id">
|
||||||
|
<div class="mb-2" id="group-password-name"></div>
|
||||||
|
<input type="password" class="form-control" id="group-password-input" placeholder="{{ __('dujiaoka.prompt.goods_group_password_placeholder') }}">
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-primary" id="group-password-submit">{{ __('dujiaoka.prompt.goods_group_password_submit') }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
@stop
|
@stop
|
||||||
|
|
||||||
@section('js')
|
@section('js')
|
||||||
@@ -185,5 +208,91 @@
|
|||||||
$(".col").show();
|
$(".col").show();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function showGroupPasswordModal() {
|
||||||
|
var modalEl = document.getElementById('group-password-modal');
|
||||||
|
if (typeof bootstrap !== 'undefined' && bootstrap.Modal) {
|
||||||
|
bootstrap.Modal.getOrCreateInstance(modalEl).show();
|
||||||
|
} else {
|
||||||
|
$('#group-password-modal').modal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$('.group-password-link').click(function() {
|
||||||
|
$('#group-password-id').val($(this).data('group-id'));
|
||||||
|
$('#group-password-name').text($(this).data('group-name'));
|
||||||
|
$('#group-password-input').val('');
|
||||||
|
showGroupPasswordModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#group-password-submit').click(function() {
|
||||||
|
var groupId = $('#group-password-id').val();
|
||||||
|
|
||||||
|
$.post("{{ url('verify-group-password') }}", {
|
||||||
|
_token: "{{ csrf_token() }}",
|
||||||
|
group_id: groupId,
|
||||||
|
password: $('#group-password-input').val()
|
||||||
|
}, function(res) {
|
||||||
|
if (res.code === 200) {
|
||||||
|
unlockGroup(groupId, res.data);
|
||||||
|
hideGroupPasswordModal();
|
||||||
|
} else {
|
||||||
|
alert(res.msg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function hideGroupPasswordModal() {
|
||||||
|
var modalEl = document.getElementById('group-password-modal');
|
||||||
|
if (typeof bootstrap !== 'undefined' && bootstrap.Modal) {
|
||||||
|
bootstrap.Modal.getOrCreateInstance(modalEl).hide();
|
||||||
|
} else {
|
||||||
|
$('#group-password-modal').modal('hide');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function unlockGroup(groupId, group) {
|
||||||
|
var $link = $('.group-password-link[data-group-id="' + groupId + '"]');
|
||||||
|
$link.removeClass('group-password-link')
|
||||||
|
.attr('href', '#group-' + groupId)
|
||||||
|
.attr('data-bs-toggle', 'tab')
|
||||||
|
.find('.ali-icon')
|
||||||
|
.remove();
|
||||||
|
|
||||||
|
$('#group-' + groupId + ' .row').html(renderGroupGoods(group.goods || []));
|
||||||
|
|
||||||
|
if (typeof bootstrap !== 'undefined' && bootstrap.Tab) {
|
||||||
|
bootstrap.Tab.getOrCreateInstance($link[0]).show();
|
||||||
|
} else if (typeof $link.tab === 'function') {
|
||||||
|
$link.tab('show');
|
||||||
|
} else {
|
||||||
|
$('.category-menus a').removeClass('active');
|
||||||
|
$('.tab-pane').removeClass('active show');
|
||||||
|
$link.addClass('active');
|
||||||
|
$('#group-' + groupId).addClass('active show');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderGroupGoods(goodsList) {
|
||||||
|
var html = '';
|
||||||
|
$.each(goodsList, function(index, goods) {
|
||||||
|
html += '<div class="col"><div class="card position-relative">';
|
||||||
|
if (parseInt(goods.type) === {{ \App\Models\Goods::AUTOMATIC_DELIVERY }}) {
|
||||||
|
html += '<span class="badge bg-success position-absolute top-0 start-0"><i class="ali-icon"></i> {{ __('goods.fields.automatic_delivery') }}</span>';
|
||||||
|
} else {
|
||||||
|
html += '<span class="badge bg-warning position-absolute top-0 start-0"><i class="ali-icon"></i> {{ __('goods.fields.manual_processing') }}</span>';
|
||||||
|
}
|
||||||
|
html += '<img src="' + goods.picture_url + '" class="card-img-top" alt="' + goods.gd_name + '">';
|
||||||
|
html += '<div class="card-body"><h6 class="card-title text-truncate">' + goods.gd_name + '</h6>';
|
||||||
|
html += '<button type="button" class="btn btn-sm btn-outline-success"><i class="ali-icon"></i> <strong>' + goods.actual_price + '</strong></button>';
|
||||||
|
if (goods.wholesale_price_cnf) {
|
||||||
|
html += ' <button type="button" class="btn btn-sm btn-outline-warning"><i class="ali-icon"></i> {{ __('dujiaoka.home_discount') }}</button>';
|
||||||
|
}
|
||||||
|
html += '<h6 class="mt-2"><small class="text-muted">{{ __('goods.fields.in_stock') }}:' + goods.in_stock + '</small></h6>';
|
||||||
|
html += '<a href="/buy/' + goods.id + '" class="btn btn-primary fr"><i class="ali-icon"></i> {{ __('dujiaoka.order_now') }}</a>';
|
||||||
|
html += '</div></div></div>';
|
||||||
|
});
|
||||||
|
return html;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
@stop
|
@stop
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ Route::group(['middleware' => ['dujiaoka.boot'],'namespace' => 'Home'], function
|
|||||||
Route::get('check-geetest', 'HomeController@geetest');
|
Route::get('check-geetest', 'HomeController@geetest');
|
||||||
// 商品详情
|
// 商品详情
|
||||||
Route::get('buy/{id}', 'HomeController@buy');
|
Route::get('buy/{id}', 'HomeController@buy');
|
||||||
|
// 验证商品分类访问密码
|
||||||
|
Route::post('verify-group-password', 'HomeController@verifyGroupPassword');
|
||||||
|
// 取消商品分类密码访问授权
|
||||||
|
Route::post('forget-group-password-access', 'HomeController@forgetGroupPasswordAccess');
|
||||||
// 提交订单
|
// 提交订单
|
||||||
Route::post('create-order', 'OrderController@createOrder');
|
Route::post('create-order', 'OrderController@createOrder');
|
||||||
// 结算页
|
// 结算页
|
||||||
@@ -40,4 +44,3 @@ Route::group(['middleware' => ['install.check'],'namespace' => 'Home'], function
|
|||||||
// 执行安装
|
// 执行安装
|
||||||
Route::post('do-install', 'HomeController@doInstall');
|
Route::post('do-install', 'HomeController@doInstall');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user