更新若干功能
This commit is contained in:
38
app/Http/Controllers/Api/AfterSaleController.php
Normal file
38
app/Http/Controllers/Api/AfterSaleController.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\BaseApp\BaseController;
|
||||
use App\Service\business\AfterSaleService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* 后台售后
|
||||
*/
|
||||
class AfterSaleController extends BaseController
|
||||
{
|
||||
protected array $exceptRoute = ['option', 'create'];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->service = AfterSaleService::getInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* 审核售后
|
||||
* @Method POST
|
||||
*/
|
||||
public function audit(): JsonResponse
|
||||
{
|
||||
$pass = (int) request()->post('status', 1) === 1;
|
||||
return jok(
|
||||
$this->service->audit(
|
||||
(int) request()->post('id', 0),
|
||||
$pass,
|
||||
(string) request()->post('admin_remark', '')
|
||||
),
|
||||
$pass ? '已通过' : '已拒绝'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ class CatalogueController extends BaseController
|
||||
$this->service = CatalogueService::getInstance();
|
||||
$this->insertField = ['title', 'category_id', 'cover', 'identifier'];
|
||||
$this->updateField = ['id', 'title', 'category_id', 'cover', 'identifier'];
|
||||
$this->notRequest = ['alias', 'pdf', 'price', 'status'];
|
||||
$this->notRequest = ['alias', 'pdf', 'video', 'price', 'status'];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -32,4 +32,27 @@ class CatalogueController extends BaseController
|
||||
$params = $this->checkRequiredFields(request()->post());
|
||||
return jok($this->service->status($params['id'], $params['status']), '操作成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出商品表(结构化数据,前端 ExcelJS 画 xlsx)
|
||||
* @Method GET
|
||||
*/
|
||||
public function export(): JsonResponse
|
||||
{
|
||||
return jok($this->service->exportExcel());
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入商品表:前端已把 xlsx 拆成 rows
|
||||
* @Method POST
|
||||
*/
|
||||
public function import(): JsonResponse
|
||||
{
|
||||
$rows = request()->post('rows', []);
|
||||
$priceSheets = request()->post('price_sheets', []);
|
||||
return jok($this->service->importRows(
|
||||
is_array($rows) ? $rows : [],
|
||||
is_array($priceSheets) ? $priceSheets : [],
|
||||
), '导入完成');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,4 +32,23 @@ class FactoryInfoController extends BaseController
|
||||
$params = $this->checkRequiredFields(request()->post());
|
||||
return jok($this->service->status($params['id'], $params['status']), '操作成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出工厂表(结构化数据,前端 ExcelJS 画 xlsx)
|
||||
* @Method GET
|
||||
*/
|
||||
public function export(): JsonResponse
|
||||
{
|
||||
return jok($this->service->exportExcel());
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入工厂表:前端已把 xlsx 拆成 rows
|
||||
* @Method POST
|
||||
*/
|
||||
public function import(): JsonResponse
|
||||
{
|
||||
$rows = request()->post('rows', []);
|
||||
return jok($this->service->importRows(is_array($rows) ? $rows : []), '导入完成');
|
||||
}
|
||||
}
|
||||
|
||||
34
app/Http/Controllers/Api/FeedbackController.php
Normal file
34
app/Http/Controllers/Api/FeedbackController.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\BaseApp\BaseController;
|
||||
use App\Service\business\FeedbackService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* 后台用户反馈
|
||||
*/
|
||||
class FeedbackController extends BaseController
|
||||
{
|
||||
protected array $exceptRoute = ['option', 'create'];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->service = FeedbackService::getInstance();
|
||||
$this->updateField = ['id', 'reply'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 回复
|
||||
* @Method POST
|
||||
*/
|
||||
public function reply(): JsonResponse
|
||||
{
|
||||
return jok(
|
||||
$this->service->reply((int) request()->post('id', 0), (string) request()->post('reply', '')),
|
||||
'已回复'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -58,4 +58,13 @@ class ListController extends BaseController
|
||||
{
|
||||
return jok($this->service->deleteItem(request()->post('ids', [])), '删除成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出清单报价(结构化数据 + 分享文本)
|
||||
* @Method GET
|
||||
*/
|
||||
public function exportQuote(): JsonResponse
|
||||
{
|
||||
return jok($this->service->exportQuote((int) request()->get('id', 0)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ class PriceSheetController extends BaseController
|
||||
$this->service = PriceSheetService::getInstance();
|
||||
$this->insertField = ['catalogue_id'];
|
||||
$this->updateField = ['id'];
|
||||
$this->notRequest = ['specification', 'dimension', 'routine', 'rows', 'status'];
|
||||
$this->notRequest = ['specification', 'dimension', 'routine', 'stock', 'rows', 'status'];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -19,7 +19,8 @@ class WxAppController extends BaseController
|
||||
$this->updateField = ['id'];
|
||||
$this->notRequest = [
|
||||
'app_secret', 'mch_id', 'mch_key', 'mch_serial_no', 'mch_private_key',
|
||||
'platform_public_key', 'notify_url', 'template_code', 'package_enabled', 'remark', 'name', 'app_id',
|
||||
'platform_public_key', 'notify_url', 'template_code', 'package_enabled',
|
||||
'subscribe_pay_tpl', 'subscribe_ship_tpl', 'remark', 'name', 'app_id',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
39
app/Http/Controllers/Wx/AddressController.php
Normal file
39
app/Http/Controllers/Wx/AddressController.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Wx;
|
||||
|
||||
use App\BaseApp\BaseController;
|
||||
use App\Service\wx\WxAddressService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* 小程序收货地址
|
||||
*/
|
||||
class AddressController extends BaseController
|
||||
{
|
||||
protected array $exceptRoute = ['option'];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->service = WxAddressService::getInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认地址
|
||||
* @Method GET
|
||||
*/
|
||||
public function defaultOne(): JsonResponse
|
||||
{
|
||||
return jok($this->service->defaultOne());
|
||||
}
|
||||
|
||||
/**
|
||||
* 设为默认
|
||||
* @Method POST
|
||||
*/
|
||||
public function setDefault(): JsonResponse
|
||||
{
|
||||
return jok($this->service->setDefault((int) request()->post('id', 0)), '已设为默认');
|
||||
}
|
||||
}
|
||||
30
app/Http/Controllers/Wx/AfterSaleController.php
Normal file
30
app/Http/Controllers/Wx/AfterSaleController.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Wx;
|
||||
|
||||
use App\BaseApp\BaseController;
|
||||
use App\Service\wx\WxAfterSaleService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* 小程序售后
|
||||
*/
|
||||
class AfterSaleController extends BaseController
|
||||
{
|
||||
protected array $exceptRoute = ['option', 'detail', 'update', 'delete'];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->service = WxAfterSaleService::getInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* 申请售后
|
||||
* @Method POST
|
||||
*/
|
||||
public function create(): JsonResponse
|
||||
{
|
||||
return jok($this->service->create(request()->post()), '已提交');
|
||||
}
|
||||
}
|
||||
39
app/Http/Controllers/Wx/ColorcardController.php
Normal file
39
app/Http/Controllers/Wx/ColorcardController.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Wx;
|
||||
|
||||
use App\BaseApp\BaseController;
|
||||
use App\Service\wx\WxColorcardService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* 小程序色卡(免登录)
|
||||
*/
|
||||
class ColorcardController extends BaseController
|
||||
{
|
||||
protected array $exceptRoute = ['option', 'create', 'update', 'delete'];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->service = WxColorcardService::getInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* 色卡分类
|
||||
* @Method GET
|
||||
*/
|
||||
public function classList(): JsonResponse
|
||||
{
|
||||
return jok($this->service->classList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 色卡公司
|
||||
* @Method GET
|
||||
*/
|
||||
public function companyList(): JsonResponse
|
||||
{
|
||||
return jok($this->service->companyList());
|
||||
}
|
||||
}
|
||||
30
app/Http/Controllers/Wx/FactoryController.php
Normal file
30
app/Http/Controllers/Wx/FactoryController.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Wx;
|
||||
|
||||
use App\BaseApp\BaseController;
|
||||
use App\Service\wx\WxFactoryService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* 小程序工厂(免登录)
|
||||
*/
|
||||
class FactoryController extends BaseController
|
||||
{
|
||||
protected array $exceptRoute = ['option', 'create', 'update', 'delete'];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->service = WxFactoryService::getInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* 工厂分类
|
||||
* @Method GET
|
||||
*/
|
||||
public function classList(): JsonResponse
|
||||
{
|
||||
return jok($this->service->classList());
|
||||
}
|
||||
}
|
||||
30
app/Http/Controllers/Wx/FavoriteController.php
Normal file
30
app/Http/Controllers/Wx/FavoriteController.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Wx;
|
||||
|
||||
use App\BaseApp\BaseController;
|
||||
use App\Service\wx\WxFavoriteService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* 小程序收藏
|
||||
*/
|
||||
class FavoriteController extends BaseController
|
||||
{
|
||||
protected array $exceptRoute = ['option', 'detail', 'create', 'update', 'delete'];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->service = WxFavoriteService::getInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换收藏
|
||||
* @Method POST
|
||||
*/
|
||||
public function toggle(): JsonResponse
|
||||
{
|
||||
return jok($this->service->toggle((int) request()->post('catalogue_id', 0)));
|
||||
}
|
||||
}
|
||||
30
app/Http/Controllers/Wx/FeedbackController.php
Normal file
30
app/Http/Controllers/Wx/FeedbackController.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Wx;
|
||||
|
||||
use App\BaseApp\BaseController;
|
||||
use App\Service\wx\WxFeedbackService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* 小程序反馈
|
||||
*/
|
||||
class FeedbackController extends BaseController
|
||||
{
|
||||
protected array $exceptRoute = ['option', 'detail', 'update', 'delete'];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->service = WxFeedbackService::getInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交反馈
|
||||
* @Method POST
|
||||
*/
|
||||
public function create(): JsonResponse
|
||||
{
|
||||
return jok($this->service->create(request()->post()), '已提交');
|
||||
}
|
||||
}
|
||||
@@ -93,4 +93,13 @@ class ListController extends BaseController
|
||||
{
|
||||
return jok($this->service->updateItem(request()->post()), '保存成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出清单报价
|
||||
* @Method GET
|
||||
*/
|
||||
public function exportQuote(): JsonResponse
|
||||
{
|
||||
return jok($this->service->exportQuote((int) request()->get('id', 0)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,4 +48,13 @@ class UserController extends BaseController
|
||||
'保存成功'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 已绑定企业详情
|
||||
* @Method GET
|
||||
*/
|
||||
public function enterprise(): JsonResponse
|
||||
{
|
||||
return jok($this->service->enterprise());
|
||||
}
|
||||
}
|
||||
|
||||
32
app/Models/business/AfterSaleModel.php
Normal file
32
app/Models/business/AfterSaleModel.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\business;
|
||||
|
||||
use App\BaseApp\BaseBusinessModel;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
/**
|
||||
* 售后申请(cc_after_sale)
|
||||
*/
|
||||
class AfterSaleModel extends BaseBusinessModel
|
||||
{
|
||||
protected $table = 'after_sale';
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
public const STATUS_PENDING = 0;
|
||||
|
||||
public const STATUS_APPROVED = 1;
|
||||
|
||||
public const STATUS_REJECTED = 2;
|
||||
|
||||
public function order(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(OrderModel::class, 'order_id', 'id');
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(WxUserModel::class, 'user_id', 'id');
|
||||
}
|
||||
}
|
||||
21
app/Models/business/FavoriteModel.php
Normal file
21
app/Models/business/FavoriteModel.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\business;
|
||||
|
||||
use App\BaseApp\BaseBusinessModel;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
/**
|
||||
* 商品收藏(cc_favorite)
|
||||
*/
|
||||
class FavoriteModel extends BaseBusinessModel
|
||||
{
|
||||
protected $table = 'favorite';
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
public function catalogue(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(CatalogueModel::class, 'catalogue_id', 'id');
|
||||
}
|
||||
}
|
||||
25
app/Models/business/FeedbackModel.php
Normal file
25
app/Models/business/FeedbackModel.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\business;
|
||||
|
||||
use App\BaseApp\BaseBusinessModel;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
/**
|
||||
* 用户反馈(cc_feedback)
|
||||
*/
|
||||
class FeedbackModel extends BaseBusinessModel
|
||||
{
|
||||
protected $table = 'feedback';
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
public const STATUS_PENDING = 0;
|
||||
|
||||
public const STATUS_REPLIED = 1;
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(WxUserModel::class, 'user_id', 'id');
|
||||
}
|
||||
}
|
||||
15
app/Models/business/WxAddressModel.php
Normal file
15
app/Models/business/WxAddressModel.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\business;
|
||||
|
||||
use App\BaseApp\BaseBusinessModel;
|
||||
|
||||
/**
|
||||
* 小程序收货地址(cc_wx_address)
|
||||
*/
|
||||
class WxAddressModel extends BaseBusinessModel
|
||||
{
|
||||
protected $table = 'wx_address';
|
||||
|
||||
protected $guarded = [];
|
||||
}
|
||||
@@ -25,7 +25,8 @@ class WxAppConfigService extends BaseService
|
||||
$this->model = WxAppModel::class;
|
||||
$this->selectField = [
|
||||
'id', 'code', 'name', 'app_id', 'mch_id', 'mch_serial_no', 'notify_url',
|
||||
'template_code', 'package_enabled', 'status', 'remark', 'created_at', 'updated_at',
|
||||
'template_code', 'package_enabled', 'subscribe_pay_tpl', 'subscribe_ship_tpl',
|
||||
'status', 'remark', 'created_at', 'updated_at',
|
||||
];
|
||||
$this->queryField = ['code' => '=', 'name' => 'like', 'status' => '='];
|
||||
$this->orderBy = ['name' => 'id', 'sort' => 'asc'];
|
||||
|
||||
61
app/Service/business/AfterSaleService.php
Normal file
61
app/Service/business/AfterSaleService.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\business;
|
||||
|
||||
use App\BaseApp\BaseService;
|
||||
use App\Models\business\AfterSaleModel;
|
||||
|
||||
/**
|
||||
* 后台售后审核。通过只改状态,线下退款,不自动打微信。
|
||||
*/
|
||||
class AfterSaleService extends BaseService
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->model = AfterSaleModel::class;
|
||||
$this->selectField = [
|
||||
'id', 'order_id', 'user_id', 'reason', 'amount', 'images',
|
||||
'status', 'admin_remark', 'created_at', 'updated_at',
|
||||
];
|
||||
$this->queryField = ['order_id' => '=', 'user_id' => '=', 'status' => '='];
|
||||
$this->with = ['order', 'user'];
|
||||
}
|
||||
|
||||
public function list(): array
|
||||
{
|
||||
$result = $this->getPageList();
|
||||
$price = PriceService::getInstance();
|
||||
foreach ($result['items'] as &$item) {
|
||||
$item['user_name'] = $item['user']['nick_name'] ?? '';
|
||||
$item['order_no'] = $item['order']['order_no'] ?? '';
|
||||
$item['amount_text'] = $price->centsToYuan((int) ($item['amount'] ?? 0));
|
||||
unset($item['user'], $item['order']);
|
||||
}
|
||||
unset($item);
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 审核:pass=true 通过
|
||||
*/
|
||||
public function audit(int $id, bool $pass, string $remark = ''): mixed
|
||||
{
|
||||
$row = AfterSaleModel::where('id', $id)->where('deleted_at', 0)->first();
|
||||
if (empty($row)) {
|
||||
$this->utils->errorThrow('售后单不存在');
|
||||
}
|
||||
if ((int) $row['status'] !== AfterSaleModel::STATUS_PENDING) {
|
||||
$this->utils->errorThrow('已处理过');
|
||||
}
|
||||
return $this->save($id, [
|
||||
'status' => $pass ? AfterSaleModel::STATUS_APPROVED : AfterSaleModel::STATUS_REJECTED,
|
||||
'admin_remark' => mb_substr(trim($remark), 0, 255),
|
||||
]);
|
||||
}
|
||||
|
||||
public function delete($ids): mixed
|
||||
{
|
||||
return $this->del(is_array($ids) ? $ids : [$ids]);
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ use App\Models\business\ImageModel;
|
||||
use App\Models\business\PriceSheetModel;
|
||||
use App\Service\common\MediaUrlService;
|
||||
use Exception;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Psr\Container\ContainerExceptionInterface;
|
||||
use Psr\Container\NotFoundExceptionInterface;
|
||||
|
||||
@@ -23,7 +24,7 @@ class CatalogueService extends BaseService
|
||||
{
|
||||
parent::__construct();
|
||||
$this->model = CatalogueModel::class;
|
||||
$this->selectField = ['id', 'title', 'category_id', 'cover', 'pdf', 'price', 'alias', 'identifier', 'status', 'created_at', 'updated_at'];
|
||||
$this->selectField = ['id', 'title', 'category_id', 'cover', 'pdf', 'video', 'price', 'alias', 'identifier', 'status', 'created_at', 'updated_at'];
|
||||
$this->queryField = ['title' => 'like', 'alias' => 'like', 'identifier' => 'like', 'status' => '='];
|
||||
$this->media = MediaUrlService::getInstance();
|
||||
}
|
||||
@@ -55,6 +56,7 @@ class CatalogueService extends BaseService
|
||||
unset($item['category']);
|
||||
$item['cover'] = $this->media->toPublic($item['cover'] ?? '');
|
||||
$item['pdf'] = $this->media->toPublic($item['pdf'] ?? '');
|
||||
$item['video'] = $this->media->toPublic($item['video'] ?? '');
|
||||
$item['show_field'] = $this->pickUsedMaterialFields($item['price_sheet'] ?? []);
|
||||
}
|
||||
unset($item);
|
||||
@@ -76,6 +78,7 @@ class CatalogueService extends BaseService
|
||||
$info = $this->getDetail($id);
|
||||
$info->cover = $this->media->toPublic($info->cover);
|
||||
$info->pdf = $this->media->toPublic($info->pdf);
|
||||
$info->video = $this->media->toPublic($info->video);
|
||||
|
||||
$priceSheet = PriceSheetModel::where('catalogue_id', $id)
|
||||
->where('deleted_at', 0)
|
||||
@@ -134,6 +137,141 @@ class CatalogueService extends BaseService
|
||||
return $this->save($id, ['status' => (int) $status]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出商品 + 关联报价单:两个工作表,xlsx 由前端 ExcelJS 美化
|
||||
*/
|
||||
public function exportExcel(): array
|
||||
{
|
||||
$rows = CatalogueModel::where('deleted_at', 0)->orderByDesc('id')->get([
|
||||
'id', 'title', 'identifier', 'alias', 'category_id', 'price', 'status',
|
||||
])->toArray();
|
||||
$byId = [];
|
||||
foreach ($rows as &$row) {
|
||||
$row['status'] = (int) ($row['status'] ?? 0) === 0 ? '上架' : '下架';
|
||||
$byId[(int) $row['id']] = $row;
|
||||
}
|
||||
unset($row);
|
||||
|
||||
$ids = array_keys($byId);
|
||||
$priceRows = [];
|
||||
if (!empty($ids)) {
|
||||
$sheets = PriceSheetModel::where('deleted_at', 0)
|
||||
->whereIn('catalogue_id', $ids)
|
||||
->orderBy('catalogue_id')
|
||||
->orderBy('id')
|
||||
->get(['id', 'catalogue_id', 'specification', 'dimension', 'routine', 'stock', 'status'])
|
||||
->toArray();
|
||||
foreach ($sheets as $sheet) {
|
||||
$cat = $byId[(int) $sheet['catalogue_id']] ?? [];
|
||||
$priceRows[] = [
|
||||
'id' => $sheet['id'],
|
||||
'catalogue_id' => $sheet['catalogue_id'],
|
||||
'identifier' => $cat['identifier'] ?? '',
|
||||
'title' => $cat['title'] ?? '',
|
||||
'specification' => $sheet['specification'] ?? '',
|
||||
'dimension' => $sheet['dimension'] ?? '',
|
||||
'routine' => $sheet['routine'] ?? '',
|
||||
'stock' => (int) ($sheet['stock'] ?? -1),
|
||||
'status' => (int) ($sheet['status'] ?? 0) === 0 ? '启用' : '禁用',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'filename' => 'catalogue.xlsx',
|
||||
'title' => '商品图册',
|
||||
'sheets' => [
|
||||
[
|
||||
'title' => '商品图册',
|
||||
'sheet' => '商品',
|
||||
'columns' => [
|
||||
['key' => 'id', 'header' => 'ID', 'width' => 10, 'align' => 'center'],
|
||||
['key' => 'title', 'header' => '商品名称', 'width' => 28],
|
||||
['key' => 'identifier', 'header' => '商品编号', 'width' => 16],
|
||||
['key' => 'alias', 'header' => '别名', 'width' => 24],
|
||||
['key' => 'category_id', 'header' => '分类ID', 'width' => 12, 'align' => 'center'],
|
||||
['key' => 'price', 'header' => '参考价', 'width' => 14, 'align' => 'right'],
|
||||
['key' => 'status', 'header' => '状态', 'width' => 10, 'align' => 'center'],
|
||||
],
|
||||
'rows' => $rows,
|
||||
],
|
||||
[
|
||||
'title' => '关联报价单',
|
||||
'sheet' => '报价单',
|
||||
'subtitle' => '按商品编号回写;库存 -1 表示不限',
|
||||
'columns' => [
|
||||
['key' => 'id', 'header' => 'ID', 'width' => 10, 'align' => 'center'],
|
||||
['key' => 'catalogue_id', 'header' => '商品ID', 'width' => 12, 'align' => 'center'],
|
||||
['key' => 'identifier', 'header' => '商品编号', 'width' => 16],
|
||||
['key' => 'title', 'header' => '商品名称', 'width' => 24],
|
||||
['key' => 'specification', 'header' => '规格', 'width' => 20],
|
||||
['key' => 'dimension', 'header' => '尺寸', 'width' => 16],
|
||||
['key' => 'routine', 'header' => '常规价', 'width' => 14, 'align' => 'right'],
|
||||
['key' => 'stock', 'header' => '库存', 'width' => 10, 'align' => 'center'],
|
||||
['key' => 'status', 'header' => '状态', 'width' => 10, 'align' => 'center'],
|
||||
],
|
||||
'rows' => $priceRows,
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入商品 + 报价单:先落商品,再按编号挂规格
|
||||
*/
|
||||
public function importRows(array $rows, array $priceSheets = []): array
|
||||
{
|
||||
$created = 0;
|
||||
$updated = 0;
|
||||
$priceCreated = 0;
|
||||
$priceUpdated = 0;
|
||||
$now = time();
|
||||
DB::connection('business')->beginTransaction();
|
||||
try {
|
||||
foreach ($rows as $row) {
|
||||
if (!is_array($row)) {
|
||||
continue;
|
||||
}
|
||||
$identifier = trim((string) ($row['identifier'] ?? $row['商品编号'] ?? ''));
|
||||
$title = trim((string) ($row['title'] ?? $row['商品名称'] ?? ''));
|
||||
if ($title === '') {
|
||||
continue;
|
||||
}
|
||||
$payload = [
|
||||
'title' => $title,
|
||||
'identifier' => $identifier,
|
||||
'alias' => (string) ($row['alias'] ?? $row['别名'] ?? ''),
|
||||
'category_id' => (int) ($row['category_id'] ?? $row['分类ID'] ?? 0),
|
||||
'price' => (string) ($row['price'] ?? $row['参考价'] ?? ''),
|
||||
'status' => $this->parseStatus($row['status'] ?? $row['状态'] ?? 0),
|
||||
'updated_at' => $now,
|
||||
];
|
||||
$exists = $identifier !== ''
|
||||
? CatalogueModel::where('identifier', $identifier)->where('deleted_at', 0)->first()
|
||||
: null;
|
||||
if (!empty($exists)) {
|
||||
CatalogueModel::where('id', $exists['id'])->update($payload);
|
||||
$updated++;
|
||||
} else {
|
||||
$payload['created_at'] = $now;
|
||||
CatalogueModel::insert($payload);
|
||||
$created++;
|
||||
}
|
||||
}
|
||||
[$priceCreated, $priceUpdated] = $this->importPriceSheets($priceSheets, $now);
|
||||
DB::connection('business')->commit();
|
||||
} catch (Exception $e) {
|
||||
DB::connection('business')->rollBack();
|
||||
$this->utils->errorThrow($e->getMessage());
|
||||
}
|
||||
return [
|
||||
'created' => $created,
|
||||
'updated' => $updated,
|
||||
'price_created' => $priceCreated,
|
||||
'price_updated' => $priceUpdated,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 报价单里真正有值的材质列
|
||||
*/
|
||||
@@ -164,7 +302,7 @@ class CatalogueService extends BaseService
|
||||
|
||||
private function normalize(array $params): array
|
||||
{
|
||||
foreach (['cover', 'pdf'] as $field) {
|
||||
foreach (['cover', 'pdf', 'video'] as $field) {
|
||||
if (array_key_exists($field, $params)) {
|
||||
$params[$field] = $this->media->firstOf($params[$field]);
|
||||
}
|
||||
@@ -190,4 +328,100 @@ class CatalogueService extends BaseService
|
||||
$this->utils->errorThrow('商品编号已存在:' . $identifier);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入报价单:优先商品编号,其次商品ID;同商品同规格同尺寸则更新
|
||||
*/
|
||||
private function importPriceSheets(array $rows, int $now): array
|
||||
{
|
||||
$created = 0;
|
||||
$updated = 0;
|
||||
foreach ($rows as $row) {
|
||||
if (!is_array($row)) {
|
||||
continue;
|
||||
}
|
||||
$catalogueId = $this->resolveCatalogueId($row);
|
||||
$specification = trim((string) ($row['specification'] ?? $row['规格'] ?? ''));
|
||||
if ($catalogueId <= 0 || $specification === '') {
|
||||
continue;
|
||||
}
|
||||
$dimension = (string) ($row['dimension'] ?? $row['尺寸'] ?? '');
|
||||
$payload = [
|
||||
'catalogue_id' => $catalogueId,
|
||||
'specification' => $specification,
|
||||
'dimension' => $dimension,
|
||||
'routine' => (string) ($row['routine'] ?? $row['常规价'] ?? ''),
|
||||
'stock' => $this->parseStock($row['stock'] ?? $row['库存'] ?? -1),
|
||||
'status' => $this->parseStatus($row['status'] ?? $row['状态'] ?? 0),
|
||||
'updated_at' => $now,
|
||||
];
|
||||
$id = (int) ($row['id'] ?? $row['ID'] ?? 0);
|
||||
$exists = $id > 0
|
||||
? PriceSheetModel::where('id', $id)->where('catalogue_id', $catalogueId)->where('deleted_at', 0)->first()
|
||||
: null;
|
||||
if (empty($exists)) {
|
||||
$exists = PriceSheetModel::where('catalogue_id', $catalogueId)
|
||||
->where('specification', $specification)
|
||||
->where('dimension', $dimension)
|
||||
->where('deleted_at', 0)
|
||||
->first();
|
||||
}
|
||||
if (!empty($exists)) {
|
||||
PriceSheetModel::where('id', $exists['id'])->update($payload);
|
||||
$updated++;
|
||||
} else {
|
||||
$payload['created_at'] = $now;
|
||||
PriceSheetModel::insert($payload);
|
||||
$created++;
|
||||
}
|
||||
}
|
||||
return [$created, $updated];
|
||||
}
|
||||
|
||||
/**
|
||||
* 报价单行挂到哪件商品:编号优先,兼容导出里的商品ID
|
||||
*/
|
||||
private function resolveCatalogueId(array $row): int
|
||||
{
|
||||
$identifier = trim((string) ($row['identifier'] ?? $row['商品编号'] ?? ''));
|
||||
if ($identifier !== '') {
|
||||
$cat = CatalogueModel::where('identifier', $identifier)->where('deleted_at', 0)->first();
|
||||
if (!empty($cat)) {
|
||||
return (int) $cat['id'];
|
||||
}
|
||||
}
|
||||
$id = (int) ($row['catalogue_id'] ?? $row['商品ID'] ?? 0);
|
||||
if ($id <= 0) {
|
||||
return 0;
|
||||
}
|
||||
$exists = CatalogueModel::where('id', $id)->where('deleted_at', 0)->exists();
|
||||
return $exists ? $id : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Excel 状态列可能是「上架/下架」或 0/1,统一成库里的数字
|
||||
*/
|
||||
private function parseStatus(mixed $value): int
|
||||
{
|
||||
$raw = trim((string) $value);
|
||||
if (in_array($raw, ['上架', '启用', '正常', '0', ''], true)) {
|
||||
return 0;
|
||||
}
|
||||
if (in_array($raw, ['下架', '禁用', '1'], true)) {
|
||||
return 1;
|
||||
}
|
||||
return ((int) $raw) === 1 ? 1 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 库存:空 / 不限 → -1
|
||||
*/
|
||||
private function parseStock(mixed $value): int
|
||||
{
|
||||
$raw = trim((string) $value);
|
||||
if ($raw === '' || in_array($raw, ['不限', '无限', '-1'], true)) {
|
||||
return -1;
|
||||
}
|
||||
return (int) $raw;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,4 +100,84 @@ class FactoryInfoService extends BaseService
|
||||
{
|
||||
return $this->save($id, ['status' => (int) $status]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出工厂表:只回结构化数据,xlsx 由前端 ExcelJS 美化
|
||||
*/
|
||||
public function exportExcel(): array
|
||||
{
|
||||
$rows = FactoryInfoModel::where('deleted_at', 0)->orderByDesc('id')->get([
|
||||
'id', 'name', 'phone', 'classification', 'address', 'status',
|
||||
])->toArray();
|
||||
foreach ($rows as &$row) {
|
||||
$row['status'] = (int) ($row['status'] ?? 0) === 0 ? '启用' : '禁用';
|
||||
}
|
||||
unset($row);
|
||||
return [
|
||||
'filename' => 'factory.xlsx',
|
||||
'title' => '工厂信息',
|
||||
'sheet' => '工厂',
|
||||
'columns' => [
|
||||
['key' => 'id', 'header' => 'ID', 'width' => 10, 'align' => 'center'],
|
||||
['key' => 'name', 'header' => '工厂名称', 'width' => 22],
|
||||
['key' => 'phone', 'header' => '电话', 'width' => 16],
|
||||
['key' => 'classification', 'header' => '分类ID', 'width' => 12, 'align' => 'center'],
|
||||
['key' => 'address', 'header' => '地址', 'width' => 32],
|
||||
['key' => 'status', 'header' => '状态', 'width' => 10, 'align' => 'center'],
|
||||
],
|
||||
'rows' => $rows,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入工厂:前端 ExcelJS 拆好的行,按 name 更新
|
||||
*/
|
||||
public function importRows(array $rows): array
|
||||
{
|
||||
$created = 0;
|
||||
$updated = 0;
|
||||
$now = time();
|
||||
foreach ($rows as $row) {
|
||||
if (!is_array($row)) {
|
||||
continue;
|
||||
}
|
||||
$name = trim((string) ($row['name'] ?? $row['工厂名称'] ?? ''));
|
||||
if ($name === '') {
|
||||
continue;
|
||||
}
|
||||
$payload = [
|
||||
'name' => $name,
|
||||
'phone' => (string) ($row['phone'] ?? $row['电话'] ?? ''),
|
||||
'classification' => (int) ($row['classification'] ?? $row['分类ID'] ?? 0),
|
||||
'address' => (string) ($row['address'] ?? $row['地址'] ?? ''),
|
||||
'status' => $this->parseStatus($row['status'] ?? $row['状态'] ?? 0),
|
||||
'updated_at' => $now,
|
||||
];
|
||||
$exists = FactoryInfoModel::where('name', $name)->where('deleted_at', 0)->first();
|
||||
if (!empty($exists)) {
|
||||
FactoryInfoModel::where('id', $exists['id'])->update($payload);
|
||||
$updated++;
|
||||
} else {
|
||||
$payload['created_at'] = $now;
|
||||
FactoryInfoModel::insert($payload);
|
||||
$created++;
|
||||
}
|
||||
}
|
||||
return ['created' => $created, 'updated' => $updated];
|
||||
}
|
||||
|
||||
/**
|
||||
* Excel 状态列可能是「启用/禁用」或 0/1,统一成库里的数字
|
||||
*/
|
||||
private function parseStatus(mixed $value): int
|
||||
{
|
||||
$raw = trim((string) $value);
|
||||
if (in_array($raw, ['上架', '启用', '正常', '0', ''], true)) {
|
||||
return 0;
|
||||
}
|
||||
if (in_array($raw, ['下架', '禁用', '1'], true)) {
|
||||
return 1;
|
||||
}
|
||||
return ((int) $raw) === 1 ? 1 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
56
app/Service/business/FeedbackService.php
Normal file
56
app/Service/business/FeedbackService.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\business;
|
||||
|
||||
use App\BaseApp\BaseService;
|
||||
use App\Models\business\FeedbackModel;
|
||||
|
||||
/**
|
||||
* 后台用户反馈
|
||||
*/
|
||||
class FeedbackService extends BaseService
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->model = FeedbackModel::class;
|
||||
$this->selectField = ['id', 'user_id', 'content', 'images', 'reply', 'status', 'created_at', 'updated_at'];
|
||||
$this->queryField = ['user_id' => '=', 'status' => '=', 'content' => 'like'];
|
||||
$this->with = ['user'];
|
||||
}
|
||||
|
||||
public function list(): array
|
||||
{
|
||||
$result = $this->getPageList();
|
||||
foreach ($result['items'] as &$item) {
|
||||
$item['user_name'] = $item['user']['nick_name'] ?? '';
|
||||
$item['user_phone'] = $item['user']['phone'] ?? '';
|
||||
unset($item['user']);
|
||||
}
|
||||
unset($item);
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 回复反馈
|
||||
*/
|
||||
public function reply(int $id, string $reply): mixed
|
||||
{
|
||||
if ($id <= 0) {
|
||||
$this->utils->errorThrow('参数错误');
|
||||
}
|
||||
$reply = trim($reply);
|
||||
if ($reply === '') {
|
||||
$this->utils->errorThrow('请填写回复');
|
||||
}
|
||||
return $this->save($id, [
|
||||
'reply' => mb_substr($reply, 0, 500),
|
||||
'status' => FeedbackModel::STATUS_REPLIED,
|
||||
]);
|
||||
}
|
||||
|
||||
public function delete($ids): mixed
|
||||
{
|
||||
return $this->del(is_array($ids) ? $ids : [$ids]);
|
||||
}
|
||||
}
|
||||
@@ -222,4 +222,16 @@ class ListService extends BaseService
|
||||
'updated_at' => time(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出清单报价:后台画 xlsx,小程序用 text 分享
|
||||
*/
|
||||
public function exportQuote(int $id): array
|
||||
{
|
||||
$info = $this->detail($id);
|
||||
if (!is_array($info)) {
|
||||
$this->utils->errorThrow('清单不存在');
|
||||
}
|
||||
return \App\Service\common\ExcelCsvService::getInstance()->quoteFromList($info);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,6 +91,7 @@ class OrderCoreService
|
||||
continue;
|
||||
}
|
||||
$quantity = max(1, (int) $item['quantity']);
|
||||
StockService::getInstance()->assertAvailable((int) $item['price_sheet_id'], $quantity);
|
||||
$unitPrice = (int) $item['unit_price'];
|
||||
if ($unitPrice <= 0) {
|
||||
$unitPrice = $price->resolveUnitPrice($sheet['routine'] ?? '', (string) $item['material_key'], $multiplier);
|
||||
@@ -152,6 +153,9 @@ class OrderCoreService
|
||||
}
|
||||
unset($row);
|
||||
OrderItemModel::insert($rows);
|
||||
foreach ($rows as $row) {
|
||||
StockService::getInstance()->deduct((int) $row['price_sheet_id'], (int) $row['quantity']);
|
||||
}
|
||||
ListModel::where('id', $list['id'])->update(['status' => 1, 'updated_at' => $now]);
|
||||
});
|
||||
|
||||
@@ -270,6 +274,9 @@ class OrderCoreService
|
||||
}
|
||||
$this->applyPaid($orderId, (int) $payment['amount'], $now);
|
||||
});
|
||||
if ($status === OrderPaymentModel::STATUS_CONFIRMED) {
|
||||
$this->notifySubscribe($orderId, 'pay');
|
||||
}
|
||||
return $this->detail($orderId);
|
||||
}
|
||||
|
||||
@@ -308,11 +315,14 @@ class OrderCoreService
|
||||
public function confirmWechatPay(string $outTradeNo, string $transactionId, int $amount): bool
|
||||
{
|
||||
$done = false;
|
||||
DB::connection('business')->transaction(function () use ($outTradeNo, $transactionId, $amount, &$done) {
|
||||
$fresh = false;
|
||||
$orderId = 0;
|
||||
DB::connection('business')->transaction(function () use ($outTradeNo, $transactionId, $amount, &$done, &$fresh, &$orderId) {
|
||||
$payment = OrderPaymentModel::where('out_trade_no', $outTradeNo)->lockForUpdate()->first();
|
||||
if (empty($payment)) {
|
||||
return;
|
||||
}
|
||||
$orderId = (int) $payment['order_id'];
|
||||
if ((int) $payment['status'] === OrderPaymentModel::STATUS_CONFIRMED) {
|
||||
$done = true;
|
||||
return;
|
||||
@@ -325,9 +335,13 @@ class OrderCoreService
|
||||
'audited_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
$this->applyPaid((int) $payment['order_id'], $amount > 0 ? $amount : (int) $payment['amount'], $now);
|
||||
$this->applyPaid($orderId, $amount > 0 ? $amount : (int) $payment['amount'], $now);
|
||||
$done = true;
|
||||
$fresh = true;
|
||||
});
|
||||
if ($fresh && $orderId > 0) {
|
||||
$this->notifySubscribe($orderId, 'pay');
|
||||
}
|
||||
return $done;
|
||||
}
|
||||
|
||||
@@ -367,6 +381,7 @@ class OrderCoreService
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
});
|
||||
$this->notifySubscribe($orderId, 'ship');
|
||||
return $this->detail($orderId);
|
||||
}
|
||||
|
||||
@@ -405,4 +420,47 @@ class OrderCoreService
|
||||
}
|
||||
return $order->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* 付款/发货后尽力发订阅消息,失败不影响主流程
|
||||
*/
|
||||
private function notifySubscribe(int $orderId, string $scene): void
|
||||
{
|
||||
if ($orderId <= 0) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
$order = OrderModel::where('id', $orderId)->first();
|
||||
if (empty($order)) {
|
||||
return;
|
||||
}
|
||||
$user = WxUserModel::where('id', (int) $order['user_id'])->first(['open_id']);
|
||||
$openId = (string) ($user['open_id'] ?? '');
|
||||
if ($openId === '') {
|
||||
return;
|
||||
}
|
||||
$app = \App\Service\wx\WxAppService::getInstance()->current();
|
||||
$tplKey = $scene === 'ship' ? 'subscribe_ship_tpl' : 'subscribe_pay_tpl';
|
||||
$templateId = trim((string) ($app[$tplKey] ?? ''));
|
||||
if ($templateId === '') {
|
||||
return;
|
||||
}
|
||||
$price = PriceService::getInstance();
|
||||
$page = 'pages/order/detail?id=' . $orderId;
|
||||
$data = $scene === 'ship'
|
||||
? [
|
||||
'character_string1' => ['value' => (string) $order['order_no']],
|
||||
'thing2' => ['value' => '订单已发货'],
|
||||
'time3' => ['value' => date('Y-m-d H:i')],
|
||||
]
|
||||
: [
|
||||
'character_string1' => ['value' => (string) $order['order_no']],
|
||||
'amount2' => ['value' => $price->centsToYuan((int) $order['total_amount'])],
|
||||
'thing3' => ['value' => '付款成功'],
|
||||
];
|
||||
\App\Service\wx\WxMiniService::getInstance()->sendSubscribe($openId, $templateId, $page, $data);
|
||||
} catch (\Throwable) {
|
||||
// 订阅消息是附加能力,模板字段对不上或用户未授权都不能挡住收款/发货
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Service\business;
|
||||
|
||||
use App\BaseApp\BaseService;
|
||||
use App\Models\business\ListModel;
|
||||
use App\Models\business\OrderModel;
|
||||
use App\Models\business\OrderPaymentModel;
|
||||
use App\Models\business\WxUserModel;
|
||||
@@ -188,6 +189,15 @@ class OrderService extends BaseService
|
||||
$stat['auditing'] = OrderPaymentModel::where('deleted_at', 0)
|
||||
->where('status', OrderPaymentModel::STATUS_AUDITING)
|
||||
->count();
|
||||
$stat['list_count'] = ListModel::where('deleted_at', 0)->count();
|
||||
$stat['wx_user_count'] = WxUserModel::where('deleted_at', 0)->count();
|
||||
$statusMap = [];
|
||||
foreach ($stat['status'] as $row) {
|
||||
$statusMap[(int) $row['status']] = (int) $row['count'];
|
||||
}
|
||||
$stat['unpaid'] = $statusMap[OrderModel::STATUS_UNPAID] ?? 0;
|
||||
$stat['paid'] = $statusMap[OrderModel::STATUS_PAID] ?? 0;
|
||||
$stat['shipped'] = $statusMap[OrderModel::STATUS_SHIPPED] ?? 0;
|
||||
return $stat;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ class PriceSheetService extends BaseService
|
||||
$this->selectField = array_merge(
|
||||
['id', 'catalogue_id', 'specification', 'dimension'],
|
||||
PriceSheetModel::MATERIAL_FIELDS,
|
||||
['status', 'created_at', 'updated_at']
|
||||
['stock', 'status', 'created_at', 'updated_at']
|
||||
);
|
||||
$this->queryField = ['catalogue_id' => '=', 'specification' => 'like', 'status' => '='];
|
||||
$this->orderBy = ['name' => 'id', 'sort' => 'asc'];
|
||||
@@ -195,6 +195,9 @@ class PriceSheetService extends BaseService
|
||||
$payload[$field] = is_scalar($row[$field]) ? (string) $row[$field] : '';
|
||||
}
|
||||
}
|
||||
if (array_key_exists('stock', $row)) {
|
||||
$payload['stock'] = (int) $row['stock'];
|
||||
}
|
||||
return $payload;
|
||||
}
|
||||
|
||||
|
||||
67
app/Service/business/StockService.php
Normal file
67
app/Service/business/StockService.php
Normal file
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\business;
|
||||
|
||||
use App\Models\business\PriceSheetModel;
|
||||
use App\Service\common\UtilsService;
|
||||
|
||||
/**
|
||||
* 规格库存:stock=-1 表示不限,兼容历史数据全是不限。
|
||||
*/
|
||||
class StockService
|
||||
{
|
||||
private static mixed $_instance;
|
||||
|
||||
public static function getInstance(): null|static
|
||||
{
|
||||
$name = get_called_class();
|
||||
if (!isset(self::$_instance[$name])) {
|
||||
self::$_instance[$name] = new static();
|
||||
}
|
||||
return self::$_instance[$name];
|
||||
}
|
||||
|
||||
/**
|
||||
* 加入清单 / 下单前校验。sheetId=0 时跳过(还没选规格)。
|
||||
*/
|
||||
public function assertAvailable(int $sheetId, int $quantity): void
|
||||
{
|
||||
if ($sheetId <= 0) {
|
||||
return;
|
||||
}
|
||||
$sheet = PriceSheetModel::where('id', $sheetId)->where('deleted_at', 0)->first();
|
||||
if (empty($sheet)) {
|
||||
UtilsService::getInstance()->errorThrow('规格不存在');
|
||||
}
|
||||
$stock = (int) ($sheet['stock'] ?? -1);
|
||||
if ($stock < 0) {
|
||||
return;
|
||||
}
|
||||
if ($stock < $quantity) {
|
||||
UtilsService::getInstance()->errorThrow('库存不足(剩余 ' . $stock . ')');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 下单成功后扣库存;不限库存不写库。
|
||||
*/
|
||||
public function deduct(int $sheetId, int $quantity): void
|
||||
{
|
||||
if ($sheetId <= 0 || $quantity <= 0) {
|
||||
return;
|
||||
}
|
||||
$sheet = PriceSheetModel::where('id', $sheetId)->where('deleted_at', 0)->first();
|
||||
if (empty($sheet)) {
|
||||
return;
|
||||
}
|
||||
$stock = (int) ($sheet['stock'] ?? -1);
|
||||
if ($stock < 0) {
|
||||
return;
|
||||
}
|
||||
$next = max(0, $stock - $quantity);
|
||||
PriceSheetModel::where('id', $sheetId)->update([
|
||||
'stock' => $next,
|
||||
'updated_at' => time(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -56,6 +56,9 @@ class WxTemplateSchemaService
|
||||
'header' => ['gradient', 'image', 'plain', 'split', 'editorial'],
|
||||
'menu' => ['grid', 'list', 'card', 'tile', 'compact'],
|
||||
],
|
||||
'package' => [
|
||||
'list' => ['card', 'featured', 'magazine', 'list'],
|
||||
],
|
||||
'effect' => [
|
||||
'transition' => ['fade', 'slide', 'zoom', 'none'],
|
||||
'skeleton' => ['shimmer', 'pulse', 'none'],
|
||||
@@ -70,7 +73,7 @@ class WxTemplateSchemaService
|
||||
*/
|
||||
public const PAGE_SECTION_TYPES = [
|
||||
'home' => ['search', 'hero', 'category', 'package', 'product'],
|
||||
'package' => ['list'],
|
||||
'package' => ['search', 'list'],
|
||||
'catalog' => ['filter', 'list'],
|
||||
'search' => ['search', 'list'],
|
||||
'product' => ['gallery', 'info', 'spec', 'price', 'action'],
|
||||
@@ -90,7 +93,7 @@ class WxTemplateSchemaService
|
||||
'filter' => ['chip', 'bar', 'sidebar', 'hidden'],
|
||||
'list' => [
|
||||
'card', 'table', 'timeline', 'waterfall', 'grid', 'list',
|
||||
'masonry', 'featured', 'shelf', 'compact', 'airy', 'mosaic', 'duo',
|
||||
'masonry', 'featured', 'magazine', 'shelf', 'compact', 'airy', 'mosaic', 'duo',
|
||||
'ticket', 'stacked',
|
||||
],
|
||||
'gallery' => ['swiper', 'stack', 'fullbleed', 'peek', 'fade', 'coverflow', 'mosaic', 'filmstrip'],
|
||||
@@ -111,6 +114,7 @@ class WxTemplateSchemaService
|
||||
'package' => ['card', 'scroll', 'featured', 'magazine'],
|
||||
],
|
||||
'package' => [
|
||||
'search' => ['bar', 'overlay', 'pill', 'float'],
|
||||
'list' => ['card', 'featured', 'magazine', 'list'],
|
||||
],
|
||||
'catalog' => [
|
||||
@@ -198,6 +202,7 @@ class WxTemplateSchemaService
|
||||
'mosaic' => '马赛克',
|
||||
'duo' => '对开',
|
||||
'card' => '卡片',
|
||||
'magazine' => '刊名条',
|
||||
'table' => '表格',
|
||||
'timeline' => '时间线',
|
||||
'ticket' => '票根',
|
||||
|
||||
142
app/Service/common/ExcelCsvService.php
Normal file
142
app/Service/common/ExcelCsvService.php
Normal file
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\common;
|
||||
|
||||
/**
|
||||
* 表格导出辅助:报价给结构化列/行(前端 ExcelJS 画 xlsx),小程序继续用 text。
|
||||
* 仍保留 toCsv / parseUpload,避免旧调用立刻断掉。
|
||||
*/
|
||||
class ExcelCsvService
|
||||
{
|
||||
private static mixed $_instance;
|
||||
|
||||
public static function getInstance(): null|static
|
||||
{
|
||||
$name = get_called_class();
|
||||
if (!isset(self::$_instance[$name])) {
|
||||
self::$_instance[$name] = new static();
|
||||
}
|
||||
return self::$_instance[$name];
|
||||
}
|
||||
|
||||
/**
|
||||
* 行数组转带 BOM 的 CSV 文本
|
||||
*/
|
||||
public function toCsv(array $header, array $rows): string
|
||||
{
|
||||
$fp = fopen('php://temp', 'r+');
|
||||
fprintf($fp, chr(0xEF) . chr(0xBB) . chr(0xBF));
|
||||
fputcsv($fp, $header);
|
||||
foreach ($rows as $row) {
|
||||
$line = [];
|
||||
foreach ($header as $key) {
|
||||
$line[] = $row[$key] ?? '';
|
||||
}
|
||||
fputcsv($fp, $line);
|
||||
}
|
||||
rewind($fp);
|
||||
$csv = stream_get_contents($fp);
|
||||
fclose($fp);
|
||||
return $csv === false ? '' : $csv;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析上传文件(csv / 文本)
|
||||
*/
|
||||
public function parseUpload($file): array
|
||||
{
|
||||
if (empty($file)) {
|
||||
UtilsService::getInstance()->errorThrow('请上传表格文件');
|
||||
}
|
||||
$path = is_string($file) ? $file : $file->getRealPath();
|
||||
$raw = file_get_contents($path);
|
||||
if ($raw === false) {
|
||||
UtilsService::getInstance()->errorThrow('文件读取失败');
|
||||
}
|
||||
if (str_starts_with($raw, "\xEF\xBB\xBF")) {
|
||||
$raw = substr($raw, 3);
|
||||
}
|
||||
$fp = fopen('php://temp', 'r+');
|
||||
fwrite($fp, $raw);
|
||||
rewind($fp);
|
||||
$header = fgetcsv($fp);
|
||||
if (empty($header)) {
|
||||
fclose($fp);
|
||||
UtilsService::getInstance()->errorThrow('表格没有表头');
|
||||
}
|
||||
$header = array_map(static fn ($v) => trim((string) $v), $header);
|
||||
$rows = [];
|
||||
while (($cols = fgetcsv($fp)) !== false) {
|
||||
if ($this->rowEmpty($cols)) {
|
||||
continue;
|
||||
}
|
||||
$item = [];
|
||||
foreach ($header as $i => $key) {
|
||||
$item[$key] = isset($cols[$i]) ? trim((string) $cols[$i]) : '';
|
||||
}
|
||||
$rows[] = $item;
|
||||
}
|
||||
fclose($fp);
|
||||
return $rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清单报价:后台用 ExcelJS 画 xlsx,小程序继续用 text 分享
|
||||
*/
|
||||
public function quoteFromList(array $info): array
|
||||
{
|
||||
$rows = [];
|
||||
$lines = [];
|
||||
$name = (string) ($info['name'] ?? $info['list_no'] ?? '报价');
|
||||
$lines[] = $name;
|
||||
foreach ($info['items'] ?? [] as $item) {
|
||||
$title = (string) ($item['title'] ?? $item['catalogue']['title'] ?? $item['product']['title'] ?? '');
|
||||
$spec = (string) ($item['specification'] ?? $item['price_sheet']['specification'] ?? '');
|
||||
$qty = (int) ($item['quantity'] ?? 1);
|
||||
$unit = (string) ($item['unit_price_text'] ?? '');
|
||||
$total = (string) ($item['total_price_text'] ?? '');
|
||||
$rows[] = [
|
||||
'title' => $title,
|
||||
'specification' => $spec,
|
||||
'quantity' => $qty,
|
||||
'unit_price' => $unit,
|
||||
'total_price' => $total,
|
||||
];
|
||||
$lines[] = trim($title . ' ' . $spec . ' x' . $qty . ($unit !== '' ? ' ¥' . $unit : ''));
|
||||
}
|
||||
$payable = (string) ($info['payable_amount_text'] ?? $info['total_amount_text'] ?? '');
|
||||
if ($payable !== '') {
|
||||
$lines[] = '合计 ¥' . $payable;
|
||||
}
|
||||
$no = (string) ($info['list_no'] ?? 'quote');
|
||||
return [
|
||||
'filename' => 'quote-' . $no . '.xlsx',
|
||||
'title' => $name,
|
||||
'sheet' => '报价',
|
||||
'subtitle' => '清单号 ' . $no,
|
||||
'columns' => [
|
||||
['key' => 'title', 'header' => '商品', 'width' => 28],
|
||||
['key' => 'specification', 'header' => '规格', 'width' => 22],
|
||||
['key' => 'quantity', 'header' => '数量', 'width' => 10, 'align' => 'center'],
|
||||
['key' => 'unit_price', 'header' => '单价', 'width' => 14, 'align' => 'right'],
|
||||
['key' => 'total_price', 'header' => '小计', 'width' => 14, 'align' => 'right'],
|
||||
],
|
||||
'rows' => $rows,
|
||||
'summary' => [
|
||||
'title' => '合计',
|
||||
'total_price' => $payable === '' ? '' : ('¥' . $payable),
|
||||
],
|
||||
'text' => implode("\n", $lines),
|
||||
];
|
||||
}
|
||||
|
||||
private function rowEmpty(array $cols): bool
|
||||
{
|
||||
foreach ($cols as $col) {
|
||||
if (trim((string) $col) !== '') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
117
app/Service/wx/WxAddressService.php
Normal file
117
app/Service/wx/WxAddressService.php
Normal file
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\wx;
|
||||
|
||||
use App\BaseApp\BaseWxService;
|
||||
use App\Models\business\WxAddressModel;
|
||||
|
||||
/**
|
||||
* 小程序收货地址
|
||||
*/
|
||||
class WxAddressService extends BaseWxService
|
||||
{
|
||||
public function list(): array
|
||||
{
|
||||
return WxAddressModel::where('user_id', $this->userId)
|
||||
->where('deleted_at', 0)
|
||||
->orderByDesc('is_default')
|
||||
->orderByDesc('id')
|
||||
->get()
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认地址,下单表单回填
|
||||
*/
|
||||
public function defaultOne(): array
|
||||
{
|
||||
$row = WxAddressModel::where('user_id', $this->userId)
|
||||
->where('deleted_at', 0)
|
||||
->orderByDesc('is_default')
|
||||
->orderByDesc('id')
|
||||
->first();
|
||||
return $row ? $row->toArray() : [];
|
||||
}
|
||||
|
||||
public function create(array $params): array
|
||||
{
|
||||
$payload = $this->pick($params);
|
||||
$now = time();
|
||||
if ((int) ($payload['is_default'] ?? 0) === 1) {
|
||||
$this->clearDefault();
|
||||
}
|
||||
$id = (int) WxAddressModel::insertGetId(array_merge($payload, [
|
||||
'user_id' => $this->userId,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]));
|
||||
return WxAddressModel::where('id', $id)->first()->toArray();
|
||||
}
|
||||
|
||||
public function update($id, $params): mixed
|
||||
{
|
||||
$row = $this->owned((int) $id);
|
||||
$payload = $this->pick($params);
|
||||
if ((int) ($payload['is_default'] ?? 0) === 1) {
|
||||
$this->clearDefault();
|
||||
}
|
||||
$payload['updated_at'] = time();
|
||||
WxAddressModel::where('id', $row['id'])->update($payload);
|
||||
return WxAddressModel::where('id', $row['id'])->first()->toArray();
|
||||
}
|
||||
|
||||
public function delete($ids): mixed
|
||||
{
|
||||
$id = (int) (is_array($ids) ? ($ids[0] ?? 0) : $ids);
|
||||
$row = $this->owned($id);
|
||||
WxAddressModel::where('id', $row['id'])->update(['deleted_at' => time(), 'updated_at' => time()]);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设为默认
|
||||
*/
|
||||
public function detail($id): mixed
|
||||
{
|
||||
return $this->owned((int) $id);
|
||||
}
|
||||
|
||||
public function setDefault(int $id): array
|
||||
{
|
||||
$row = $this->owned($id);
|
||||
$this->clearDefault();
|
||||
WxAddressModel::where('id', $row['id'])->update(['is_default' => 1, 'updated_at' => time()]);
|
||||
return WxAddressModel::where('id', $row['id'])->first()->toArray();
|
||||
}
|
||||
|
||||
private function owned(int $id): array
|
||||
{
|
||||
$row = WxAddressModel::where('id', $id)->where('user_id', $this->userId)->where('deleted_at', 0)->first();
|
||||
if (empty($row)) {
|
||||
$this->utils->errorThrow('地址不存在');
|
||||
}
|
||||
return $row->toArray();
|
||||
}
|
||||
|
||||
private function clearDefault(): void
|
||||
{
|
||||
WxAddressModel::where('user_id', $this->userId)->where('deleted_at', 0)
|
||||
->update(['is_default' => 0, 'updated_at' => time()]);
|
||||
}
|
||||
|
||||
private function pick(array $params): array
|
||||
{
|
||||
$name = trim((string) ($params['name'] ?? ''));
|
||||
$phone = trim((string) ($params['phone'] ?? ''));
|
||||
$address = trim((string) ($params['address'] ?? ''));
|
||||
if ($name === '' || $phone === '' || $address === '') {
|
||||
$this->utils->errorThrow('请填写姓名、手机和地址');
|
||||
}
|
||||
return [
|
||||
'name' => $name,
|
||||
'phone' => $phone,
|
||||
'address' => $address,
|
||||
'is_default' => (int) ($params['is_default'] ?? 0) === 1 ? 1 : 0,
|
||||
];
|
||||
}
|
||||
}
|
||||
82
app/Service/wx/WxAfterSaleService.php
Normal file
82
app/Service/wx/WxAfterSaleService.php
Normal file
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\wx;
|
||||
|
||||
use App\BaseApp\BaseWxService;
|
||||
use App\Models\business\AfterSaleModel;
|
||||
use App\Models\business\OrderModel;
|
||||
use App\Service\business\PriceService;
|
||||
|
||||
/**
|
||||
* 小程序售后:已付款/已发货/已完成可申请,后台审核,不自动退微信款。
|
||||
*/
|
||||
class WxAfterSaleService extends BaseWxService
|
||||
{
|
||||
public function list(): array
|
||||
{
|
||||
$price = PriceService::getInstance();
|
||||
$rows = AfterSaleModel::with('order')
|
||||
->where('user_id', $this->userId)
|
||||
->where('deleted_at', 0)
|
||||
->orderByDesc('id')
|
||||
->get()
|
||||
->toArray();
|
||||
foreach ($rows as &$row) {
|
||||
$row['amount_text'] = $price->centsToYuan((int) ($row['amount'] ?? 0));
|
||||
$row['order_no'] = $row['order']['order_no'] ?? '';
|
||||
$row['status_text'] = match ((int) ($row['status'] ?? 0)) {
|
||||
AfterSaleModel::STATUS_APPROVED => '已通过',
|
||||
AfterSaleModel::STATUS_REJECTED => '已拒绝',
|
||||
default => '待审核',
|
||||
};
|
||||
unset($row['order']);
|
||||
}
|
||||
unset($row);
|
||||
return $rows;
|
||||
}
|
||||
|
||||
public function create(array $params): array
|
||||
{
|
||||
$orderId = (int) ($params['order_id'] ?? 0);
|
||||
$reason = trim((string) ($params['reason'] ?? ''));
|
||||
if ($orderId <= 0 || $reason === '') {
|
||||
$this->utils->errorThrow('请填写订单和原因');
|
||||
}
|
||||
$order = OrderModel::where('id', $orderId)->where('user_id', $this->userId)->where('deleted_at', 0)->first();
|
||||
if (empty($order)) {
|
||||
$this->utils->errorThrow('订单不存在');
|
||||
}
|
||||
$status = (int) $order['status'];
|
||||
if (!in_array($status, [OrderModel::STATUS_PAID, OrderModel::STATUS_SHIPPED, OrderModel::STATUS_DONE], true)) {
|
||||
$this->utils->errorThrow('当前订单状态不能申请售后');
|
||||
}
|
||||
$pending = AfterSaleModel::where('order_id', $orderId)
|
||||
->where('deleted_at', 0)
|
||||
->where('status', AfterSaleModel::STATUS_PENDING)
|
||||
->exists();
|
||||
if ($pending) {
|
||||
$this->utils->errorThrow('已有待审核的售后申请');
|
||||
}
|
||||
$amount = (int) ($params['amount'] ?? 0);
|
||||
if ($amount <= 0) {
|
||||
$amount = (int) $order['paid_amount'];
|
||||
}
|
||||
if ($amount > (int) $order['paid_amount']) {
|
||||
$this->utils->errorThrow('退款金额不能超过已付金额');
|
||||
}
|
||||
$now = time();
|
||||
$id = (int) AfterSaleModel::insertGetId([
|
||||
'order_id' => $orderId,
|
||||
'user_id' => $this->userId,
|
||||
'reason' => mb_substr($reason, 0, 255),
|
||||
'amount' => $amount,
|
||||
'images' => is_array($params['images'] ?? null)
|
||||
? implode(',', $params['images'])
|
||||
: (string) ($params['images'] ?? ''),
|
||||
'status' => AfterSaleModel::STATUS_PENDING,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
return AfterSaleModel::where('id', $id)->first()->toArray();
|
||||
}
|
||||
}
|
||||
76
app/Service/wx/WxColorcardService.php
Normal file
76
app/Service/wx/WxColorcardService.php
Normal file
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\wx;
|
||||
|
||||
use App\BaseApp\BaseWxService;
|
||||
use App\Models\business\CardClassModel;
|
||||
use App\Models\business\ColorcardModel;
|
||||
use App\Models\business\CompanyModel;
|
||||
use App\Service\common\MediaUrlService;
|
||||
|
||||
/**
|
||||
* 小程序色卡目录(只读)
|
||||
*/
|
||||
class WxColorcardService extends BaseWxService
|
||||
{
|
||||
protected bool $needLogin = false;
|
||||
|
||||
public function list(): array
|
||||
{
|
||||
$classId = (int) request()->get('card_class', 0);
|
||||
$companyId = (int) request()->get('company', 0);
|
||||
$keyword = trim((string) request()->get('keyword', ''));
|
||||
$query = ColorcardModel::with(['cardClassInfo', 'companyInfo'])
|
||||
->where('deleted_at', 0)
|
||||
->where('status', 0)
|
||||
->orderByDesc('id');
|
||||
if ($classId > 0) {
|
||||
$query->where('card_class', $classId);
|
||||
}
|
||||
if ($companyId > 0) {
|
||||
$query->where('company', $companyId);
|
||||
}
|
||||
if ($keyword !== '') {
|
||||
$query->where('description', 'like', '%' . $keyword . '%');
|
||||
}
|
||||
$rows = $query->get()->toArray();
|
||||
$media = MediaUrlService::getInstance();
|
||||
foreach ($rows as &$row) {
|
||||
$row['cover'] = $media->toPublic($row['cover'] ?? '');
|
||||
$row['card_class_name'] = $row['card_class_info']['name'] ?? '';
|
||||
$row['company_name'] = $row['company_info']['name'] ?? '';
|
||||
unset($row['card_class_info'], $row['company_info']);
|
||||
}
|
||||
unset($row);
|
||||
return $rows;
|
||||
}
|
||||
|
||||
public function detail(int $id): array
|
||||
{
|
||||
$info = ColorcardModel::with(['cardClassInfo', 'companyInfo'])
|
||||
->where('id', $id)
|
||||
->where('deleted_at', 0)
|
||||
->where('status', 0)
|
||||
->first();
|
||||
if (empty($info)) {
|
||||
$this->utils->errorThrow('色卡不存在');
|
||||
}
|
||||
$info = $info->toArray();
|
||||
$media = MediaUrlService::getInstance();
|
||||
$info['cover'] = $media->toPublic($info['cover'] ?? '');
|
||||
$info['card_class_name'] = $info['card_class_info']['name'] ?? '';
|
||||
$info['company_name'] = $info['company_info']['name'] ?? '';
|
||||
unset($info['card_class_info'], $info['company_info']);
|
||||
return $info;
|
||||
}
|
||||
|
||||
public function classList(): array
|
||||
{
|
||||
return CardClassModel::where('deleted_at', 0)->orderBy('id')->get(['id', 'name'])->toArray();
|
||||
}
|
||||
|
||||
public function companyList(): array
|
||||
{
|
||||
return CompanyModel::where('deleted_at', 0)->orderBy('id')->get(['id', 'name'])->toArray();
|
||||
}
|
||||
}
|
||||
74
app/Service/wx/WxFactoryService.php
Normal file
74
app/Service/wx/WxFactoryService.php
Normal file
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\wx;
|
||||
|
||||
use App\BaseApp\BaseWxService;
|
||||
use App\Models\business\FactoryClassificationModel;
|
||||
use App\Models\business\FactoryImageModel;
|
||||
use App\Models\business\FactoryInfoModel;
|
||||
use App\Service\common\MediaUrlService;
|
||||
|
||||
/**
|
||||
* 小程序工厂目录(只读)
|
||||
*/
|
||||
class WxFactoryService extends BaseWxService
|
||||
{
|
||||
protected bool $needLogin = false;
|
||||
|
||||
public function list(): array
|
||||
{
|
||||
$classId = (int) request()->get('classification', 0);
|
||||
$keyword = trim((string) request()->get('keyword', ''));
|
||||
$query = FactoryInfoModel::with('classificationInfo')
|
||||
->where('deleted_at', 0)
|
||||
->where('status', 0)
|
||||
->orderByDesc('id');
|
||||
if ($classId > 0) {
|
||||
$query->where('classification', $classId);
|
||||
}
|
||||
if ($keyword !== '') {
|
||||
$query->where('name', 'like', '%' . $keyword . '%');
|
||||
}
|
||||
$rows = $query->get()->toArray();
|
||||
$media = MediaUrlService::getInstance();
|
||||
foreach ($rows as &$row) {
|
||||
$row['cover'] = $media->toPublic($row['cover'] ?? '');
|
||||
$row['classification_name'] = $row['classification_info']['name'] ?? '';
|
||||
unset($row['classification_info']);
|
||||
}
|
||||
unset($row);
|
||||
return $rows;
|
||||
}
|
||||
|
||||
public function detail(int $id): array
|
||||
{
|
||||
$info = FactoryInfoModel::with('classificationInfo')
|
||||
->where('id', $id)
|
||||
->where('deleted_at', 0)
|
||||
->where('status', 0)
|
||||
->first();
|
||||
if (empty($info)) {
|
||||
$this->utils->errorThrow('工厂不存在');
|
||||
}
|
||||
$info = $info->toArray();
|
||||
$media = MediaUrlService::getInstance();
|
||||
$info['cover'] = $media->toPublic($info['cover'] ?? '');
|
||||
$info['classification_name'] = $info['classification_info']['name'] ?? '';
|
||||
unset($info['classification_info']);
|
||||
$images = FactoryImageModel::where('factory', $id)->where('deleted_at', 0)->orderBy('id')->get()->toArray();
|
||||
$media->publicEach($images, ['url']);
|
||||
$info['images'] = $images;
|
||||
return $info;
|
||||
}
|
||||
|
||||
/**
|
||||
* 工厂分类
|
||||
*/
|
||||
public function classList(): array
|
||||
{
|
||||
return FactoryClassificationModel::where('deleted_at', 0)
|
||||
->orderBy('id')
|
||||
->get(['id', 'name'])
|
||||
->toArray();
|
||||
}
|
||||
}
|
||||
73
app/Service/wx/WxFavoriteService.php
Normal file
73
app/Service/wx/WxFavoriteService.php
Normal file
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\wx;
|
||||
|
||||
use App\BaseApp\BaseWxService;
|
||||
use App\Models\business\CatalogueModel;
|
||||
use App\Models\business\FavoriteModel;
|
||||
use App\Service\common\MediaUrlService;
|
||||
|
||||
/**
|
||||
* 商品收藏
|
||||
*/
|
||||
class WxFavoriteService extends BaseWxService
|
||||
{
|
||||
public function list(): array
|
||||
{
|
||||
$rows = FavoriteModel::with('catalogue')
|
||||
->where('user_id', $this->userId)
|
||||
->where('deleted_at', 0)
|
||||
->orderByDesc('id')
|
||||
->get()
|
||||
->toArray();
|
||||
$media = MediaUrlService::getInstance();
|
||||
foreach ($rows as &$row) {
|
||||
$cat = $row['catalogue'] ?? [];
|
||||
$row['title'] = $cat['title'] ?? '';
|
||||
$row['cover'] = $media->toPublic($cat['cover'] ?? '');
|
||||
$row['price'] = $cat['price'] ?? '';
|
||||
}
|
||||
unset($row);
|
||||
return $rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换收藏,返回当前是否已收藏
|
||||
*/
|
||||
public function toggle(int $catalogueId): array
|
||||
{
|
||||
if ($catalogueId <= 0) {
|
||||
$this->utils->errorThrow('参数错误');
|
||||
}
|
||||
$exists = CatalogueModel::where('id', $catalogueId)->where('deleted_at', 0)->exists();
|
||||
if (!$exists) {
|
||||
$this->utils->errorThrow('商品不存在');
|
||||
}
|
||||
$row = FavoriteModel::where('user_id', $this->userId)
|
||||
->where('catalogue_id', $catalogueId)
|
||||
->where('deleted_at', 0)
|
||||
->first();
|
||||
if (!empty($row)) {
|
||||
FavoriteModel::where('id', $row['id'])->update(['deleted_at' => time(), 'updated_at' => time()]);
|
||||
return ['favorited' => false];
|
||||
}
|
||||
FavoriteModel::insert([
|
||||
'user_id' => $this->userId,
|
||||
'catalogue_id' => $catalogueId,
|
||||
'created_at' => time(),
|
||||
'updated_at' => time(),
|
||||
]);
|
||||
return ['favorited' => true];
|
||||
}
|
||||
|
||||
public function isFavorited(int $catalogueId): bool
|
||||
{
|
||||
if ($this->userId <= 0 || $catalogueId <= 0) {
|
||||
return false;
|
||||
}
|
||||
return FavoriteModel::where('user_id', $this->userId)
|
||||
->where('catalogue_id', $catalogueId)
|
||||
->where('deleted_at', 0)
|
||||
->exists();
|
||||
}
|
||||
}
|
||||
41
app/Service/wx/WxFeedbackService.php
Normal file
41
app/Service/wx/WxFeedbackService.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\wx;
|
||||
|
||||
use App\BaseApp\BaseWxService;
|
||||
use App\Models\business\FeedbackModel;
|
||||
|
||||
/**
|
||||
* 小程序用户反馈
|
||||
*/
|
||||
class WxFeedbackService extends BaseWxService
|
||||
{
|
||||
public function list(): array
|
||||
{
|
||||
return FeedbackModel::where('user_id', $this->userId)
|
||||
->where('deleted_at', 0)
|
||||
->orderByDesc('id')
|
||||
->get()
|
||||
->toArray();
|
||||
}
|
||||
|
||||
public function create(array $params): array
|
||||
{
|
||||
$content = trim((string) ($params['content'] ?? ''));
|
||||
if ($content === '') {
|
||||
$this->utils->errorThrow('请填写反馈内容');
|
||||
}
|
||||
$now = time();
|
||||
$id = (int) FeedbackModel::insertGetId([
|
||||
'user_id' => $this->userId,
|
||||
'content' => mb_substr($content, 0, 500),
|
||||
'images' => is_array($params['images'] ?? null)
|
||||
? implode(',', $params['images'])
|
||||
: (string) ($params['images'] ?? ''),
|
||||
'status' => FeedbackModel::STATUS_PENDING,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
return FeedbackModel::where('id', $id)->first()->toArray();
|
||||
}
|
||||
}
|
||||
@@ -191,6 +191,7 @@ class WxListService extends BaseWxService
|
||||
}
|
||||
$priceSheetId = (int) ($params['price_sheet_id'] ?? 0);
|
||||
$quantity = max(1, (int) ($params['quantity'] ?? 1));
|
||||
\App\Service\business\StockService::getInstance()->assertAvailable($priceSheetId, $quantity);
|
||||
|
||||
$exists = ListItemModel::where('list_id', $listId)
|
||||
->where('catalogue_id', $catalogueId)
|
||||
@@ -242,6 +243,8 @@ class WxListService extends BaseWxService
|
||||
$this->utils->errorThrow('明细不存在');
|
||||
}
|
||||
$sheetId = (int) ($update['price_sheet_id'] ?? $item['price_sheet_id']);
|
||||
$qty = (int) ($update['quantity'] ?? $item['quantity']);
|
||||
\App\Service\business\StockService::getInstance()->assertAvailable($sheetId, $qty);
|
||||
$material = (string) ($update['material_key'] ?? $item['material_key'] ?? 'routine');
|
||||
if ($sheetId > 0) {
|
||||
$sheet = PriceSheetModel::where('id', $sheetId)->where('deleted_at', 0)->first();
|
||||
@@ -255,4 +258,13 @@ class WxListService extends BaseWxService
|
||||
}
|
||||
return ListItemModel::where('id', $itemId)->whereIn('list_id', $listIds)->update($update);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出本人清单报价,给分享/下载用
|
||||
*/
|
||||
public function exportQuote(int $id): array
|
||||
{
|
||||
$info = $this->detail($id);
|
||||
return \App\Service\common\ExcelCsvService::getInstance()->quoteFromList($info);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,4 +91,32 @@ class WxMiniService
|
||||
$redis->set($app['app_id'], $result['access_token'], max(60, (int) ($result['expires_in'] ?? 7200) - 300));
|
||||
return (string) $result['access_token'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送订阅消息。模板字段必须与公众平台里的关键词一致,对不上微信会拒收。
|
||||
* 这里只尽力发送,失败不抛错,避免挡住收款/发货。
|
||||
*/
|
||||
public function sendSubscribe(string $openId, string $templateId, string $page, array $data): bool
|
||||
{
|
||||
if ($openId === '' || $templateId === '') {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
$token = $this->accessToken();
|
||||
$result = Http::asJson()->post(
|
||||
$this->baseUrl . 'cgi-bin/message/subscribe/send?access_token=' . $token,
|
||||
[
|
||||
'touser' => $openId,
|
||||
'template_id' => $templateId,
|
||||
'page' => $page,
|
||||
'data' => $data,
|
||||
'miniprogram_state' => 'formal',
|
||||
'lang' => 'zh_CN',
|
||||
]
|
||||
)->json();
|
||||
return empty($result['errcode']);
|
||||
} catch (\Throwable) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,6 +140,13 @@ class WxPackageService extends BaseWxService
|
||||
if (empty($items)) {
|
||||
$this->utils->errorThrow('套餐还没有搭配商品');
|
||||
}
|
||||
$stock = \App\Service\business\StockService::getInstance();
|
||||
foreach ($items as $item) {
|
||||
$stock->assertAvailable(
|
||||
(int) ($item['price_sheet_id'] ?? 0),
|
||||
max(1, (int) ($item['quantity'] ?? 1))
|
||||
);
|
||||
}
|
||||
|
||||
$listId = (int) ($params['list_id'] ?? 0);
|
||||
$list = null;
|
||||
|
||||
@@ -7,6 +7,7 @@ use App\Models\business\CatalogueModel;
|
||||
use App\Models\business\CategoryModel;
|
||||
use App\Models\business\ImageModel;
|
||||
use App\Service\business\PriceService;
|
||||
use App\Service\common\MediaUrlService;
|
||||
|
||||
/**
|
||||
* 小程序商品列表与详情
|
||||
@@ -16,6 +17,8 @@ use App\Service\business\PriceService;
|
||||
*/
|
||||
class WxProductService extends BaseWxService
|
||||
{
|
||||
protected bool $needLogin = false;
|
||||
|
||||
/**
|
||||
* 商品列表:分类点到二级时按父分类下的所有子分类查
|
||||
*/
|
||||
@@ -76,12 +79,15 @@ class WxProductService extends BaseWxService
|
||||
$this->utils->errorThrow('商品不存在');
|
||||
}
|
||||
$info = $info->toArray();
|
||||
$media = MediaUrlService::getInstance();
|
||||
$info['cover'] = $media->toPublic($info['cover'] ?? '');
|
||||
$info['video'] = $media->toPublic($info['video'] ?? '');
|
||||
|
||||
// 渲染图作为详情轮播
|
||||
$info['carousel'] = [];
|
||||
foreach ($info['images'] ?? [] as $image) {
|
||||
if ((int) $image['type'] === ImageModel::TYPE_RENDER) {
|
||||
$info['carousel'][] = $image['url'];
|
||||
$info['carousel'][] = $media->toPublic($image['url'] ?? '');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,6 +95,9 @@ class WxProductService extends BaseWxService
|
||||
$applied = PriceService::getInstance()->applyToRows($info['price_sheet'] ?? [], $showPrice, $this->priceMultiplier());
|
||||
$info['price_sheet'] = $applied['rows'];
|
||||
$info['is_show_price'] = $applied['is_show_price'];
|
||||
$info['favorited'] = $this->userId > 0
|
||||
? WxFavoriteService::getInstance()->isFavorited((int) $id)
|
||||
: false;
|
||||
return $info;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,6 +120,8 @@ class WxThemeService extends BaseWxService
|
||||
'card_schemes' => WxCardSchemeService::mapByCodes($this->collectSchemeCodes($layout)),
|
||||
'features' => [
|
||||
'package_enabled' => WxAppService::getInstance()->packageEnabled(),
|
||||
'subscribe_pay_tpl' => $this->subscribeTpl('subscribe_pay_tpl'),
|
||||
'subscribe_ship_tpl' => $this->subscribeTpl('subscribe_ship_tpl'),
|
||||
],
|
||||
'fallback' => $fallback,
|
||||
];
|
||||
@@ -163,4 +165,17 @@ class WxThemeService extends BaseWxService
|
||||
->orderBy('sort', 'asc')
|
||||
->get(['code', 'name', 'style_tag', 'preview']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前品牌的订阅模板 ID,给小程序 requestSubscribeMessage 用
|
||||
*/
|
||||
private function subscribeTpl(string $field): string
|
||||
{
|
||||
try {
|
||||
$app = WxAppService::getInstance()->current();
|
||||
return trim((string) ($app[$field] ?? ''));
|
||||
} catch (\Throwable) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,4 +64,19 @@ class WxUserCenterService extends BaseWxService
|
||||
]);
|
||||
return ['nick_name' => $nickName];
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前用户绑定的企业资料,给企业详情页
|
||||
*/
|
||||
public function enterprise(): array
|
||||
{
|
||||
$user = WxUserModel::with('enterprise')->where('id', $this->userId)->first();
|
||||
$ent = $user['enterprise'] ?? null;
|
||||
if (empty($ent)) {
|
||||
$this->utils->errorThrow('未绑定企业');
|
||||
}
|
||||
$row = is_array($ent) ? $ent : $ent->toArray();
|
||||
$row['logo'] = \App\Service\common\MediaUrlService::getInstance()->toPublic($row['logo'] ?? '');
|
||||
return $row;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,6 +66,8 @@ return [
|
||||
['title' => '企业管理', 'name' => 'CustomerEnterprise', 'path' => '/customer/enterprise', 'component' => '/customer/enterprise/index', 'icon' => 'lucide:building', 'sort' => 2],
|
||||
['title' => '清单管理', 'name' => 'CustomerList', 'path' => '/customer/list', 'component' => '/customer/list/index', 'icon' => 'lucide:clipboard-list', 'sort' => 3],
|
||||
['title' => '订单管理', 'name' => 'CustomerOrder', 'path' => '/customer/order', 'component' => '/customer/order/index', 'icon' => 'lucide:receipt-text', 'sort' => 4],
|
||||
['title' => '用户反馈', 'name' => 'CustomerFeedback', 'path' => '/customer/feedback', 'component' => '/customer/feedback/index', 'icon' => 'lucide:message-square', 'sort' => 5],
|
||||
['title' => '售后管理', 'name' => 'CustomerAfterSale', 'path' => '/customer/after-sale', 'component' => '/customer/after-sale/index', 'icon' => 'lucide:undo-2', 'sort' => 6],
|
||||
],
|
||||
],
|
||||
[
|
||||
|
||||
@@ -401,11 +401,11 @@ foreach ($home as &$homeRow) {
|
||||
unset($homeRow);
|
||||
|
||||
$package = [
|
||||
$row('package-00-default', '默认', 'package', [$sec('list', 'card')], 'clay-edge', 'regular', 'none', 'shop', '默认'),
|
||||
$row('package-01-card', '封面卡片', 'package', [$sec('list', 'card')], 'champagne-foil', 'regular', 'none', 'shop', '套餐'),
|
||||
$row('package-02-featured', '首图通栏', 'package', [$sec('list', 'featured')], 'walnut-frame', 'regular', 'inset', 'shop', '套餐'),
|
||||
$row('package-03-magazine', '刊名目录', 'package', [$sec('list', 'magazine')], 'thin-rule', 'airy', 'ornament', 'magazine', '套餐'),
|
||||
$row('package-04-list', '单列图文', 'package', [$sec('list', 'list')], 'pearl-line', 'compact', 'none', 'shop', '套餐'),
|
||||
$row('package-00-default', '默认', 'package', [$sec('search', 'bar'), $sec('list', 'card')], 'clay-edge', 'regular', 'none', 'shop', '默认'),
|
||||
$row('package-01-card', '封面卡片', 'package', [$sec('search', 'bar'), $sec('list', 'card')], 'champagne-foil', 'regular', 'none', 'shop', '套餐'),
|
||||
$row('package-02-featured', '首图通栏', 'package', [$sec('search', 'bar'), $sec('list', 'featured')], 'walnut-frame', 'regular', 'inset', 'shop', '套餐'),
|
||||
$row('package-03-magazine', '刊名目录', 'package', [$sec('search', 'pill'), $sec('list', 'magazine')], 'thin-rule', 'airy', 'ornament', 'magazine', '套餐'),
|
||||
$row('package-04-list', '单列图文', 'package', [$sec('search', 'bar'), $sec('list', 'list')], 'pearl-line', 'compact', 'none', 'shop', '套餐'),
|
||||
];
|
||||
|
||||
return [
|
||||
|
||||
167
database/sql/2026-08-19/01_gap_features.sql
Normal file
167
database/sql/2026-08-19/01_gap_features.sql
Normal file
@@ -0,0 +1,167 @@
|
||||
-- ============================================================
|
||||
-- 缺口补齐:地址簿 / 反馈 / 收藏 / 售后 / 库存 / 视频 / 订阅模板 / 菜单
|
||||
-- 幂等;业务表 cc_*,系统表 nl_*
|
||||
-- ============================================================
|
||||
|
||||
SET @db := DATABASE();
|
||||
|
||||
-- 收货地址
|
||||
SET @exists := (SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'cc_wx_address');
|
||||
SET @sql := IF(@exists = 0,
|
||||
'CREATE TABLE `cc_wx_address` (
|
||||
`id` int unsigned NOT NULL AUTO_INCREMENT,
|
||||
`user_id` int NOT NULL DEFAULT 0,
|
||||
`name` varchar(50) NOT NULL DEFAULT '''',
|
||||
`phone` varchar(20) NOT NULL DEFAULT '''',
|
||||
`address` varchar(255) NOT NULL DEFAULT '''',
|
||||
`is_default` tinyint NOT NULL DEFAULT 0 COMMENT ''1=默认'',
|
||||
`created_at` int NOT NULL DEFAULT 0,
|
||||
`updated_at` int NOT NULL DEFAULT 0,
|
||||
`deleted_at` int NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_addr_user` (`user_id`, `deleted_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT=''小程序收货地址''',
|
||||
'SELECT ''skip cc_wx_address'' AS msg');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 用户反馈
|
||||
SET @exists := (SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'cc_feedback');
|
||||
SET @sql := IF(@exists = 0,
|
||||
'CREATE TABLE `cc_feedback` (
|
||||
`id` int unsigned NOT NULL AUTO_INCREMENT,
|
||||
`user_id` int NOT NULL DEFAULT 0,
|
||||
`content` varchar(500) NOT NULL DEFAULT '''',
|
||||
`images` varchar(1000) NOT NULL DEFAULT '''' COMMENT ''图 URL,逗号分隔'',
|
||||
`reply` varchar(500) NOT NULL DEFAULT '''',
|
||||
`status` tinyint NOT NULL DEFAULT 0 COMMENT ''0=待处理 1=已回复'',
|
||||
`created_at` int NOT NULL DEFAULT 0,
|
||||
`updated_at` int NOT NULL DEFAULT 0,
|
||||
`deleted_at` int NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_fb_user` (`user_id`, `status`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT=''用户反馈''',
|
||||
'SELECT ''skip cc_feedback'' AS msg');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 收藏
|
||||
SET @exists := (SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'cc_favorite');
|
||||
SET @sql := IF(@exists = 0,
|
||||
'CREATE TABLE `cc_favorite` (
|
||||
`id` int unsigned NOT NULL AUTO_INCREMENT,
|
||||
`user_id` int NOT NULL DEFAULT 0,
|
||||
`catalogue_id` int NOT NULL DEFAULT 0,
|
||||
`created_at` int NOT NULL DEFAULT 0,
|
||||
`updated_at` int NOT NULL DEFAULT 0,
|
||||
`deleted_at` int NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_fav_user` (`user_id`, `deleted_at`),
|
||||
KEY `idx_fav_cat` (`catalogue_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT=''商品收藏''',
|
||||
'SELECT ''skip cc_favorite'' AS msg');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 售后
|
||||
SET @exists := (SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'cc_after_sale');
|
||||
SET @sql := IF(@exists = 0,
|
||||
'CREATE TABLE `cc_after_sale` (
|
||||
`id` int unsigned NOT NULL AUTO_INCREMENT,
|
||||
`order_id` int NOT NULL DEFAULT 0,
|
||||
`user_id` int NOT NULL DEFAULT 0,
|
||||
`reason` varchar(255) NOT NULL DEFAULT '''',
|
||||
`amount` int NOT NULL DEFAULT 0 COMMENT ''申请退款金额(分)'',
|
||||
`images` varchar(1000) NOT NULL DEFAULT '''',
|
||||
`status` tinyint NOT NULL DEFAULT 0 COMMENT ''0=待审 1=通过 2=拒绝'',
|
||||
`admin_remark` varchar(255) NOT NULL DEFAULT '''',
|
||||
`created_at` int NOT NULL DEFAULT 0,
|
||||
`updated_at` int NOT NULL DEFAULT 0,
|
||||
`deleted_at` int NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_as_order` (`order_id`),
|
||||
KEY `idx_as_user` (`user_id`, `status`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT=''售后申请''',
|
||||
'SELECT ''skip cc_after_sale'' AS msg');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 商品视频
|
||||
SET @col := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'cc_catalogue' AND COLUMN_NAME = 'video');
|
||||
SET @sql := IF(@col = 0,
|
||||
'ALTER TABLE `cc_catalogue` ADD COLUMN `video` varchar(500) NOT NULL DEFAULT '''' COMMENT ''商品视频'' AFTER `pdf`',
|
||||
'SELECT ''skip cc_catalogue.video'' AS msg');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 规格库存:-1=不限
|
||||
SET @col := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'cc_price_sheet' AND COLUMN_NAME = 'stock');
|
||||
SET @sql := IF(@col = 0,
|
||||
'ALTER TABLE `cc_price_sheet` ADD COLUMN `stock` int NOT NULL DEFAULT -1 COMMENT ''库存,-1不限'' AFTER `routine`',
|
||||
'SELECT ''skip cc_price_sheet.stock'' AS msg');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 订阅消息模板
|
||||
SET @col := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'nl_wx_app' AND COLUMN_NAME = 'subscribe_pay_tpl');
|
||||
SET @sql := IF(@col = 0,
|
||||
'ALTER TABLE `nl_wx_app` ADD COLUMN `subscribe_pay_tpl` varchar(64) NOT NULL DEFAULT '''' COMMENT ''付款成功订阅模板'' AFTER `package_enabled`',
|
||||
'SELECT ''skip nl_wx_app.subscribe_pay_tpl'' AS msg');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
SET @col := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'nl_wx_app' AND COLUMN_NAME = 'subscribe_ship_tpl');
|
||||
SET @sql := IF(@col = 0,
|
||||
'ALTER TABLE `nl_wx_app` ADD COLUMN `subscribe_ship_tpl` varchar(64) NOT NULL DEFAULT '''' COMMENT ''发货订阅模板'' AFTER `subscribe_pay_tpl`',
|
||||
'SELECT ''skip nl_wx_app.subscribe_ship_tpl'' AS msg');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 套餐菜单(若 lgp:menu-sync 未跑过)
|
||||
SET @parent_id := (SELECT id FROM nl_menu WHERE name = 'Goods' AND deleted_at = 0 LIMIT 1);
|
||||
SET @exists := (SELECT COUNT(*) FROM nl_menu WHERE name = 'GoodsPackage' AND deleted_at = 0);
|
||||
SET @sql := IF(@parent_id IS NOT NULL AND @exists = 0,
|
||||
CONCAT(
|
||||
'INSERT INTO `nl_menu` (`title`,`icon`,`name`,`path`,`component`,`redirect`,`keep_alive`,`hide_in_menu`,`affix_tab`,`badge`,`badge_type`,`badge_variants`,`iframe_src`,`pid`,`sort`,`query`,`created_at`,`updated_at`,`deleted_at`) VALUES (',
|
||||
'''套餐搭配'', ''lucide:package'', ''GoodsPackage'', ''/goods/package'', ''/goods/package/index'', '''', 1, 0, 1, '''', 0, 0, '''', ',
|
||||
@parent_id, ', 6, '''', UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), 0)'
|
||||
),
|
||||
'SELECT ''skip menu GoodsPackage'' AS msg');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 反馈 / 售后菜单
|
||||
SET @parent_id := (SELECT id FROM nl_menu WHERE name = 'Customer' AND deleted_at = 0 LIMIT 1);
|
||||
SET @exists := (SELECT COUNT(*) FROM nl_menu WHERE name = 'CustomerFeedback' AND deleted_at = 0);
|
||||
SET @sql := IF(@parent_id IS NOT NULL AND @exists = 0,
|
||||
CONCAT(
|
||||
'INSERT INTO `nl_menu` (`title`,`icon`,`name`,`path`,`component`,`redirect`,`keep_alive`,`hide_in_menu`,`affix_tab`,`badge`,`badge_type`,`badge_variants`,`iframe_src`,`pid`,`sort`,`query`,`created_at`,`updated_at`,`deleted_at`) VALUES (',
|
||||
'''用户反馈'', ''lucide:message-square'', ''CustomerFeedback'', ''/customer/feedback'', ''/customer/feedback/index'', '''', 1, 0, 1, '''', 0, 0, '''', ',
|
||||
@parent_id, ', 5, '''', UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), 0)'
|
||||
),
|
||||
'SELECT ''skip menu CustomerFeedback'' AS msg');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @exists := (SELECT COUNT(*) FROM nl_menu WHERE name = 'CustomerAfterSale' AND deleted_at = 0);
|
||||
SET @sql := IF(@parent_id IS NOT NULL AND @exists = 0,
|
||||
CONCAT(
|
||||
'INSERT INTO `nl_menu` (`title`,`icon`,`name`,`path`,`component`,`redirect`,`keep_alive`,`hide_in_menu`,`affix_tab`,`badge`,`badge_type`,`badge_variants`,`iframe_src`,`pid`,`sort`,`query`,`created_at`,`updated_at`,`deleted_at`) VALUES (',
|
||||
'''售后管理'', ''lucide:undo-2'', ''CustomerAfterSale'', ''/customer/after-sale'', ''/customer/after-sale/index'', '''', 1, 0, 1, '''', 0, 0, '''', ',
|
||||
@parent_id, ', 6, '''', UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), 0)'
|
||||
),
|
||||
'SELECT ''skip menu CustomerAfterSale'' AS msg');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 有微信用户菜单的角色同步新菜单
|
||||
SET @wx_user_menu_id := (SELECT id FROM nl_menu WHERE name = 'CustomerWxUser' AND deleted_at = 0 LIMIT 1);
|
||||
SET @menu_id := (SELECT id FROM nl_menu WHERE name = 'CustomerFeedback' AND deleted_at = 0 LIMIT 1);
|
||||
INSERT INTO `nl_role_menu_relations` (`role_id`, `menu_id`, `created_at`)
|
||||
SELECT r.role_id, @menu_id, UNIX_TIMESTAMP()
|
||||
FROM `nl_role_menu_relations` r
|
||||
WHERE @menu_id IS NOT NULL AND @wx_user_menu_id IS NOT NULL AND r.menu_id = @wx_user_menu_id
|
||||
AND NOT EXISTS (SELECT 1 FROM `nl_role_menu_relations` x WHERE x.role_id = r.role_id AND x.menu_id = @menu_id);
|
||||
|
||||
SET @menu_id := (SELECT id FROM nl_menu WHERE name = 'CustomerAfterSale' AND deleted_at = 0 LIMIT 1);
|
||||
INSERT INTO `nl_role_menu_relations` (`role_id`, `menu_id`, `created_at`)
|
||||
SELECT r.role_id, @menu_id, UNIX_TIMESTAMP()
|
||||
FROM `nl_role_menu_relations` r
|
||||
WHERE @menu_id IS NOT NULL AND @wx_user_menu_id IS NOT NULL AND r.menu_id = @wx_user_menu_id
|
||||
AND NOT EXISTS (SELECT 1 FROM `nl_role_menu_relations` x WHERE x.role_id = r.role_id AND x.menu_id = @menu_id);
|
||||
|
||||
SET @cat_menu := (SELECT id FROM nl_menu WHERE name = 'GoodsCatalogue' AND deleted_at = 0 LIMIT 1);
|
||||
SET @pkg_menu := (SELECT id FROM nl_menu WHERE name = 'GoodsPackage' AND deleted_at = 0 LIMIT 1);
|
||||
INSERT INTO `nl_role_menu_relations` (`role_id`, `menu_id`, `created_at`)
|
||||
SELECT r.role_id, @pkg_menu, UNIX_TIMESTAMP()
|
||||
FROM `nl_role_menu_relations` r
|
||||
WHERE @pkg_menu IS NOT NULL AND @cat_menu IS NOT NULL AND r.menu_id = @cat_menu
|
||||
AND NOT EXISTS (SELECT 1 FROM `nl_role_menu_relations` x WHERE x.role_id = r.role_id AND x.menu_id = @pkg_menu);
|
||||
@@ -867,6 +867,8 @@ CREATE TABLE `nl_wx_app` (
|
||||
`notify_url` varchar(255) NOT NULL DEFAULT '' COMMENT '支付回调地址',
|
||||
`template_code` varchar(50) NOT NULL DEFAULT '' COMMENT '默认装修模板 code',
|
||||
`package_enabled` tinyint NOT NULL DEFAULT 0 COMMENT '1=启用套餐(首页热门+底栏)',
|
||||
`subscribe_pay_tpl` varchar(64) NOT NULL DEFAULT '' COMMENT '付款成功订阅模板',
|
||||
`subscribe_ship_tpl` varchar(64) NOT NULL DEFAULT '' COMMENT '发货订阅模板',
|
||||
`status` tinyint NOT NULL DEFAULT 0 COMMENT '0=启用 1=停用',
|
||||
`remark` varchar(255) NOT NULL DEFAULT '',
|
||||
`created_at` int NOT NULL DEFAULT 0,
|
||||
|
||||
@@ -37,16 +37,22 @@ Route::group(['prefix' => 'wx', 'middleware' => 'nl.wx.https'], function () {
|
||||
'auth' => \App\Http\Controllers\Wx\AuthController::class, // 小程序登录
|
||||
'home' => \App\Http\Controllers\Wx\HomeController::class, // 首页轮播与分类
|
||||
'package' => \App\Http\Controllers\Wx\PackageController::class, // 套餐热门/列表/详情/一件加入
|
||||
'product' => \App\Http\Controllers\Wx\ProductController::class, // 商品浏览免登录(和老 wx-api 一致)
|
||||
'factory' => \App\Http\Controllers\Wx\FactoryController::class, // 工厂目录
|
||||
'colorcard' => \App\Http\Controllers\Wx\ColorcardController::class, // 色卡目录
|
||||
'' => \App\Http\Controllers\Wx\ThemeController::class, // 主题下发与支付回调
|
||||
]);
|
||||
|
||||
Route::group(['middleware' => 'nl.wx'], function () {
|
||||
UtilsService::class::getInstance()->autoRouteRegister([
|
||||
'product' => \App\Http\Controllers\Wx\ProductController::class, // 商品
|
||||
'list' => \App\Http\Controllers\Wx\ListController::class, // 清单
|
||||
'user' => \App\Http\Controllers\Wx\UserController::class, // 我的
|
||||
'order' => \App\Http\Controllers\Wx\OrderController::class, // 订单
|
||||
'upload' => \App\Http\Controllers\Wx\WxUploadController::class, // 上传(转账凭证等)
|
||||
'address' => \App\Http\Controllers\Wx\AddressController::class, // 收货地址
|
||||
'feedback' => \App\Http\Controllers\Wx\FeedbackController::class, // 用户反馈
|
||||
'favorite' => \App\Http\Controllers\Wx\FavoriteController::class, // 收藏
|
||||
'after-sale' => \App\Http\Controllers\Wx\AfterSaleController::class, // 售后
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -91,6 +97,8 @@ Route::group(['middleware' => 'nl.auth'], function () {
|
||||
'enterprise' => \App\Http\Controllers\Api\EnterpriseController::class, // 企业管理
|
||||
'list' => \App\Http\Controllers\Api\ListController::class, // 清单管理
|
||||
'order' => \App\Http\Controllers\Api\OrderController::class, // 订单管理
|
||||
'feedback' => \App\Http\Controllers\Api\FeedbackController::class, // 用户反馈
|
||||
'after-sale' => \App\Http\Controllers\Api\AfterSaleController::class, // 售后审核
|
||||
'wx-template' => \App\Http\Controllers\Api\WxTemplateController::class, // 小程序装修模板
|
||||
'wx-card-scheme' => \App\Http\Controllers\Api\WxCardSchemeController::class, // 卡片画布方案
|
||||
'wx-app' => \App\Http\Controllers\Api\WxAppController::class, // 小程序应用配置
|
||||
|
||||
Reference in New Issue
Block a user