This commit is contained in:
wanghai
2026-05-29 17:44:49 +08:00
parent 7a2bfc6c5c
commit 4584fde5e5
38 changed files with 2167 additions and 123 deletions

View File

@@ -28,6 +28,11 @@ use SystemLinkBundle\Services\ThirdSettingService;
use CompanysBundle\Services\ShopsService;
use CompanysBundle\Services\Shops\WxShopsService;
use DistributionBundle\Services\DistributorService;
use PromotionsBundle\Services\SmsDriver\ShopexSmsClient;
use ThirdPartyBundle\Services\SaasCertCentre\CertClient;
use ThirdPartyBundle\Services\SaasCertCentre\CertService;
use ThirdPartyBundle\Services\AppsCenter\AppsCenterService;
use CompanysBundle\Ego\PrismEgo;
class Companys extends BaseController
{
@@ -635,5 +640,108 @@ class Companys extends BaseController
return $this->response->array($result);
}
public function loginUsercenter(Request $request)
{
$companyId = app('auth')->user()->get('company_id');
$code = $request->get('code');
if (!$code) {
throw new ResourceException('缺少授权code');
}
$prismEgo = new PrismEgo();
$res = $prismEgo->getToken($code);
app('log')->debug($res);
$res['access_token'] = $res['access_token'] ?? '';
$res['data'] = $res['data'] ?? [];
if (!$res['access_token'] || !$res['data']) {
throw new ResourceException('登录失败');
}
//保存 access_token 和 refresh_token
$prismData = $res['data'];
$accessToken = $res['access_token'];
$expiresIn = $res['expires_in'];
$refreshToken = $res['refresh_token'];
$refreshExpires = $res['refresh_expires'];
$shopexSmsClient = new ShopexSmsClient($companyId, $prismData['passport_uid']);
$shopexSmsClient->setAccessToken($accessToken, $expiresIn);
$shopexSmsClient->setRefreshToken($refreshToken, $refreshExpires);
$this->companysService->updateInfo(['company_id' => $companyId], [
'passport_uid' => $prismData['passport_uid'],
'eid' => $prismData['eid'],
]);
// 获取shopex证书
$certService = new CertService(new CertClient($companyId, $prismData['passport_uid']));
$certService->getAouthCert();
return $this->response->array(['status' => true]);
}
public function getUsercenterOuthorizeurl()
{
$callback = rtrim(config('common.shop_admin_url'), '/') . '/setting/ShangPai/usercenter';
$data = array(
'response_type' => 'code',
'client_id' => config('common.prism_key'),
'redirect_uri' => $callback,
'view' => 'oauth_usercenter'
);
$query = http_build_query($data);
$shopexUrl = config('common.openapi_shopex_url');
$url = "{$shopexUrl}/oauth/authorize?{$query}";
return $this->response->array(['url' => $url]);
}
public function getAppcenterGoods()
{
$appsCenterService = new AppsCenterService();
$data = $appsCenterService->fetchEmbedGoods();
$data['open_base_url'] = rtrim(config('common.appcenter_base_url'), '/');
return $this->response->array($data);
}
public function getAppcenterUrl()
{
$companyId = app('auth')->user()->get('company_id');
$shopexUid = $this->companysService->getPassportUidByCompanyId($companyId);
if (!$shopexUid) {
throw new ResourceException('缺少 Shopex ID请先完成用户中心登录');
}
$certService = new CertService(false, $companyId, $shopexUid);
$certSetting = $certService->getCertSetting();
$nodeId = trim((string) ($certSetting['node_id'] ?? ''));
$token = trim((string) ($certSetting['token'] ?? ''));
if (!$nodeId) {
throw new ResourceException('缺少节点号,请先完成证书绑定');
}
if (!$token) {
throw new ResourceException('缺少证书 token请先完成证书绑定');
}
$appsCenterService = new AppsCenterService();
$callback = $appsCenterService->buildAppcenterUrl([
'channel' => config('common.appcenter_channel'),
'shopexid' => $shopexUid,
'sys_node_id' => $nodeId,
'callback' => rtrim(config('common.shop_admin_url'), '/'),
], $token);
$data = array(
'response_type' => 'code',
'client_id' => config('common.prism_key'),
'redirect_uri' => $callback,
'view' => 'oauth_usercenter'
);
$query = http_build_query($data);
$shopexUrl = config('common.openapi_shopex_url');
$url = "{$shopexUrl}/oauth/authorize?{$query}";
return $this->response->array(['url' => $url]);
}
}

View File

@@ -253,10 +253,12 @@ class PickupLocation extends Controller
unset($filter['distributor_id']);
}
// if (isset($params['rel_distributor_id']) && $params['rel_distributor_id']) {
// $filter['rel_distributor_id'] = $params['rel_distributor_id'];
// unset($filter['distributor_id']); //总部
// }
if (isset($params['rel_distributor_id']) && $params['rel_distributor_id']) {
$filter['rel_distributor_id'] = $params['rel_distributor_id'];
if ($operatorType != 'distributor') {
unset($filter['distributor_id']);
}
}
if (isset($params['name']) && $params['name']) {
$filter['name|contains'] = $params['name'];
@@ -436,17 +438,29 @@ class PickupLocation extends Controller
throw new ResourceException($error);
}
$filter['company_id'] = app('auth')->user()->get('company_id');
$companyId = app('auth')->user()->get('company_id');
$operatorType = app('auth')->user()->get('operator_type');
$filter['distributor_id'] = 0;
$distributorId = 0;
if ($operatorType == 'distributor') { //店铺端
$filter['distributor_id'] = $request->get('distributor_id');
$distributorId = $request->get('distributor_id');
}
$filter['id'] = $params['id'];
$filter['rel_distributor_id'] = $params['rel_distributor_id'];
$pickupLocationService = new PickupLocationService();
$pickupLocationService->updateBy($filter, ['rel_distributor_id' => 0]);
$ids = is_array($params['id']) ? $params['id'] : [$params['id']];
foreach ($ids as $id) {
$filter = [
'company_id' => $companyId,
'id' => $id,
'rel_distributor_id' => $params['rel_distributor_id'],
];
if ($operatorType == 'distributor') {
$filter['distributor_id'] = $distributorId;
}
$affected = $pickupLocationService->updateBy($filter, ['rel_distributor_id' => 0]);
if (!$affected) {
throw new ResourceException(trans('DistributionBundle/Services/PickupLocationService.pickup_location_not_exist'));
}
}
return $this->response->array(['status' => true]);
}

View File

@@ -27,6 +27,7 @@ use LaravelDoctrine\Extensions\SoftDeletes\SoftDeletes;
* @ORM\Table(name="theme_pc_template", options={"comment":"pc页面装修"},
* indexes={
* @ORM\Index(name="idx_company_id", columns={"company_id"}),
* @ORM\Index(name="idx_company_distributor", columns={"company_id", "distributor_id"}),
* },)
* @ORM\Entity(repositoryClass="ThemeBundle\Repositories\ThemePcTemplateRepository")
*/
@@ -50,6 +51,13 @@ class ThemePcTemplate
*/
private $company_id;
/**
* @var integer
*
* @ORM\Column(name="distributor_id", type="integer", options={"comment":"店铺ID", "default":0})
*/
private $distributor_id = 0;
/**
* @var string
*
@@ -136,6 +144,30 @@ class ThemePcTemplate
return $this->company_id;
}
/**
* Set distributorId.
*
* @param int $distributorId
*
* @return ThemePcTemplate
*/
public function setDistributorId($distributorId = 0)
{
$this->distributor_id = (int)$distributorId;
return $this;
}
/**
* Get distributorId.
*
* @return int
*/
public function getDistributorId()
{
return $this->distributor_id;
}
/**
* Set templateTitle.
*

View File

@@ -57,7 +57,7 @@ class ThemePcTemplateContent
/**
* @var string
*
* @ORM\Column(name="name", type="string", length=20, options={"comment":"配置名称"})
* @ORM\Column(name="name", type="string", length=64, options={"comment":"配置名称"})
*/
private $name;

View File

