Phase1 后端核心:
- 新增 fsgx_v1.sql 迁移脚本(is_queue_goods/frozen_points/available_points/no_assess)
- SystemConfigServices 返佣设置扩展(周期人数/分档比例/范围/时机)
- StoreOrderCreateServices 周期循环佣金计算
- StoreOrderTakeServices 佣金发放后同步冻结积分
- StoreProductServices/StoreProduct 保存 is_queue_goods
Phase2 后端接口:
- GET /api/hjf/brokerage/progress 佣金周期进度
- GET /api/hjf/assets/overview 资产总览
- HjfPointsServices 每日 frozen_points 0.4‰ 释放定时任务
- PUT /adminapi/hjf/member/{uid}/no_assess 不考核接口
- GET /adminapi/hjf/points/release_log 积分日志接口
Phase3 前端清理:
- hjfCustom.js 路由精简(仅保留 points/log)
- hjfQueue.js/hjfMember.js API 清理/重定向至 CRMEB 原生接口
- pages.json 公排→推荐佣金/佣金记录/佣金规则
Phase4-5 前端改造:
- queue/status.vue 推荐佣金进度页整体重写
- 商品详情/订单确认/支付结果页文案与逻辑改造
- 个人中心/资产页/引导页/规则页文案改造
- HjfQueueProgress/HjfRefundNotice/HjfAssetCard 组件改造
- 推广中心嵌入佣金进度摘要
- hjfMockData.js 全量更新(公排字段→佣金字段)
Phase6 Admin 增强:
- 用户列表新增 frozen_points/available_points 列及不考核操作按钮
- hjfPoints.js USE_MOCK=false 对接真实积分日志接口
Phase7 配置文档:
- docs/fsgx-phase7-config-checklist.md 后台配置与全链路验收清单
Made-with: Cursor
257 lines
7.3 KiB
PHP
257 lines
7.3 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
/**
|
|
* Copyright (c) 2017-2022 Andreas Möller
|
|
*
|
|
* For the full copyright and license information, please view
|
|
* the LICENSE.md file that was distributed with this source code.
|
|
*
|
|
* @see https://github.com/ergebnis/classy
|
|
*/
|
|
|
|
namespace Ergebnis\Classy;
|
|
|
|
final class Constructs
|
|
{
|
|
/**
|
|
* Returns an array of names of classy constructs (classes, interfaces, traits) found in source.
|
|
*
|
|
* @throws Exception\ParseError
|
|
*
|
|
* @return array<int, Construct>
|
|
*/
|
|
public static function fromSource(string $source): array
|
|
{
|
|
$constructs = [];
|
|
|
|
try {
|
|
$sequence = \token_get_all(
|
|
$source,
|
|
\TOKEN_PARSE,
|
|
);
|
|
} catch (\ParseError $exception) {
|
|
throw Exception\ParseError::fromParseError($exception);
|
|
}
|
|
|
|
$count = \count($sequence);
|
|
$namespacePrefix = '';
|
|
|
|
$namespaceSegmentOrNamespaceTokens = [
|
|
\T_STRING,
|
|
\T_NAME_QUALIFIED,
|
|
];
|
|
|
|
$classyTokens = [
|
|
\T_CLASS,
|
|
\T_INTERFACE,
|
|
\T_TRAIT,
|
|
];
|
|
|
|
// https://wiki.php.net/rfc/enumerations
|
|
if (\PHP_VERSION_ID >= 80100 && \defined('T_ENUM')) {
|
|
$classyTokens = [
|
|
\T_CLASS,
|
|
\T_ENUM,
|
|
\T_INTERFACE,
|
|
\T_TRAIT,
|
|
];
|
|
}
|
|
|
|
for ($index = 0; $index < $count; ++$index) {
|
|
$token = $sequence[$index];
|
|
|
|
// collect namespace name
|
|
if (\is_array($token) && \T_NAMESPACE === $token[0]) {
|
|
$namespaceSegments = [];
|
|
|
|
// collect namespace segments
|
|
for ($index = self::significantAfter($index, $sequence, $count); $index < $count; ++$index) {
|
|
$token = $sequence[$index];
|
|
|
|
if (\is_array($token) && !\in_array($token[0], $namespaceSegmentOrNamespaceTokens, true)) {
|
|
continue;
|
|
}
|
|
|
|
$content = self::content($token);
|
|
|
|
if (\in_array($content, ['{', ';'], true)) {
|
|
break;
|
|
}
|
|
|
|
$namespaceSegments[] = $content;
|
|
}
|
|
|
|
$namespace = \implode('\\', $namespaceSegments);
|
|
$namespacePrefix = $namespace . '\\';
|
|
}
|
|
|
|
// skip non-classy tokens
|
|
if (!\is_array($token) || !\in_array($token[0], $classyTokens, true)) {
|
|
continue;
|
|
}
|
|
|
|
// skip anonymous classes
|
|
if (\T_CLASS === $token[0]) {
|
|
$current = self::significantBefore($index, $sequence);
|
|
$token = $sequence[$current];
|
|
|
|
// if significant token before T_CLASS is T_NEW, it's an instantiation of an anonymous class
|
|
if (\is_array($token) && \T_NEW === $token[0]) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
$index = self::significantAfter($index, $sequence, $count);
|
|
$token = $sequence[$index];
|
|
|
|
$constructs[] = Construct::fromName($namespacePrefix . self::content($token));
|
|
}
|
|
|
|
\usort($constructs, static function (Construct $a, Construct $b): int {
|
|
return \strcmp(
|
|
$a->name(),
|
|
$b->name(),
|
|
);
|
|
});
|
|
|
|
return $constructs;
|
|
}
|
|
|
|
/**
|
|
* Returns an array of constructs defined in a directory.
|
|
*
|
|
* @throws Exception\DirectoryDoesNotExist
|
|
* @throws Exception\MultipleDefinitionsFound
|
|
*
|
|
* @return array<int, Construct>
|
|
*/
|
|
public static function fromDirectory(string $directory): array
|
|
{
|
|
if (!\is_dir($directory)) {
|
|
throw Exception\DirectoryDoesNotExist::fromDirectory($directory);
|
|
}
|
|
|
|
$iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator(
|
|
$directory,
|
|
\RecursiveDirectoryIterator::FOLLOW_SYMLINKS,
|
|
));
|
|
|
|
$constructs = [];
|
|
|
|
foreach ($iterator as $fileInfo) {
|
|
/** @var \SplFileInfo $fileInfo */
|
|
if (!$fileInfo->isFile()) {
|
|
continue;
|
|
}
|
|
|
|
if ($fileInfo->getBasename('.php') === $fileInfo->getBasename()) {
|
|
continue;
|
|
}
|
|
|
|
/** @var string $fileName */
|
|
$fileName = $fileInfo->getRealPath();
|
|
|
|
/** @var string $source */
|
|
$source = \file_get_contents($fileName);
|
|
|
|
try {
|
|
$constructsFromFile = self::fromSource($source);
|
|
} catch (Exception\ParseError $exception) {
|
|
throw Exception\ParseError::fromFileNameAndParseError(
|
|
$fileName,
|
|
$exception,
|
|
);
|
|
}
|
|
|
|
if (0 === \count($constructsFromFile)) {
|
|
continue;
|
|
}
|
|
|
|
foreach ($constructsFromFile as $construct) {
|
|
$name = $construct->name();
|
|
|
|
if (\array_key_exists($name, $constructs)) {
|
|
$construct = $constructs[$name];
|
|
}
|
|
|
|
$constructs[$name] = $construct->definedIn($fileName);
|
|
}
|
|
}
|
|
|
|
\usort($constructs, static function (Construct $a, Construct $b): int {
|
|
return \strcmp(
|
|
$a->name(),
|
|
$b->name(),
|
|
);
|
|
});
|
|
|
|
$constructsWithMultipleDefinitions = \array_filter($constructs, static function (Construct $construct): bool {
|
|
return 1 < \count($construct->fileNames());
|
|
});
|
|
|
|
if (0 < \count($constructsWithMultipleDefinitions)) {
|
|
throw Exception\MultipleDefinitionsFound::fromConstructs($constructsWithMultipleDefinitions);
|
|
}
|
|
|
|
return \array_values($constructs);
|
|
}
|
|
|
|
/**
|
|
* Returns the index of the significant token after the index.
|
|
*
|
|
* @param array<int, array{0: int, 1: string, 2: int}|string> $sequence
|
|
*/
|
|
private static function significantAfter(int $index, array $sequence, int $count): int
|
|
{
|
|
for ($current = $index + 1; $current < $count; ++$current) {
|
|
$token = $sequence[$current];
|
|
|
|
if (\is_array($token) && \in_array($token[0], [\T_COMMENT, \T_DOC_COMMENT, \T_WHITESPACE], true)) {
|
|
continue;
|
|
}
|
|
|
|
return $current;
|
|
}
|
|
|
|
throw Exception\ShouldNotHappen::create();
|
|
}
|
|
|
|
/**
|
|
* Returns the index of the significant token after the index.
|
|
*
|
|
* @param array<int, array{0: int, 1: string, 2: int}|string> $sequence
|
|
*/
|
|
private static function significantBefore(
|
|
int $index,
|
|
array $sequence,
|
|
): int {
|
|
for ($current = $index - 1; -1 < $current; --$current) {
|
|
$token = $sequence[$current];
|
|
|
|
if (\is_array($token) && \in_array($token[0], [\T_COMMENT, \T_DOC_COMMENT, \T_WHITESPACE], true)) {
|
|
continue;
|
|
}
|
|
|
|
return $current;
|
|
}
|
|
|
|
throw Exception\ShouldNotHappen::create();
|
|
}
|
|
|
|
/**
|
|
* Returns the string content of a token.
|
|
*
|
|
* @param array{0: int, 1: string, 2: int}|string $token
|
|
*/
|
|
private static function content($token): string
|
|
{
|
|
if (\is_array($token)) {
|
|
return $token[1];
|
|
}
|
|
|
|
return $token;
|
|
}
|
|
}
|