更新若干功能
Some checks failed
Tests / PHP 8.2 (push) Has been cancelled
Tests / PHP 8.3 (push) Has been cancelled
Tests / PHP 8.4 (push) Has been cancelled

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