更新若干功能

This commit is contained in:
2026-08-20 19:16:49 +08:00
parent 72bc6502eb
commit 4ddf5e3c5f
48 changed files with 1908 additions and 18 deletions

View 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 ? '已通过' : '已拒绝'
);
}
}

View File

@@ -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 : [],
), '导入完成');
}
}

View File

@@ -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 : []), '导入完成');
}
}

View 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', '')),
'已回复'
);
}
}

View File

@@ -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)));
}
}

View File

@@ -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'];
}
/**

View File

@@ -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',
];
}

View 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)), '已设为默认');
}
}

View 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()), '已提交');
}
}

View 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());
}
}

View 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());
}
}

View 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)));
}
}

View 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()), '已提交');
}
}

View File

@@ -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)));
}
}

View File

@@ -48,4 +48,13 @@ class UserController extends BaseController
'保存成功'
);
}
/**
* 已绑定企业详情
* @Method GET
*/
public function enterprise(): JsonResponse
{
return jok($this->service->enterprise());
}
}

View 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');
}
}

View 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');
}
}

View 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');
}
}

View 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 = [];
}

View File

@@ -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'];

View 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]);
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View 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]);
}
}

View File

@@ -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);
}
}

View File

@@ -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) {
// 订阅消息是附加能力,模板字段对不上或用户未授权都不能挡住收款/发货
}
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View 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(),
]);
}
}

View File

@@ -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' => '票根',

View 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;
}
}

View 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,
];
}
}

View 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();
}
}

View 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();
}
}

View 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();
}
}

View 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();
}
}

View 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();
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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 '';
}
}
}

View File

@@ -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;
}
}

View File

@@ -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],
],
],
[

View File

@@ -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 [

View 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);

View File

@@ -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,

View File

@@ -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, // 小程序应用配置