feat(fsgx): 完成全部24项开发任务 Phase1-7

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
This commit is contained in:
apple
2026-03-23 22:32:19 +08:00
parent 788ee0c0c0
commit 434aa8c69d
13098 changed files with 2008990 additions and 961 deletions

View File

@@ -0,0 +1,19 @@
<?php
namespace Smf\ConnectionPool;
class BorrowConnectionTimeoutException extends \Exception
{
protected $timeout;
public function getTimeout(): float
{
return $this->timeout;
}
public function setTimeout(float $timeout): self
{
$this->timeout = $timeout;
return $this;
}
}

View File

@@ -0,0 +1,279 @@
<?php
namespace Smf\ConnectionPool;
use Smf\ConnectionPool\Connectors\ConnectorInterface;
use Swoole\Coroutine\Channel;
use Swoole\Coroutine;
class ConnectionPool implements ConnectionPoolInterface
{
/**@var float The timeout of the operation channel */
const CHANNEL_TIMEOUT = 0.001;
/**@var float The minimum interval to check the idle connections */
const MIN_CHECK_IDLE_INTERVAL = 10;
/**@var string The key about the last active time of connection */
const KEY_LAST_ACTIVE_TIME = '__lat';
/**@var bool Whether the connection pool is initialized */
protected $initialized;
/**@var bool Whether the connection pool is closed */
protected $closed;
/**@var Channel The connection pool */
protected $pool;
/**@var ConnectorInterface The connector */
protected $connector;
/**@var array The config of connection */
protected $connectionConfig;
/**@var int Current all connection count */
protected $connectionCount = 0;
/**@var int The minimum number of active connections */
protected $minActive = 1;
/**@var int The maximum number of active connections */
protected $maxActive = 1;
/**@var float The maximum waiting time for connection, when reached, an exception will be thrown */
protected $maxWaitTime = 5;
/**@var float The maximum idle time for the connection, when reached, the connection will be removed from pool, and keep the least $minActive connections in the pool */
protected $maxIdleTime = 5;
/**@var float The interval to check idle connection */
protected $idleCheckInterval = 5;
/**@var int The timer id of balancer */
protected $balancerTimerId;
/**
* ConnectionPool constructor.
* @param array $poolConfig The minimum number of active connections, the detail keys:
* int minActive The minimum number of active connections
* int maxActive The maximum number of active connections
* float maxWaitTime The maximum waiting time for connection, when reached, an exception will be thrown
* float maxIdleTime The maximum idle time for the connection, when reached, the connection will be removed from pool, and keep the least $minActive connections in the pool
* float idleCheckInterval The interval to check idle connection
* @param ConnectorInterface $connector The connector instance of ConnectorInterface
* @param array $connectionConfig The config of connection
*/
public function __construct(array $poolConfig, ConnectorInterface $connector, array $connectionConfig)
{
$this->initialized = false;
$this->closed = false;
$this->minActive = $poolConfig['minActive'] ?? 20;
$this->maxActive = $poolConfig['maxActive'] ?? 100;
$this->maxWaitTime = $poolConfig['maxWaitTime'] ?? 5;
$this->maxIdleTime = $poolConfig['maxIdleTime'] ?? 30;
$poolConfig['idleCheckInterval'] = $poolConfig['idleCheckInterval'] ?? 15;
$this->idleCheckInterval = $poolConfig['idleCheckInterval'] >= static::MIN_CHECK_IDLE_INTERVAL ? $poolConfig['idleCheckInterval'] : static::MIN_CHECK_IDLE_INTERVAL;
$this->connectionConfig = $connectionConfig;
$this->connector = $connector;
}
/**
* Initialize the connection pool
* @return bool
*/
public function init(): bool
{
if ($this->initialized) {
return false;
}
$this->initialized = true;
$this->pool = new Channel($this->maxActive);
$this->balancerTimerId = $this->startBalanceTimer($this->idleCheckInterval);
Coroutine::create(function () {
for ($i = 0; $i < $this->minActive; $i++) {
$connection = $this->createConnection();
$ret = $this->pool->push($connection, static::CHANNEL_TIMEOUT);
if ($ret === false) {
$this->removeConnection($connection);
}
}
});
return true;
}
/**
* Borrow a connection from the connection pool, throw an exception if timeout
* @return mixed The connection resource
* @throws BorrowConnectionTimeoutException
* @throws \RuntimeException
*/
public function borrow()
{
if (!$this->initialized) {
throw new \RuntimeException('Please initialize the connection pool first, call $pool->init().');
}
if ($this->pool->isEmpty()) {
// Create more connections
if ($this->connectionCount < $this->maxActive) {
return $this->createConnection();
}
}
$connection = $this->pool->pop($this->maxWaitTime);
if ($connection === false) {
$exception = new BorrowConnectionTimeoutException(sprintf(
'Borrow the connection timeout in %.2f(s), connections in pool: %d, all connections: %d',
$this->maxWaitTime,
$this->pool->length(),
$this->connectionCount
));
$exception->setTimeout($this->maxWaitTime);
throw $exception;
}
if ($this->connector->isConnected($connection)) {
// Reset the connection for the connected connection
$this->connector->reset($connection, $this->connectionConfig);
} else {
// Remove the disconnected connection, then create a new connection
$this->removeConnection($connection);
$connection = $this->createConnection();
}
return $connection;
}
/**
* Return a connection to the connection pool
* @param mixed $connection The connection resource
* @return bool
*/
public function return($connection): bool
{
if (!$this->connector->validate($connection)) {
throw new \RuntimeException('Connection of unexpected type returned.');
}
if (!$this->initialized) {
throw new \RuntimeException('Please initialize the connection pool first, call $pool->init().');
}
if ($this->pool->isFull()) {
// Discard the connection
$this->removeConnection($connection);
return false;
}
$connection->{static::KEY_LAST_ACTIVE_TIME} = time();
$ret = $this->pool->push($connection, static::CHANNEL_TIMEOUT);
if ($ret === false) {
$this->removeConnection($connection);
}
return true;
}
/**
* Get the number of created connections
* @return int
*/
public function getConnectionCount(): int
{
return $this->connectionCount;
}
/**
* Get the number of idle connections
* @return int
*/
public function getIdleCount(): int
{
return $this->pool->length();
}
/**
* Close the connection pool and disconnect all connections
* @return bool
*/
public function close(): bool
{
if (!$this->initialized) {
return false;
}
if ($this->closed) {
return false;
}
$this->closed = true;
swoole_timer_clear($this->balancerTimerId);
Coroutine::create(function () {
while (true) {
if ($this->pool->isEmpty()) {
break;
}
$connection = $this->pool->pop(static::CHANNEL_TIMEOUT);
if ($connection !== false) {
$this->connector->disconnect($connection);
}
}
$this->pool->close();
});
return true;
}
public function __destruct()
{
$this->close();
}
protected function startBalanceTimer(float $interval)
{
return swoole_timer_tick(round($interval) * 1000, function () {
$now = time();
$validConnections = [];
while (true) {
if ($this->closed) {
break;
}
if ($this->connectionCount <= $this->minActive) {
break;
}
if ($this->pool->isEmpty()) {
break;
}
$connection = $this->pool->pop(static::CHANNEL_TIMEOUT);
if ($connection === false) {
continue;
}
$lastActiveTime = $connection->{static::KEY_LAST_ACTIVE_TIME} ?? 0;
if ($now - $lastActiveTime < $this->maxIdleTime) {
$validConnections[] = $connection;
} else {
$this->removeConnection($connection);
}
}
foreach ($validConnections as $validConnection) {
$ret = $this->pool->push($validConnection, static::CHANNEL_TIMEOUT);
if ($ret === false) {
$this->removeConnection($validConnection);
}
}
});
}
protected function createConnection()
{
$this->connectionCount++;
$connection = $this->connector->connect($this->connectionConfig);
$connection->{static::KEY_LAST_ACTIVE_TIME} = time();
return $connection;
}
protected function removeConnection($connection)
{
$this->connectionCount--;
Coroutine::create(function () use ($connection) {
try {
$this->connector->disconnect($connection);
} catch (\Throwable $e) {
// Ignore this exception.
}
});
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace Smf\ConnectionPool;
interface ConnectionPoolInterface
{
/**
* Initialize the connection pool
* @return bool
*/
public function init(): bool;
/**
* Return a connection to the connection pool
* @param mixed $connection
* @return bool
*/
public function return($connection): bool;
/**
* Borrow a connection to the connection pool
* @return mixed
* @throws BorrowConnectionTimeoutException
*/
public function borrow();
/**
* Close the connection pool, release the resource of all connections
* @return bool
*/
public function close(): bool;
}

View File

@@ -0,0 +1,51 @@
<?php
namespace Smf\ConnectionPool;
trait ConnectionPoolTrait
{
/**
* @var ConnectionPool[] $pools
*/
protected $pools = [];
/**
* Add a connection pool
* @param string $key
* @param ConnectionPool $pool
*/
public function addConnectionPool(string $key, ConnectionPool $pool)
{
$this->pools[$key] = $pool;
}
/**
* Get a connection pool by key
* @param string $key
* @return ConnectionPool
*/
public function getConnectionPool(string $key): ConnectionPool
{
return $this->pools[$key];
}
/**
* Close the connection by key
* @param string $key
* @return bool
*/
public function closeConnectionPool(string $key)
{
return $this->pools[$key]->close();
}
/**
* Close all connection pools
*/
public function closeConnectionPools()
{
foreach ($this->pools as $pool) {
$pool->close();
}
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace Smf\ConnectionPool\Connectors;
interface ConnectorInterface
{
/**
* Connect to the specified Server and returns the connection resource
* @param array $config
* @return mixed
*/
public function connect(array $config);
/**
* Disconnect and free resources
* @param mixed $connection
* @return mixed
*/
public function disconnect($connection);
/**
* Whether the connection is established
* @param mixed $connection
* @return bool
*/
public function isConnected($connection): bool;
/**
* Reset the connection
* @param mixed $connection
* @param array $config
* @return mixed
*/
public function reset($connection, array $config);
/**
* Validate the connection
*
* @param mixed $connection
* @return bool
*/
public function validate($connection): bool;
}

View File

@@ -0,0 +1,39 @@
<?php
namespace Smf\ConnectionPool\Connectors;
use Swoole\Coroutine\MySQL;
class CoroutineMySQLConnector implements ConnectorInterface
{
public function connect(array $config)
{
$connection = new MySQL();
if ($connection->connect($config) === false) {
throw new \RuntimeException(sprintf('Failed to connect MySQL server: [%d] %s', $connection->connect_errno, $connection->connect_error));
}
return $connection;
}
public function disconnect($connection)
{
/**@var MySQL $connection */
$connection->close();
}
public function isConnected($connection): bool
{
/**@var MySQL $connection */
return $connection->connected;
}
public function reset($connection, array $config)
{
}
public function validate($connection): bool
{
return $connection instanceof MySQL;
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace Smf\ConnectionPool\Connectors;
use Swoole\Coroutine\PostgreSQL;
class CoroutinePostgreSQLConnector implements ConnectorInterface
{
public function connect(array $config)
{
if (!isset($config['connection_strings'])) {
throw new \InvalidArgumentException('The key "connection_string" is missing.');
}
$connection = new PostgreSQL();
$ret = $connection->connect($config['connection_strings']);
if ($ret === false) {
throw new \RuntimeException(sprintf('Failed to connect PostgreSQL server: %s', $connection->error));
}
return $connection;
}
public function disconnect($connection)
{
/**@var PostgreSQL $connection */
}
public function isConnected($connection): bool
{
/**@var PostgreSQL $connection */
return true;
}
public function reset($connection, array $config)
{
/**@var PostgreSQL $connection */
}
public function validate($connection): bool
{
return $connection instanceof PostgreSQL;
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace Smf\ConnectionPool\Connectors;
use Swoole\Coroutine\Redis;
class CoroutineRedisConnector implements ConnectorInterface
{
public function connect(array $config)
{
$connection = new Redis($config['options'] ?? []);
$ret = $connection->connect($config['host'], $config['port']);
if ($ret === false) {
throw new \RuntimeException(sprintf('Failed to connect Redis server: [%s] %s', $connection->errCode, $connection->errMsg));
}
if (isset($config['password'])) {
$config['password'] = (string)$config['password'];
if ($config['password'] !== '') {
$connection->auth($config['password']);
}
}
if (isset($config['database'])) {
$connection->select($config['database']);
}
return $connection;
}
public function disconnect($connection)
{
/**@var Redis $connection */
$connection->close();
}
public function isConnected($connection): bool
{
/**@var Redis $connection */
return $connection->connected;
}
public function reset($connection, array $config)
{
/**@var Redis $connection */
$connection->setDefer(false);
if (isset($config['database'])) {
$connection->select($config['database']);
}
}
public function validate($connection): bool
{
return $connection instanceof Redis;
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace Smf\ConnectionPool\Connectors;
class PDOConnector implements ConnectorInterface
{
public function connect(array $config)
{
try {
$connection = new \PDO($config['dsn'], $config['username'] ?? '', $config['password'] ?? '', $config['options'] ?? []);
} catch (\Throwable $e) {
throw new \RuntimeException(sprintf('Failed to connect the requested database: [%d] %s', $e->getCode(), $e->getMessage()));
}
return $connection;
}
public function disconnect($connection)
{
/**@var \PDO $connection */
$connection = null;
}
public function isConnected($connection): bool
{
/**@var \PDO $connection */
try {
return !!@$connection->getAttribute(\PDO::ATTR_SERVER_INFO);
} catch (\Throwable $e) {
return false;
}
}
public function reset($connection, array $config)
{
}
public function validate($connection): bool
{
return $connection instanceof \PDO;
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace Smf\ConnectionPool\Connectors;
class PhpRedisConnector implements ConnectorInterface
{
public function connect(array $config)
{
$connection = new \Redis();
$ret = $connection->connect($config['host'], $config['port'], $config['timeout'] ?? 10);
if ($ret === false) {
throw new \RuntimeException(sprintf('Failed to connect Redis server: %s', $connection->getLastError()));
}
if (isset($config['password'])) {
$config['password'] = (string)$config['password'];
if ($config['password'] !== '') {
$connection->auth($config['password']);
}
}
if (isset($config['database'])) {
$connection->select($config['database']);
}
foreach ($config['options'] ?? [] as $key => $value) {
$connection->setOption($key, $value);
}
return $connection;
}
public function disconnect($connection)
{
/**@var \Redis $connection */
$connection->close();
}
public function isConnected($connection): bool
{
/**@var \Redis $connection */
return $connection->isConnected();
}
public function reset($connection, array $config)
{
/**@var \Redis $connection */
if (isset($config['database'])) {
$connection->select($config['database']);
}
}
public function validate($connection): bool
{
return $connection instanceof \Redis;
}
}