@@ -0,0 +1,127 @@
<?php
/**
* Copyright 2019-2026 ShopeX
*/
namespace ThemeBundle\Entities;
use Doctrine\ORM\Mapping as ORM;
/**
* Web 端商城 — 菜单主表
*
* @ORM\Entity(repositoryClass="ThemeBundle\Repositories\WebMenuRepository")
* @ORM\Table(
* name="web_menus",
* options={"comment":"Web端商城-菜单主表"},
* uniqueConstraints={
* @ORM\UniqueConstraint(name="uk_company_key", columns={"company_id", "key"})
* }
* )
*/
class WebMenu
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer", options={"unsigned":true})
*/
private $id;
/**
* @ORM\Column(name="company_id", type="integer", options={"unsigned":true})
*/
private $companyId;
/** @ORM\Column(type="string", length=100) */
private $name;
/** @ORM\Column(name="`key`", type="string", length=100) */
private $key;
/** @ORM\Column(type="smallint", options={"default":1}) */
private $status = 1;
/** @ORM\Column(name="created_at", type="datetime") */
private $createdAt;
/** @ORM\Column(name="updated_at", type="datetime") */
private $updatedAt;
public function getId(): ?int
{
return $this->id;
}
public function setCompanyId(int $companyId): self
{
$this->companyId = $companyId;
return $this;
}
public function getCompanyId(): int
{
return (int) $this->companyId;
}
public function setName(string $name): self
{
$this->name = $name;
return $this;
}
public function getName(): string
{
return $this->name;
}
public function setKey(string $key): self
{
$this->key = $key;
return $this;
}
public function getKey(): string
{
return $this->key;
}
public function setStatus(int $status): self
{
$this->status = $status;
return $this;
}
public function getStatus(): int
{
return (int) $this->status;
}
public function setCreatedAt(\DateTimeInterface $createdAt): self
{
$this->createdAt = $createdAt;
return $this;
}
public function getCreatedAt(): ?\DateTimeInterface
{
return $this->createdAt;
}
public function setUpdatedAt(\DateTimeInterface $updatedAt): self
{
$this->updatedAt = $updatedAt;
return $this;
}
public function getUpdatedAt(): ?\DateTimeInterface
{
return $this->updatedAt;
}
}

View File

@@ -0,0 +1,217 @@
<?php
/**
* Copyright 2019-2026 ShopeX
*/
namespace ThemeBundle\Entities;
use Doctrine\ORM\Mapping as ORM;
/**
* Web 端商城 — 菜单项表
*
* @ORM\Entity(repositoryClass="ThemeBundle\Repositories\WebMenuItemRepository")
* @ORM\Table(
* name="web_menu_items",
* options={"comment":"Web端商城-菜单项表"},
* indexes={
* @ORM\Index(name="idx_menu_id", columns={"menu_id"}),
* @ORM\Index(name="idx_company_id", columns={"company_id"}),
* @ORM\Index(name="idx_parent_id", columns={"parent_id"})
* }
* )
*/
class WebMenuItem
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer", options={"unsigned":true})
*/
private $id;
/** @ORM\Column(name="menu_id", type="integer", options={"unsigned":true}) */
private $menuId;
/** @ORM\Column(name="company_id", type="integer", options={"unsigned":true}) */
private $companyId;
/** @ORM\Column(name="parent_id", type="integer", options={"unsigned":true, "default":0}) */
private $parentId = 0;
/** @ORM\Column(type="string", length=100) */
private $name;
/** @ORM\Column(name="image_url", type="string", length=500, nullable=true) */
private $imageUrl;
/** @ORM\Column(name="link_type", type="string", length=50, options={"default":"url"}) */
private $linkType = 'url';
/** @ORM\Column(name="link_value", type="string", length=500, nullable=true) */
private $linkValue;
/** @ORM\Column(name="link_extra", type="text", nullable=true) */
private $linkExtra;
/** @ORM\Column(type="integer", options={"default":0}) */
private $sort = 0;
/** @ORM\Column(type="smallint", options={"default":1}) */
private $status = 1;
/** @ORM\Column(name="created_at", type="datetime") */
private $createdAt;
/** @ORM\Column(name="updated_at", type="datetime") */
private $updatedAt;
public function getId(): ?int
{
return $this->id;
}
public function setMenuId(int $menuId): self
{
$this->menuId = $menuId;
return $this;
}
public function getMenuId(): int
{
return (int) $this->menuId;
}
public function setCompanyId(int $companyId): self
{
$this->companyId = $companyId;
return $this;
}
public function getCompanyId(): int
{
return (int) $this->companyId;
}
public function setParentId(int $parentId): self
{
$this->parentId = $parentId;
return $this;
}
public function getParentId(): int
{
return (int) $this->parentId;
}
public function setName(string $name): self
{
$this->name = $name;
return $this;
}
public function getName(): string
{
return $this->name;
}
public function setImageUrl(?string $imageUrl): self
{
$this->imageUrl = $imageUrl;
return $this;
}
public function getImageUrl(): ?string
{
return $this->imageUrl;
}
public function setLinkType(string $linkType): self
{
$this->linkType = $linkType;
return $this;
}
public function getLinkType(): string
{
return $this->linkType;
}
public function setLinkValue(?string $linkValue): self
{
$this->linkValue = $linkValue;
return $this;
}
public function getLinkValue(): ?string
{
return $this->linkValue;
}
public function setLinkExtra(?string $linkExtra): self
{
$this->linkExtra = $linkExtra;
return $this;
}
public function getLinkExtra(): ?string
{
return $this->linkExtra;
}
public function setSort(int $sort): self
{
$this->sort = $sort;
return $this;
}
public function getSort(): int
{
return (int) $this->sort;
}
public function setStatus(int $status): self
{
$this->status = $status;
return $this;
}
public function getStatus(): int
{
return (int) $this->status;
}
public function setCreatedAt(\DateTimeInterface $createdAt): self
{
$this->createdAt = $createdAt;
return $this;
}
public function getCreatedAt(): ?\DateTimeInterface
{
return $this->createdAt;
}
public function setUpdatedAt(\DateTimeInterface $updatedAt): self
{
$this->updatedAt = $updatedAt;
return $this;
}
public function getUpdatedAt(): ?\DateTimeInterface
{
return $this->updatedAt;
}
}

View File

@@ -108,8 +108,8 @@ class OpenScreenAd extends Controller
'show_time' => ['required|in:always,first', '请选择曝光时间'],
'waiting_time' => ['required', '请设置秒数'],
// 'app' => ['required', '请选择应用端'],
'start_time' => ['required', '请选择展示开始时间'],
'end_time' => ['required', '请选择展示结束时间'],
// 'start_time' => ['required', '请选择展示开始时间'],
// 'end_time' => ['required', '请选择展示结束时间'],
];
$error = validator_params($params, $rules);
if ($error) {

View File

@@ -47,6 +47,13 @@ class PcTemplate extends Controller
* type="string",
* ),
* @SWG\Parameter(
* name="distributor_id",
* in="query",
* description="店铺ID不传默认0",
* required=false,
* type="integer",
* ),
* @SWG\Parameter(
* name="page_no",
* in="query",
* description="页号",
@@ -76,6 +83,7 @@ class PcTemplate extends Controller
* @SWG\Items(
* type="object",
* @SWG\Property(property="company_id", type="int"),
* @SWG\Property(property="distributor_id", type="int"),
* @SWG\Property(property="created", type="string"),
* @SWG\Property(property="deleted_at", type="string"),
* @SWG\Property(property="page_type", type="string"),
@@ -102,6 +110,7 @@ class PcTemplate extends Controller
$page_no = $request->input('page_no', 1);
$page_size = $request->input('page_size', 20);
$status = $request->input('status');
$distributor_id = $request->input('distributor_id');
$params = [
'company_id' => $company_id,
@@ -110,6 +119,9 @@ class PcTemplate extends Controller
'page_size' => $page_size,
'status' => $status,
];
if ($distributor_id !== null) {
$params['distributor_id'] = (int)$distributor_id;
}
$theme_pc_template_services = new ThemePcTemplateServices();
$result = $theme_pc_template_services->lists($params);
@@ -160,6 +172,13 @@ class PcTemplate extends Controller
* type="string",
* ),
* @SWG\Parameter(
* name="distributor_id",
* in="formData",
* description="店铺ID不传默认0",
* required=false,
* type="integer",
* ),
* @SWG\Parameter(
* name="status",
* in="formData",
* description="是否启用",
@@ -176,6 +195,7 @@ class PcTemplate extends Controller
* @SWG\Items(
* type="object",
* @SWG\Property(property="company_id", type="int"),
* @SWG\Property(property="distributor_id", type="int"),
* @SWG\Property(property="created", type="string"),
* @SWG\Property(property="deleted_at", type="string"),
* @SWG\Property(property="page_type", type="string"),
@@ -201,9 +221,11 @@ class PcTemplate extends Controller
$page_type = $request->input('page_type');
$version = $request->input('version', 'v1.0.1');
$status = $request->input('status', 2);
$distributor_id = $request->input('distributor_id', 0);
$params = [
'company_id' => $company_id,
'distributor_id' => (int)$distributor_id,
'template_title' => $template_title,
'template_description' => $template_description,
'page_type' => $page_type,
@@ -213,7 +235,7 @@ class PcTemplate extends Controller
$rules = [
'template_title' => ['required', '缺少页面名称'],
'template_description' => ['required', '缺少页面描述'],
'page_type' => ['required|in:index,custom', '缺少页面类型'],
'page_type' => ['required|in:index,custom,product_list', '缺少页面类型'],
'version' => ['required', '缺少版本号']
];
$error = validator_params($params, $rules);
@@ -277,6 +299,13 @@ class PcTemplate extends Controller
* type="string",
* ),
* @SWG\Parameter(
* name="distributor_id",
* in="formData",
* description="店铺ID不传不更新",
* required=false,
* type="integer",
* ),
* @SWG\Parameter(
* name="status",
* in="formData",
* description="是否启用",
@@ -293,6 +322,7 @@ class PcTemplate extends Controller
* @SWG\Items(
* type="object",
* @SWG\Property(property="company_id", type="int"),
* @SWG\Property(property="distributor_id", type="int"),
* @SWG\Property(property="created", type="string"),
* @SWG\Property(property="deleted_at", type="string"),
* @SWG\Property(property="page_type", type="string"),
@@ -317,6 +347,7 @@ class PcTemplate extends Controller
$template_description = $request->input('template_description');
$page_type = $request->input('page_type');
$status = $request->input('status');
$distributor_id = $request->input('distributor_id');
$params = [
'company_id' => $company_id,
@@ -326,6 +357,9 @@ class PcTemplate extends Controller
'page_type' => $page_type,
'status' => $status
];
if ($distributor_id !== null) {
$params['distributor_id'] = (int)$distributor_id;
}
$rules = [
'theme_pc_template_id' => ['required', '缺少theme_pc_template_id'],
];
@@ -581,6 +615,30 @@ class PcTemplate extends Controller
return $this->response->array($result);
}
/**
* 获取 PC 模板装修内容
*/
public function getDecorationContent(Request $request)
{
$company_id = app('auth')->user()->get('company_id');
$page_name = $request->input('page_name', 'page');
$theme_pc_template_id = $request->input('theme_pc_template_id');
$page_type = $request->input('page_type');
$distributor_id = (int)$request->input('distributor_id', 0);
$params = [
'company_id' => $company_id,
'page_name' => $page_name,
'theme_pc_template_id' => $theme_pc_template_id,
'page_type' => $page_type,
'distributor_id' => $distributor_id,
];
$service = new ThemePcTemplateContentServices();
$result = $service->decorationContent($params);
return $this->response->array($result);
}
/**
* @SWG\Post(
* path="/pctemplate/saveTemplateContent",
@@ -651,9 +709,15 @@ class PcTemplate extends Controller
$result = app('redis')->connection('companys')->get('pc_login_page:'.$companyId);
if (!$result) {
$result['logo'] = '';
$result['logo_light'] = '';
$result['logo_dark'] = '';
$result['background'] = '';
} else {
$result = json_decode($result, true);
$result['logo_light'] = $result['logo_light'] ?? ($result['logo'] ?? '');
$result['logo_dark'] = $result['logo_dark'] ?? ($result['logo'] ?? '');
$result['logo'] = $result['logo'] ?? ($result['logo_light'] ?? '');
$result['background'] = $result['background'] ?? '';
}
return $this->response->array($result);
}
@@ -662,9 +726,13 @@ class PcTemplate extends Controller
{
$companyId = app('auth')->user()->get('company_id');
// 整理参数
$logoLight = $request->input('logo_light', $request->input('logo', ''));
$logoDark = $request->input('logo_dark', $request->input('logo', ''));
$params = [
'logo' => $request->input('logo'),
'background' => $request->input('background'),
'logo' => $request->input('logo', $logoLight),
'logo_light' => $logoLight,
'logo_dark' => $logoDark,
'background' => $request->input('background', ''),
];
app('redis')->connection('companys')->set('pc_login_page:'.$companyId, json_encode($params));
return $this->response->array(['status' => true]);

View File

@@ -0,0 +1,106 @@
<?php
/**
* Copyright 2019-2026 ShopeX
*/
namespace ThemeBundle\Http\Api\V1\Action;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use ThemeBundle\Entities\WebMenu;
use ThemeBundle\Entities\WebMenuItem;
use ThemeBundle\Services\WebMenuService;
use ThemeBundle\Transformers\WebMenuItemTransformer;
use ThemeBundle\Transformers\WebMenuTransformer;
class WebMenuAction extends Controller
{
/**
* GET /web-menus
* 列表接口:{ "data": { "total_count": N, "list": [...] } }(与 Wiki 接口响应格式一致)
*/
public function index(Request $request)
{
$companyId = (int) app('auth')->user()->get('company_id');
$page = (int) $request->query('page', 1);
$pageSize = (int) $request->query('page_size', 20);
$name = $request->query('name');
$name = is_string($name) ? trim($name) : null;
if ($name === '') {
$name = null;
}
$service = new WebMenuService();
$result = $service->listMenus($companyId, $page, $pageSize, $name);
$transformer = new WebMenuTransformer();
$em = app('registry')->getManager('default');
$itemRepo = $em->getRepository(WebMenuItem::class);
$menuIds = array_map(fn ($m) => $m->getId(), $result['list']);
$counts = $itemRepo->countItemsByMenuIds($companyId, $menuIds);
$topLevelNames = $itemRepo->findTopLevelItemNamesGroupedByMenuIds($companyId, $menuIds);
$list = [];
foreach ($result['list'] as $m) {
$row = $transformer->transform($m);
$mid = $m->getId();
$row['items_count'] = $counts[$mid] ?? 0;
$row['top_level_item_names'] = $topLevelNames[$mid] ?? '';
$list[] = $row;
}
return $this->response->array([
'total_count' => $result['total'],
'list' => $list,
]);
}
/** POST /web-menus */
public function store(Request $request)
{
$companyId = (int) app('auth')->user()->get('company_id');
$service = new WebMenuService();
$menu = $service->createMenu($companyId, $request->only(['name', 'key', 'status']));
return $this->response->item($menu, new WebMenuTransformer());
}
/** GET /web-menus/{id} */
public function show(Request $request, $id)
{
$companyId = (int) app('auth')->user()->get('company_id');
$menuId = (int) $id;
$service = new WebMenuService();
$em = app('registry')->getManager('default');
$menu = $em->getRepository(WebMenu::class)->findOneByIdAndCompany($menuId, $companyId);
if (!$menu) {
return $this->response->errorNotFound('菜单不存在');
}
$items = $em->getRepository(WebMenuItem::class)->findAllByMenu($menuId, $companyId);
$tree = $service->buildTree($items);
$itemTr = new WebMenuItemTransformer();
/** 详情:{ "data": { id, name, key, status, items: [...] } } */
return $this->response->array(array_merge(
(new WebMenuTransformer())->transform($menu),
['items' => array_map([$itemTr, 'transform'], $tree)]
));
}
/** PUT /web-menus/{id} */
public function update(Request $request, $id)
{
$companyId = (int) app('auth')->user()->get('company_id');
$service = new WebMenuService();
$menu = $service->updateMenu((int) $id, $companyId, $request->only(['name', 'key', 'status']));
return $this->response->item($menu, new WebMenuTransformer());
}
/** DELETE /web-menus/{id} */
public function destroy(Request $request, $id)
{
$companyId = (int) app('auth')->user()->get('company_id');
$service = new WebMenuService();
$service->deleteMenu((int) $id, $companyId);
return $this->response->noContent();
}
}

View File

@@ -0,0 +1,62 @@
<?php
/**
* Copyright 2019-2026 ShopeX
*/
namespace ThemeBundle\Http\Api\V1\Action;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use ThemeBundle\Services\WebMenuService;
use ThemeBundle\Transformers\WebMenuItemFlatTransformer;
class WebMenuItemAction extends Controller
{
/** POST /web-menus/{id}/items */
public function store(Request $request, $id)
{
$companyId = (int) app('auth')->user()->get('company_id');
$service = new WebMenuService();
$item = $service->createItem((int) $id, $companyId, $request->only([
'name', 'image_url', 'link_type', 'link_value', 'link_extra', 'sort', 'status', 'parent_id',
]));
return $this->response->item($item, new WebMenuItemFlatTransformer());
}
/** PUT /web-menus/{id}/items/{itemId} */
public function update(Request $request, $id, $itemId)
{
$companyId = (int) app('auth')->user()->get('company_id');
$service = new WebMenuService();
$item = $service->updateItem((int) $itemId, (int) $id, $companyId, $request->only([
'name', 'image_url', 'link_type', 'link_value', 'link_extra', 'sort', 'status', 'parent_id',
]));
return $this->response->item($item, new WebMenuItemFlatTransformer());
}
/** DELETE /web-menus/{id}/items/{itemId} */
public function destroy(Request $request, $id, $itemId)
{
$companyId = (int) app('auth')->user()->get('company_id');
$service = new WebMenuService();
$service->deleteItem((int) $itemId, (int) $id, $companyId);
return $this->response->noContent();
}
/** PUT /web-menus/{id}/items/sort */
public function batchSort(Request $request, $id)
{
$companyId = (int) app('auth')->user()->get('company_id');
$sorts = $request->input('sorts', $request->input('items', []));
if (!is_array($sorts)) {
$sorts = [];
}
$service = new WebMenuService();
$service->batchUpdateSort((int) $id, $companyId, $sorts);
return $this->response->noContent();
}
}

View File

@@ -27,6 +27,7 @@ use TdksetBundle\Services\TdkGlobalService;
use ThemeBundle\Services\PagesTemplateServices;
use ThemeBundle\Services\PagesTemplateSetServices;
use DistributionBundle\Entities\Distributor;
use EmployeePurchaseBundle\Services\ActivityItemsService;
use GoodsBundle\Services\ItemsService;
class PagesTemplate extends Controller
@@ -72,6 +73,13 @@ class PagesTemplate extends Controller
* required=true,
* type="integer",
* ),
* @SWG\Parameter(
* name="e_activity_id",
* in="query",
* description="内购活动 ID传入时按活动店铺查商品并附加 employee_purchase_activity_items 活动价",
* required=false,
* type="integer",
* ),
* @SWG\Response(
* response=200,
* description="成功返回结构",
@@ -101,9 +109,10 @@ class PagesTemplate extends Controller
public function getWidgetItems(Request $request)
{
$authInfo = $request->get('auth');
$params = $request->all('regionauth_id', 'distributor_id', 'data_type', 'data_value', 'num', 'page', 'pageSize', 'sort_gte');
$params = $request->all('regionauth_id', 'distributor_id', 'data_type', 'data_value', 'num', 'page', 'pageSize', 'sort_gte', 'e_activity_id');
$params['company_id'] = $authInfo['company_id'];
$params['user_id'] = $authInfo['user_id'] ?? 0;
$eActivityId = (int) ($params['e_activity_id'] ?? 0);
$pages_template_services = new PagesTemplateServices();
$result = $pages_template_services->getWidgetItems($params);
@@ -113,6 +122,12 @@ class PagesTemplate extends Controller
if ($result['data']) {
$result['data'] = $itemsService->applyMultiSpecTotalStoreForItemList($result['data']);
}
if ($eActivityId > 0 && $result['data']) {
$activityItemsService = new ActivityItemsService();
$wrapped = ['list' => $result['data']];
$wrapped = $activityItemsService->getItemsListActityPrice($wrapped, $eActivityId, (int) $params['company_id']);
$result['data'] = $wrapped['list'];
}
if ($result['data']) {
$result['data'] = $itemsService->getItemsListMemberPrice($result['data'], $authInfo['user_id'], $params['company_id']);
}

View File

@@ -24,80 +24,6 @@ use ThemeBundle\Services\ThemePcTemplateContentServices;
class PcTemplate extends Controller
{
/**
* @SWG\Get(
* path="/h5app/wxapp/pctemplate/getHeaderOrFooter",
* summary="获取pc模板头尾部",
* tags={"模板"},
* description="获取pc模板头尾部",
* operationId="getHeaderOrFooter",
* @SWG\Parameter(
* name="Authorization",
* in="header",
* description="JWT验证token",
* required=true,
* type="string",
* ),
* @SWG\Parameter(
* name="page_name",
* in="query",
* description="页面名称",
* required=true,
* type="string",
* ),
* @SWG\Parameter(
* name="company_id",
* in="query",
* description="公司编号",
* required=true,
* type="integer",
* ),
* @SWG\Response(
* response=200,
* description="成功返回结构",
* @SWG\Schema(
* @SWG\Property(
* property="data",
* type="array",
* @SWG\Items(
* type="object",
* @SWG\Property(property="company_id", type="int"),
* @SWG\Property(property="created", type="string"),
* @SWG\Property(property="name", type="string"),
* @SWG\Property(property="params", type="string"),
* @SWG\Property(property="theme_pc_template_content_id", type="int"),
* @SWG\Property(property="theme_pc_template_id", type="int"),
* @SWG\Property(property="updated", type="string"),
* )
* ),
* ),
* ),
* @SWG\Response( response="default", description="错误返回结构", @SWG\Schema( type="array", @SWG\Items(ref="#/definitions/ThemeErrorRespones") ) )
* )
*/
public function getHeaderOrFooter(Request $request)
{
$company_id = $request->get('company_id');
$page_name = $request->input('page_name');
$params = [
'company_id' => $company_id,
'page_name' => $page_name,
];
$rules = [
'company_id' => ['required', '缺少company_id'],
'page_name' => ['required', '缺少page_name'],
];
$error = validator_params($params, $rules);
if ($error) {
throw new ResourceException($error);
}
$service = new ThemePcTemplateContentServices();
$result = $service->detail($params);
return $this->response->array($result);
}
/**
* @SWG\Get(
* path="/h5app/wxapp/pctemplate/getTemplateContent",
@@ -113,11 +39,18 @@ class PcTemplate extends Controller
* type="string",
* ),
* @SWG\Parameter(
* name="theme_pc_template_id",
* name="page_type",
* in="query",
* description="主题PC模板ID",
* description="页面类型 home/header/footer/custom/product_list",
* required=true,
* type="integer",
* type="string",
* ),
* @SWG\Parameter(
* name="page_id",
* in="query",
* description="页面ID自定义页传模板ID",
* required=false,
* type="string",
* ),
* @SWG\Parameter(
* name="company_id",
@@ -132,16 +65,10 @@ class PcTemplate extends Controller
* @SWG\Schema(
* @SWG\Property(
* property="data",
* type="array",
* @SWG\Items(
* type="array",
* @SWG\Items(
* type="object",
* @SWG\Property(property="config", type="string"),
* @SWG\Property(property="name", type="string"),
* )
* ),
* ),
* type="object",
* @SWG\Property(property="id", type="integer"),
* @SWG\Property(property="name", type="string"),
* @SWG\Property(property="config", type="string")
* ),
* ),
* @SWG\Response( response="default", description="错误返回结构", @SWG\Schema( type="array", @SWG\Items(ref="#/definitions/ThemeErrorRespones") ) )
@@ -151,17 +78,19 @@ class PcTemplate extends Controller
{
$authInfo = $request->get('auth');
$company_id = $request->get('company_id');
$page_type = $request->get('page_type', 'index');
$theme_pc_template_id = $request->input('theme_pc_template_id', '');
$page_type = $request->get('page_type', 'home');
$page_id = $request->input('page_id', '');
$params = [
'company_id' => $company_id,
'page_type' => $page_type,
'page_id' => $page_id,
'user_id' => $authInfo['user_id'] ?? 0,
'theme_pc_template_id' => $theme_pc_template_id,
];
$rules = [
'company_id' => ['required', '缺少company_id'],
'page_type' => ['required|in:home,header,footer,custom,product_list', '缺少或错误的page_type'],
'page_id' => ['required_if:page_type,custom', '缺少page_id'],
];
$error = validator_params($params, $rules);
if ($error) {
@@ -169,7 +98,7 @@ class PcTemplate extends Controller
}
$service = new ThemePcTemplateContentServices();
$result = $service->templateContent($params);
$result = $service->decorationContent($params);
return $this->response->array($result);
}
@@ -181,9 +110,15 @@ class PcTemplate extends Controller
$result = app('redis')->connection('companys')->get('pc_login_page:'.$companyId);
if (!$result) {
$result['logo'] = '';
$result['logo_light'] = '';
$result['logo_dark'] = '';
$result['background'] = '';
} else {
$result = json_decode($result, true);
$result['logo_light'] = $result['logo_light'] ?? ($result['logo'] ?? '');
$result['logo_dark'] = $result['logo_dark'] ?? ($result['logo'] ?? '');
$result['logo'] = $result['logo'] ?? ($result['logo_light'] ?? '');
$result['background'] = $result['background'] ?? '';
}
return $this->response->array($result);
}

View File

@@ -0,0 +1,82 @@
<?php
/**
* Copyright 2019-2026 ShopeX
*/
namespace ThemeBundle\Http\FrontApi\V1\Action;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use ThemeBundle\Entities\WebMenu;
use ThemeBundle\Entities\WebMenuItem;
use ThemeBundle\Services\WebMenuService;
use ThemeBundle\Transformers\WebMenuItemTransformer;
class WebMenuFrontAction extends Controller
{
/** GET /h5app/web/menus/{key} */
public function show(Request $request, string $key)
{
$companyId = $this->resolveCompanyId($request);
if ($companyId <= 0) {
return $this->response->errorUnauthorized('缺少 company 上下文');
}
$em = app('registry')->getManager('default');
/** @var \ThemeBundle\Repositories\WebMenuRepository $menuRepo */
$menuRepo = $em->getRepository(WebMenu::class);
$menu = $menuRepo->findActiveByCompanyAndKey($companyId, $key);
if (!$menu) {
return $this->response->errorNotFound('菜单不存在');
}
return $this->renderMenuTree($companyId, $menu);
}
/** GET /h5app/web/menus/id/{id} */
public function showById(Request $request, int $id)
{
$companyId = $this->resolveCompanyId($request);
if ($companyId <= 0) {
return $this->response->errorUnauthorized('缺少 company 上下文');
}
$em = app('registry')->getManager('default');
/** @var \ThemeBundle\Repositories\WebMenuRepository $menuRepo */
$menuRepo = $em->getRepository(WebMenu::class);
$menu = $menuRepo->findActiveByCompanyAndId($companyId, $id);
if (!$menu) {
return $this->response->errorNotFound('菜单不存在');
}
return $this->renderMenuTree($companyId, $menu);
}
private function resolveCompanyId(Request $request): int
{
$auth = $request->get('auth');
if (!is_array($auth) || empty($auth['company_id'])) {
$auth = $request->attributes->get('auth', []);
}
return (int) ($auth['company_id'] ?? 0);
}
private function renderMenuTree(int $companyId, WebMenu $menu)
{
$em = app('registry')->getManager('default');
/** @var \ThemeBundle\Repositories\WebMenuItemRepository $itemRepo */
$itemRepo = $em->getRepository(WebMenuItem::class);
$items = $itemRepo->findActiveByMenu($menu->getId(), $companyId);
$service = new WebMenuService();
$tree = $service->buildTree($items);
$tr = new WebMenuItemTransformer();
return $this->response->array([
'id' => $menu->getId(),
'name' => $menu->getName(),
'key' => $menu->getKey(),
'items' => array_map([$tr, 'transformFront'], $tree),
]);
}
}

View File

@@ -25,7 +25,7 @@ use Dingo\Api\Exception\ResourceException;
class ThemePcTemplateRepository extends EntityRepository
{
public $table = "theme_pc_template";
public $cols = ['theme_pc_template_id','company_id','template_title','template_description','page_type','status','version','created','updated','deleted_at'];
public $cols = ['theme_pc_template_id','company_id','distributor_id','template_title','template_description','page_type','status','version','created','updated','deleted_at'];
public $module = 'theme_pc_template'; // 多语言对应的模块
public $primaryKey = 'theme_pc_template_id'; // 主键对应data_id
@@ -159,7 +159,7 @@ class ThemePcTemplateRepository extends EntityRepository
private function setColumnNamesData($entity, $params)
{
foreach ($this->cols as $col) {
if (isset($params[$col])) {
if (array_key_exists($col, $params)) {
$fun = "set". str_replace(" ", "", ucwords(str_replace("_", " ", $col)));
if (method_exists($entity, $fun)) {
$entity->$fun($params[$col]);

View File

@@ -0,0 +1,135 @@
<?php
/**
* Copyright 2019-2026 ShopeX
*/
namespace ThemeBundle\Repositories;
use Doctrine\ORM\EntityRepository;
use ThemeBundle\Entities\WebMenuItem;
class WebMenuItemRepository extends EntityRepository
{
/**
* 前台:仅启用项
*
* @return WebMenuItem[]
*/
public function findActiveByMenu(int $menuId, int $companyId): array
{
return $this->createQueryBuilder('i')
->where('i.menuId = :mid AND i.companyId = :cid AND i.status = 1')
->setParameter('mid', $menuId)
->setParameter('cid', $companyId)
->orderBy('i.parentId', 'ASC')
->addOrderBy('i.sort', 'ASC')
->addOrderBy('i.id', 'ASC')
->getQuery()
->getResult();
}
/**
* 后台:含禁用项
*
* @return WebMenuItem[]
*/
public function findAllByMenu(int $menuId, int $companyId): array
{
return $this->createQueryBuilder('i')
->where('i.menuId = :mid AND i.companyId = :cid')
->setParameter('mid', $menuId)
->setParameter('cid', $companyId)
->orderBy('i.parentId', 'ASC')
->addOrderBy('i.sort', 'ASC')
->addOrderBy('i.id', 'ASC')
->getQuery()
->getResult();
}
public function findOneByIdMenuCompany(int $itemId, int $menuId, int $companyId): ?WebMenuItem
{
return $this->findOneBy([
'id' => $itemId,
'menuId' => $menuId,
'companyId' => $companyId,
]);
}
/**
* 批量统计各菜单下的菜单项数量(含子项)
*
* @param int[] $menuIds
*
* @return array<int, int> menu_id => count
*/
public function countItemsByMenuIds(int $companyId, array $menuIds): array
{
$menuIds = array_values(array_unique(array_filter(array_map('intval', $menuIds))));
if ($menuIds === []) {
return [];
}
$qb = $this->createQueryBuilder('i')
->select('i.menuId AS menuId')
->addSelect('COUNT(i.id) AS cnt')
->where('i.companyId = :cid')
->andWhere('i.menuId IN (:mids)')
->setParameter('cid', $companyId)
->setParameter('mids', $menuIds)
->groupBy('i.menuId');
$out = array_fill_keys($menuIds, 0);
foreach ($qb->getQuery()->getScalarResult() as $row) {
$mid = (int) ($row['menuId'] ?? $row['menu_id'] ?? 0);
$cnt = (int) ($row['cnt'] ?? 0);
if ($mid > 0) {
$out[$mid] = $cnt;
}
}
return $out;
}
/**
* 批量查询各菜单下「一级」菜单项名称parent_id = 0按 sort、id 排序后供列表展示
*
* @param int[] $menuIds
*
* @return array<int, string> menu_id => 名称以英文逗号连接
*/
public function findTopLevelItemNamesGroupedByMenuIds(int $companyId, array $menuIds): array
{
$menuIds = array_values(array_unique(array_filter(array_map('intval', $menuIds))));
if ($menuIds === []) {
return [];
}
$qb = $this->createQueryBuilder('i')
->select('i.menuId AS menuId')
->addSelect('i.name AS name')
->where('i.companyId = :cid')
->andWhere('i.menuId IN (:mids)')
->andWhere('i.parentId = 0')
->setParameter('cid', $companyId)
->setParameter('mids', $menuIds)
->orderBy('i.menuId', 'ASC')
->addOrderBy('i.sort', 'ASC')
->addOrderBy('i.id', 'ASC');
$grouped = [];
foreach ($menuIds as $mid) {
$grouped[$mid] = [];
}
foreach ($qb->getQuery()->getScalarResult() as $row) {
$mid = (int) ($row['menuId'] ?? $row['menu_id'] ?? 0);
$name = isset($row['name']) ? trim((string) $row['name']) : '';
if ($mid > 0 && $name !== '') {
$grouped[$mid][] = $name;
}
}
$out = [];
foreach ($grouped as $mid => $names) {
$out[$mid] = $names === [] ? '' : implode(',', $names);
}
return $out;
}
}

View File

@@ -0,0 +1,85 @@
<?php
/**
* Copyright 2019-2026 ShopeX
*/
namespace ThemeBundle\Repositories;
use Doctrine\ORM\EntityRepository;
use ThemeBundle\Entities\WebMenu;
class WebMenuRepository extends EntityRepository
{
public function findActiveByCompanyAndKey(int $companyId, string $key): ?WebMenu
{
return $this->findOneBy([
'companyId' => $companyId,
'key' => $key,
'status' => 1,
]);
}
public function findOneByIdAndCompany(int $id, int $companyId): ?WebMenu
{
return $this->findOneBy(['id' => $id, 'companyId' => $companyId]);
}
public function findActiveByCompanyAndId(int $companyId, int $id): ?WebMenu
{
return $this->findOneBy([
'id' => $id,
'companyId' => $companyId,
'status' => 1,
]);
}
/**
* @return WebMenu[]
*/
public function findPageByCompany(int $companyId, int $page = 1, int $pageSize = 20, ?string $name = null): array
{
$qb = $this->createQueryBuilder('m')
->where('m.companyId = :cid')
->setParameter('cid', $companyId);
$this->applyNameFilter($qb, $name);
return $qb->orderBy('m.id', 'DESC')
->setFirstResult(($page - 1) * $pageSize)
->setMaxResults($pageSize)
->getQuery()
->getResult();
}
public function countByCompany(int $companyId, ?string $name = null): int
{
$qb = $this->createQueryBuilder('m')
->select('COUNT(m.id)')
->where('m.companyId = :cid')
->setParameter('cid', $companyId);
$this->applyNameFilter($qb, $name);
return (int) $qb->getQuery()->getSingleScalarResult();
}
private function applyNameFilter($qb, ?string $name): void
{
$name = $name !== null ? trim($name) : '';
if ($name !== '') {
$qb->andWhere('m.name LIKE :menuName')->setParameter('menuName', '%' . $name . '%');
}
}
public function existsDuplicateKey(int $companyId, string $key, ?int $excludeId = null): bool
{
$qb = $this->createQueryBuilder('m')
->select('COUNT(m.id)')
->where('m.companyId = :cid AND m.key = :k')
->setParameter('cid', $companyId)
->setParameter('k', $key);
if ($excludeId !== null) {
$qb->andWhere('m.id != :eid')->setParameter('eid', $excludeId);
}
return (int) $qb->getQuery()->getSingleScalarResult() > 0;
}
}

View File

@@ -1533,6 +1533,18 @@ class PagesTemplateServices
public function getWidgetItems($params)
{
$eActivityId = (int) ($params['e_activity_id'] ?? 0);
if ($eActivityId > 0 && !empty($params['company_id'])) {
$activitiesService = new ActivitiesService();
$activityInfo = $activitiesService->entityRepository->getInfo([
'id' => $eActivityId,
'company_id' => $params['company_id'],
]);
if (!empty($activityInfo['distributor_id'])) {
$params['distributor_id'] = (int) $activityInfo['distributor_id'];
}
}
if ($params['data_type'] != 'sales') {
if (!isset($params['data_value']) || !$params['data_value']) {
return [

View File

@@ -86,6 +86,93 @@ class ThemePcTemplateContentServices
return $result;
}
/**
* 获取 PC 模板装修内容
*/
public function decorationContent($params)
{
$page_type = $params['page_type'] ?? '';
if (in_array($page_type, ['header', 'footer'])) {
return $this->formatDecorationContent($this->detail([
'company_id' => $params['company_id'],
'page_name' => $page_type,
]));
}
return $this->pageDecorationDetail($params);
}
private function pageDecorationDetail($params)
{
$company_id = $params['company_id'];
$theme_pc_template_id = $params['theme_pc_template_id'] ?? '';
$distributor_id = $params['distributor_id'] ?? 0;
$page_type = $params['page_type'] ?? 'index';
$page_id = $params['page_id'] ?? '';
$filter = [
'company_id' => $company_id,
];
if (!empty($theme_pc_template_id)) {
$filter['theme_pc_template_id'] = $theme_pc_template_id;
} else if ($page_type == 'custom' && !empty($page_id)) {
$filter['theme_pc_template_id'] = $page_id;
} else {
$filter['page_type'] = $page_type == 'home' ? 'index' : $page_type;
$filter['status'] = 1;
$filter['distributor_id'] = (int)$distributor_id;
}
$theme_pc_template_info = $this->themePcTemplateRepository->getInfo($filter);
if (empty($theme_pc_template_info)) {
return $this->emptyDecorationContent();
}
$_filter = [
'theme_pc_template_id' => $theme_pc_template_info['theme_pc_template_id']
];
$list = $this->themePcTemplateContentRepository->getLists($_filter, '*', 1, -1, ['theme_pc_template_content_id' => 'ASC']);
if (empty($list)) {
return $this->emptyDecorationContent();
}
$target = null;
foreach ($list as $value) {
if (strpos((string)($value['params'] ?? ''), 'ECX_SP_WEB_DECORATION_DSL_V1') !== false) {
$target = $value;
break;
}
}
if ($target === null) {
$target = $list[0];
}
return $this->formatDecorationContent($target);
}
private function formatDecorationContent($row)
{
if (empty($row) || !is_array($row)) {
return $this->emptyDecorationContent();
}
return [
'id' => $row['theme_pc_template_content_id'] ?? $row['id'] ?? 0,
'name' => $row['name'] ?? '',
'config' => $row['params'] ?? $row['config'] ?? '',
];
}
private function emptyDecorationContent()
{
return [
'id' => 0,
'name' => '',
'config' => '',
];
}
/**
* 获取模版内容
*/
@@ -93,6 +180,7 @@ class ThemePcTemplateContentServices
{
$company_id = $params['company_id'];
$theme_pc_template_id = $params['theme_pc_template_id'];
$distributor_id = $params['distributor_id'] ?? 0;
$filter = [
'company_id' => $company_id,
];
@@ -101,6 +189,7 @@ class ThemePcTemplateContentServices
} else {
$filter['page_type'] = $params['page_type'];
$filter['status'] = 1;
$filter['distributor_id'] = (int)$distributor_id;
}
$data = [];

View File

@@ -40,13 +40,15 @@ class ThemePcTemplateServices
public function lists($params)
{
$company_id = $params['company_id'];
$distributor_id = $params['distributor_id'] ?? 0;
$page_type = $params['page_type'];
$page_no = $params['page_no'];
$page_size = $params['page_size'];
$status = $params['status'];
$filter = [
'company_id' => $company_id
'company_id' => $company_id,
'distributor_id' => (int)$distributor_id
];
if (!empty($page_type)) {
$filter['page_type'] = $page_type;
@@ -69,12 +71,13 @@ class ThemePcTemplateServices
* @param int|null $exclude_template_id 排除的模板ID编辑时使用
* @throws ResourceException
*/
private function checkIndexTemplateStatus($company_id, $page_type, $status, $exclude_template_id = null)
private function checkIndexTemplateStatus($company_id, $page_type, $status, $exclude_template_id = null, $distributor_id = 0)
{
// 只检查首页且启用状态的情况
if ($page_type == 'index' && $status == 1) {
$filter = [
'company_id' => $company_id,
'distributor_id' => (int)$distributor_id,
'page_type' => 'index',
'status' => 1,
];
@@ -100,9 +103,13 @@ class ThemePcTemplateServices
$this->checkIndexTemplateStatus(
$params['company_id'],
$params['page_type'] ?? '',
$params['status'] ?? 2
$params['status'] ?? 2,
null,
$params['distributor_id'] ?? 0
);
$params['distributor_id'] = $params['distributor_id'] ?? 0;
$result = $this->themePcTemplateRepository->create($params);
return $result;
@@ -130,6 +137,9 @@ class ThemePcTemplateServices
// 确定 page_type优先使用传入的值否则使用数据库中的值
$page_type = $params['page_type'] ?? $pc_template_info['page_type'];
$status = $params['status'] ?? null;
$distributor_id = array_key_exists('distributor_id', $params)
? (int)$params['distributor_id']
: (int)($pc_template_info['distributor_id'] ?? 0);
// 检查首页模板启用状态
if (!empty($status)) {
@@ -137,7 +147,8 @@ class ThemePcTemplateServices
$company_id,
$page_type,
$status,
$theme_pc_template_id
$theme_pc_template_id,
$distributor_id
);
}
@@ -161,6 +172,10 @@ class ThemePcTemplateServices
$data['page_type'] = $params['page_type'];
}
if (array_key_exists('distributor_id', $params)) {
$data['distributor_id'] = (int)$params['distributor_id'];
}
$result = $this->themePcTemplateRepository->updateOneBy($filter, $data);
$conn->commit();
} catch (\Exception $e) {

View File

@@ -0,0 +1,338 @@
<?php
/**
* Copyright 2019-2026 ShopeX
*/
namespace ThemeBundle\Services;
use Dingo\Api\Exception\ResourceException;
use Doctrine\ORM\EntityManager;
use ThemeBundle\Entities\WebMenu;
use ThemeBundle\Entities\WebMenuItem;
use ThemeBundle\Repositories\WebMenuItemRepository;
use ThemeBundle\Repositories\WebMenuRepository;
class WebMenuService
{
private EntityManager $em;
private WebMenuRepository $menuRepo;
private WebMenuItemRepository $itemRepo;
public function __construct(?EntityManager $em = null)
{
$this->em = $em ?? app('registry')->getManager('default');
$this->menuRepo = $this->em->getRepository(WebMenu::class);
$this->itemRepo = $this->em->getRepository(WebMenuItem::class);
}
/**
* @return array{list: WebMenu[], total: int, page: int, page_size: int}
*/
public function listMenus(int $companyId, int $page = 1, int $pageSize = 20, ?string $name = null): array
{
$page = max(1, $page);
$pageSize = min(100, max(1, $pageSize));
$name = $name !== null ? trim($name) : null;
if ($name === '') {
$name = null;
}
$list = $this->menuRepo->findPageByCompany($companyId, $page, $pageSize, $name);
$total = $this->menuRepo->countByCompany($companyId, $name);
return [
'list' => $list,
'total' => $total,
'page' => $page,
'page_size' => $pageSize,
];
}
public function createMenu(int $companyId, array $data): WebMenu
{
$name = trim((string) ($data['name'] ?? ''));
$key = trim((string) ($data['key'] ?? ''));
if ($name === '' || $key === '') {
throw new ResourceException('name 与 key 不能为空');
}
if ($this->menuRepo->existsDuplicateKey($companyId, $key)) {
throw new ResourceException('同一店铺下 key 已存在');
}
$now = new \DateTimeImmutable();
$menu = new WebMenu();
$menu->setCompanyId($companyId);
$menu->setName($name);
$menu->setKey($key);
$menu->setStatus(isset($data['status']) ? (int) $data['status'] : 1);
$menu->setCreatedAt($now);
$menu->setUpdatedAt($now);
$this->em->persist($menu);
$this->em->flush();
return $menu;
}
public function updateMenu(int $id, int $companyId, array $data): WebMenu
{
$menu = $this->menuRepo->findOneByIdAndCompany($id, $companyId);
if (!$menu) {
throw new ResourceException('菜单不存在');
}
if (isset($data['name'])) {
$name = trim((string) $data['name']);
if ($name === '') {
throw new ResourceException('name 不能为空');
}
$menu->setName($name);
}
if (isset($data['key'])) {
$key = trim((string) $data['key']);
if ($key === '') {
throw new ResourceException('key 不能为空');
}
if ($this->menuRepo->existsDuplicateKey($companyId, $key, $id)) {
throw new ResourceException('同一店铺下 key 已存在');
}
$menu->setKey($key);
}
if (isset($data['status'])) {
$menu->setStatus((int) $data['status']);
}
$menu->setUpdatedAt(new \DateTimeImmutable());
$this->em->flush();
return $menu;
}
public function deleteMenu(int $id, int $companyId): void
{
$menu = $this->menuRepo->findOneByIdAndCompany($id, $companyId);
if (!$menu) {
throw new ResourceException('菜单不存在');
}
$this->em->createQuery(
'DELETE ThemeBundle\Entities\WebMenuItem i WHERE i.menuId = :mid AND i.companyId = :cid'
)->setParameter('mid', $id)->setParameter('cid', $companyId)->execute();
$this->em->remove($menu);
$this->em->flush();
}
public function createItem(int $menuId, int $companyId, array $data): WebMenuItem
{
$menu = $this->menuRepo->findOneByIdAndCompany($menuId, $companyId);
if (!$menu) {
throw new ResourceException('菜单不存在');
}
$name = trim((string) ($data['name'] ?? ''));
if ($name === '') {
throw new ResourceException('菜单项名称不能为空');
}
$parentId = isset($data['parent_id']) ? (int) $data['parent_id'] : 0;
if ($parentId > 0) {
$parent = $this->itemRepo->findOneByIdMenuCompany($parentId, $menuId, $companyId);
if (!$parent) {
throw new ResourceException('父级菜单项不存在');
}
}
$now = new \DateTimeImmutable();
$item = new WebMenuItem();
$item->setMenuId($menuId);
$item->setCompanyId($companyId);
$item->setParentId($parentId);
$item->setName($name);
$imageUrl = $data['image_url'] ?? null;
$item->setImageUrl($imageUrl !== null && $imageUrl !== '' ? trim((string) $imageUrl) : null);
$item->setLinkType(trim((string) ($data['link_type'] ?? 'url')) ?: 'url');
$linkValue = $data['link_value'] ?? null;
$item->setLinkValue($linkValue !== null && $linkValue !== '' ? (string) $linkValue : null);
$linkExtra = $data['link_extra'] ?? null;
$item->setLinkExtra($this->encodeLinkExtra($linkExtra));
$item->setSort(isset($data['sort']) ? (int) $data['sort'] : 0);
$item->setStatus(isset($data['status']) ? (int) $data['status'] : 1);
$item->setCreatedAt($now);
$item->setUpdatedAt($now);
$this->em->persist($item);
$this->em->flush();
return $item;
}
public function updateItem(int $itemId, int $menuId, int $companyId, array $data): WebMenuItem
{
$item = $this->itemRepo->findOneByIdMenuCompany($itemId, $menuId, $companyId);
if (!$item) {
throw new ResourceException('菜单项不存在');
}
if (isset($data['parent_id'])) {
$parentId = (int) $data['parent_id'];
if ($parentId === $itemId) {
throw new ResourceException('parent_id 不能指向自身');
}
if ($parentId > 0) {
$parent = $this->itemRepo->findOneByIdMenuCompany($parentId, $menuId, $companyId);
if (!$parent) {
throw new ResourceException('父级菜单项不存在');
}
}
$item->setParentId($parentId);
}
if (isset($data['name'])) {
$name = trim((string) $data['name']);
if ($name === '') {
throw new ResourceException('菜单项名称不能为空');
}
$item->setName($name);
}
if (array_key_exists('image_url', $data)) {
$imageUrl = $data['image_url'];
$item->setImageUrl($imageUrl !== null && $imageUrl !== '' ? trim((string) $imageUrl) : null);
}
if (array_key_exists('link_type', $data)) {
$item->setLinkType(trim((string) $data['link_type']) ?: 'url');
}
if (array_key_exists('link_value', $data)) {
$v = $data['link_value'];
$item->setLinkValue($v !== null && $v !== '' ? (string) $v : null);
}
if (array_key_exists('link_extra', $data)) {
$item->setLinkExtra($this->encodeLinkExtra($data['link_extra']));
}
if (isset($data['sort'])) {
$item->setSort((int) $data['sort']);
}
if (isset($data['status'])) {
$item->setStatus((int) $data['status']);
}
$item->setUpdatedAt(new \DateTimeImmutable());
$this->em->flush();
return $item;
}
public function deleteItem(int $itemId, int $menuId, int $companyId): void
{
$item = $this->itemRepo->findOneByIdMenuCompany($itemId, $menuId, $companyId);
if (!$item) {
throw new ResourceException('菜单项不存在');
}
$all = $this->itemRepo->findAllByMenu($menuId, $companyId);
$ids = $this->collectDescendantIds($itemId, $all);
foreach ($ids as $id) {
$e = $this->itemRepo->find($id);
if ($e) {
$this->em->remove($e);
}
}
$this->em->flush();
}
/**
* @param WebMenuItem[] $allItems
*
* @return int[]
*/
private function collectDescendantIds(int $rootId, array $allItems): array
{
$byParent = [];
foreach ($allItems as $i) {
$pid = $i->getParentId();
if (!isset($byParent[$pid])) {
$byParent[$pid] = [];
}
$byParent[$pid][] = $i->getId();
}
$ids = [];
$stack = [$rootId];
while ($stack) {
$id = array_pop($stack);
$ids[] = $id;
if (!empty($byParent[$id])) {
foreach ($byParent[$id] as $childId) {
$stack[] = $childId;
}
}
}
return $ids;
}
private function encodeLinkExtra($linkExtra): ?string
{
if ($linkExtra === null || $linkExtra === '') {
return null;
}
if (is_string($linkExtra)) {
return trim($linkExtra) !== '' ? $linkExtra : null;
}
if (!is_array($linkExtra) && !is_object($linkExtra)) {
return null;
}
$encoded = json_encode($linkExtra, JSON_UNESCAPED_UNICODE);
return $encoded !== false ? $encoded : null;
}
/**
* @param array<int, array{id:int, sort:int}>|array<int|string, int> $sorts
*/
public function batchUpdateSort(int $menuId, int $companyId, array $sorts): void
{
$menu = $this->menuRepo->findOneByIdAndCompany($menuId, $companyId);
if (!$menu) {
throw new ResourceException('菜单不存在');
}
if ($sorts === []) {
return;
}
$pairs = [];
if (isset($sorts[0]) && is_array($sorts[0])) {
foreach ($sorts as $row) {
if (!isset($row['id'])) {
continue;
}
$pairs[(int) $row['id']] = (int) ($row['sort'] ?? 0);
}
} else {
foreach ($sorts as $id => $sort) {
if (is_int($id) || ctype_digit((string) $id)) {
$pairs[(int) $id] = (int) $sort;
}
}
}
foreach ($pairs as $itemId => $sort) {
$item = $this->itemRepo->findOneByIdMenuCompany($itemId, $menuId, $companyId);
if ($item) {
$item->setSort($sort);
$item->setUpdatedAt(new \DateTimeImmutable());
}
}
$this->em->flush();
}
/**
* @param WebMenuItem[] $items
*
* @return array<int, array{item: WebMenuItem, children: array}>
*/
public function buildTree(array $items): array
{
$indexed = [];
foreach ($items as $item) {
$indexed[$item->getId()] = ['item' => $item, 'children' => []];
}
$roots = [];
foreach ($indexed as $id => &$node) {
$pid = $node['item']->getParentId();
if ($pid === 0) {
$roots[] = &$node;
} elseif (isset($indexed[$pid])) {
$indexed[$pid]['children'][] = &$node;
}
}
unset($node);
return $roots;
}
}

View File

@@ -0,0 +1,32 @@
<?php
/**
* Copyright 2019-2026 ShopeX
*/
namespace ThemeBundle\Transformers;
use League\Fractal\TransformerAbstract;
use ThemeBundle\Entities\WebMenuItem;
class WebMenuItemFlatTransformer extends TransformerAbstract
{
public function transform(WebMenuItem $item): array
{
$decodedExtra = json_decode((string) $item->getLinkExtra(), true);
return [
'id' => $item->getId(),
'menu_id' => $item->getMenuId(),
'parent_id' => $item->getParentId(),
'name' => $item->getName(),
'image_url' => $item->getImageUrl(),
'link_type' => $item->getLinkType(),
'link_value' => $item->getLinkValue(),
'link_extra' => is_array($decodedExtra) ? $decodedExtra : [],
'sort' => $item->getSort(),
'status' => $item->getStatus(),
'created_at' => $item->getCreatedAt() ? $item->getCreatedAt()->format('Y-m-d H:i:s') : null,
'updated_at' => $item->getUpdatedAt() ? $item->getUpdatedAt()->format('Y-m-d H:i:s') : null,
];
}
}

View File

@@ -0,0 +1,66 @@
<?php
/**
* Copyright 2019-2026 ShopeX
*/
namespace ThemeBundle\Transformers;
use League\Fractal\TransformerAbstract;
use ThemeBundle\Entities\WebMenuItem;
class WebMenuItemTransformer extends TransformerAbstract
{
/**
* @param array{item: \ThemeBundle\Entities\WebMenuItem, children: array<int, mixed>} $node
*/
public function transform(array $node): array
{
$item = $node['item'];
$children = $node['children'] ?? [];
return [
'id' => $item->getId(),
'parent_id' => $item->getParentId(),
'name' => $item->getName(),
'image_url' => $item->getImageUrl(),
'link_type' => $item->getLinkType(),
'link_value' => $item->getLinkValue(),
'link_extra' => $this->decodeLinkExtra($item),
'sort' => $item->getSort(),
'status' => $item->getStatus(),
'children' => array_map([$this, 'transform'], $children),
];
}
/**
* @param array{item: \ThemeBundle\Entities\WebMenuItem, children: array<int, mixed>} $node
*/
public function transformFront(array $node): array
{
$item = $node['item'];
$children = $node['children'] ?? [];
return [
'id' => $item->getId(),
'name' => $item->getName(),
'image_url' => $item->getImageUrl(),
'link_type' => $item->getLinkType(),
'link_value' => $item->getLinkValue(),
'link_extra' => $this->decodeLinkExtra($item),
'sort' => $item->getSort(),
'children' => array_map([$this, 'transformFront'], $children),
];
}
private function decodeLinkExtra(WebMenuItem $item): array
{
$raw = $item->getLinkExtra();
if (!$raw) {
return [];
}
$decoded = json_decode($raw, true);
return is_array($decoded) ? $decoded : [];
}
}

View File

@@ -0,0 +1,24 @@
<?php
/**
* Copyright 2019-2026 ShopeX
*/
namespace ThemeBundle\Transformers;
use League\Fractal\TransformerAbstract;
use ThemeBundle\Entities\WebMenu;
class WebMenuTransformer extends TransformerAbstract
{
public function transform(WebMenu $menu): array
{
return [
'id' => $menu->getId(),
'name' => $menu->getName(),
'key' => $menu->getKey(),
'status' => $menu->getStatus(),
'created_at' => $menu->getCreatedAt() ? $menu->getCreatedAt()->format('Y-m-d H:i:s') : null,
'updated_at' => $menu->getUpdatedAt() ? $menu->getUpdatedAt()->format('Y-m-d H:i:s') : null,
];
}
}

View File

@@ -0,0 +1,80 @@
<?php
/**
* Copyright 2019-2026 ShopeX
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace ThirdPartyBundle\Services\AppsCenter;
use GuzzleHttp\Client;
use Dingo\Api\Exception\ResourceException;
class AppsCenterService
{
public function fetchEmbedGoods($channel = null)
{
$channel = $channel ?: config('common.appcenter_channel', 'ecshopx');
$baseUrl = rtrim(config('common.appcenter_base_url'), '/');
$url = $baseUrl . '/appcenter/open/embed/' . rawurlencode($channel) . '/goods';
$client = new Client(['timeout' => 15, 'http_errors' => false]);
$response = $client->get($url);
$body = json_decode((string) $response->getBody(), true);
if (!is_array($body)) {
throw new ResourceException('获取应用中心商品失败');
}
if (($body['status'] ?? '') !== 'succ') {
throw new ResourceException($body['error'] ?: '获取应用中心商品失败');
}
return $body['data'] ?? [];
}
public function buildAppcenterUrl(array $params, $token)
{
$channel = $params['channel'];
$baseUrl = rtrim(config('common.appcenter_base_url'), '/');
$base = $baseUrl . '/appcenter/embed/' . rawurlencode($channel);
$query = [
'shopexid' => (string) $params['shopexid'],
'sys_node_id' => (string) $params['sys_node_id'],
'callback' => (string) $params['callback'],
'embed' => '1',
'nonce' => bin2hex(random_bytes(8)),
'timestamp' => (string) time(),
];
$query['sign'] = $this->signAppcenterParams($channel, $query, $token);
return $base . '?' . http_build_query($query);
}
public function signAppcenterParams($channel, array $query, $token)
{
$signData = $query;
$signData['channel'] = $channel;
unset($signData['sign']);
ksort($signData, SORT_STRING);
$lines = [];
foreach ($signData as $key => $value) {
$lines[] = $key . '=' . rawurlencode((string) $value);
}
$canonicalString = implode("\n", $lines);
return hash_hmac('sha256', $canonicalString, $token);
}
}