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
294 lines
9.1 KiB
PHP
294 lines
9.1 KiB
PHP
<?php
|
|
|
|
/*
|
|
* This file is part of the Symfony package.
|
|
*
|
|
* (c) Fabien Potencier <fabien@symfony.com>
|
|
*
|
|
* For the full copyright and license information, please view the LICENSE
|
|
* file that was distributed with this source code.
|
|
*/
|
|
|
|
namespace Symfony\Component\HttpFoundation;
|
|
|
|
/**
|
|
* HTTP header utility functions.
|
|
*
|
|
* @author Christian Schmidt <github@chsc.dk>
|
|
*/
|
|
class HeaderUtils
|
|
{
|
|
public const DISPOSITION_ATTACHMENT = 'attachment';
|
|
public const DISPOSITION_INLINE = 'inline';
|
|
|
|
/**
|
|
* This class should not be instantiated.
|
|
*/
|
|
private function __construct()
|
|
{
|
|
}
|
|
|
|
/**
|
|
* Splits an HTTP header by one or more separators.
|
|
*
|
|
* Example:
|
|
*
|
|
* HeaderUtils::split("da, en-gb;q=0.8", ",;")
|
|
* // => ['da'], ['en-gb', 'q=0.8']]
|
|
*
|
|
* @param string $separators List of characters to split on, ordered by
|
|
* precedence, e.g. ",", ";=", or ",;="
|
|
*
|
|
* @return array Nested array with as many levels as there are characters in
|
|
* $separators
|
|
*/
|
|
public static function split(string $header, string $separators): array
|
|
{
|
|
$quotedSeparators = preg_quote($separators, '/');
|
|
|
|
preg_match_all('
|
|
/
|
|
(?!\s)
|
|
(?:
|
|
# quoted-string
|
|
"(?:[^"\\\\]|\\\\.)*(?:"|\\\\|$)
|
|
|
|
|
# token
|
|
[^"'.$quotedSeparators.']+
|
|
)+
|
|
(?<!\s)
|
|
|
|
|
# separator
|
|
\s*
|
|
(?<separator>['.$quotedSeparators.'])
|
|
\s*
|
|
/x', trim($header), $matches, \PREG_SET_ORDER);
|
|
|
|
return self::groupParts($matches, $separators);
|
|
}
|
|
|
|
/**
|
|
* Combines an array of arrays into one associative array.
|
|
*
|
|
* Each of the nested arrays should have one or two elements. The first
|
|
* value will be used as the keys in the associative array, and the second
|
|
* will be used as the values, or true if the nested array only contains one
|
|
* element. Array keys are lowercased.
|
|
*
|
|
* Example:
|
|
*
|
|
* HeaderUtils::combine([["foo", "abc"], ["bar"]])
|
|
* // => ["foo" => "abc", "bar" => true]
|
|
*/
|
|
public static function combine(array $parts): array
|
|
{
|
|
$assoc = [];
|
|
foreach ($parts as $part) {
|
|
$name = strtolower($part[0]);
|
|
$value = $part[1] ?? true;
|
|
$assoc[$name] = $value;
|
|
}
|
|
|
|
return $assoc;
|
|
}
|
|
|
|
/**
|
|
* Joins an associative array into a string for use in an HTTP header.
|
|
*
|
|
* The key and value of each entry are joined with "=", and all entries
|
|
* are joined with the specified separator and an additional space (for
|
|
* readability). Values are quoted if necessary.
|
|
*
|
|
* Example:
|
|
*
|
|
* HeaderUtils::toString(["foo" => "abc", "bar" => true, "baz" => "a b c"], ",")
|
|
* // => 'foo=abc, bar, baz="a b c"'
|
|
*/
|
|
public static function toString(array $assoc, string $separator): string
|
|
{
|
|
$parts = [];
|
|
foreach ($assoc as $name => $value) {
|
|
if (true === $value) {
|
|
$parts[] = $name;
|
|
} else {
|
|
$parts[] = $name.'='.self::quote($value);
|
|
}
|
|
}
|
|
|
|
return implode($separator.' ', $parts);
|
|
}
|
|
|
|
/**
|
|
* Encodes a string as a quoted string, if necessary.
|
|
*
|
|
* If a string contains characters not allowed by the "token" construct in
|
|
* the HTTP specification, it is backslash-escaped and enclosed in quotes
|
|
* to match the "quoted-string" construct.
|
|
*/
|
|
public static function quote(string $s): string
|
|
{
|
|
if (preg_match('/^[a-z0-9!#$%&\'*.^_`|~-]+$/i', $s)) {
|
|
return $s;
|
|
}
|
|
|
|
return '"'.addcslashes($s, '"\\"').'"';
|
|
}
|
|
|
|
/**
|
|
* Decodes a quoted string.
|
|
*
|
|
* If passed an unquoted string that matches the "token" construct (as
|
|
* defined in the HTTP specification), it is passed through verbatimly.
|
|
*/
|
|
public static function unquote(string $s): string
|
|
{
|
|
return preg_replace('/\\\\(.)|"/', '$1', $s);
|
|
}
|
|
|
|
/**
|
|
* Generates an HTTP Content-Disposition field-value.
|
|
*
|
|
* @param string $disposition One of "inline" or "attachment"
|
|
* @param string $filename A unicode string
|
|
* @param string $filenameFallback A string containing only ASCII characters that
|
|
* is semantically equivalent to $filename. If the filename is already ASCII,
|
|
* it can be omitted, or just copied from $filename
|
|
*
|
|
* @throws \InvalidArgumentException
|
|
*
|
|
* @see RFC 6266
|
|
*/
|
|
public static function makeDisposition(string $disposition, string $filename, string $filenameFallback = ''): string
|
|
{
|
|
if (!\in_array($disposition, [self::DISPOSITION_ATTACHMENT, self::DISPOSITION_INLINE])) {
|
|
throw new \InvalidArgumentException(sprintf('The disposition must be either "%s" or "%s".', self::DISPOSITION_ATTACHMENT, self::DISPOSITION_INLINE));
|
|
}
|
|
|
|
if ('' === $filenameFallback) {
|
|
$filenameFallback = $filename;
|
|
}
|
|
|
|
// filenameFallback is not ASCII.
|
|
if (!preg_match('/^[\x20-\x7e]*$/', $filenameFallback)) {
|
|
throw new \InvalidArgumentException('The filename fallback must only contain ASCII characters.');
|
|
}
|
|
|
|
// percent characters aren't safe in fallback.
|
|
if (str_contains($filenameFallback, '%')) {
|
|
throw new \InvalidArgumentException('The filename fallback cannot contain the "%" character.');
|
|
}
|
|
|
|
// path separators aren't allowed in either.
|
|
if (str_contains($filename, '/') || str_contains($filename, '\\') || str_contains($filenameFallback, '/') || str_contains($filenameFallback, '\\')) {
|
|
throw new \InvalidArgumentException('The filename and the fallback cannot contain the "/" and "\\" characters.');
|
|
}
|
|
|
|
$params = ['filename' => $filenameFallback];
|
|
if ($filename !== $filenameFallback) {
|
|
$params['filename*'] = "utf-8''".rawurlencode($filename);
|
|
}
|
|
|
|
return $disposition.'; '.self::toString($params, ';');
|
|
}
|
|
|
|
/**
|
|
* Like parse_str(), but preserves dots in variable names.
|
|
*/
|
|
public static function parseQuery(string $query, bool $ignoreBrackets = false, string $separator = '&'): array
|
|
{
|
|
$q = [];
|
|
|
|
foreach (explode($separator, $query) as $v) {
|
|
if (false !== $i = strpos($v, "\0")) {
|
|
$v = substr($v, 0, $i);
|
|
}
|
|
|
|
if (false === $i = strpos($v, '=')) {
|
|
$k = urldecode($v);
|
|
$v = '';
|
|
} else {
|
|
$k = urldecode(substr($v, 0, $i));
|
|
$v = substr($v, $i);
|
|
}
|
|
|
|
if (false !== $i = strpos($k, "\0")) {
|
|
$k = substr($k, 0, $i);
|
|
}
|
|
|
|
$k = ltrim($k, ' ');
|
|
|
|
if ($ignoreBrackets) {
|
|
$q[$k][] = urldecode(substr($v, 1));
|
|
|
|
continue;
|
|
}
|
|
|
|
if (false === $i = strpos($k, '[')) {
|
|
$q[] = bin2hex($k).$v;
|
|
} else {
|
|
$q[] = bin2hex(substr($k, 0, $i)).rawurlencode(substr($k, $i)).$v;
|
|
}
|
|
}
|
|
|
|
if ($ignoreBrackets) {
|
|
return $q;
|
|
}
|
|
|
|
parse_str(implode('&', $q), $q);
|
|
|
|
$query = [];
|
|
|
|
foreach ($q as $k => $v) {
|
|
if (false !== $i = strpos($k, '_')) {
|
|
$query[substr_replace($k, hex2bin(substr($k, 0, $i)).'[', 0, 1 + $i)] = $v;
|
|
} else {
|
|
$query[hex2bin($k)] = $v;
|
|
}
|
|
}
|
|
|
|
return $query;
|
|
}
|
|
|
|
private static function groupParts(array $matches, string $separators, bool $first = true): array
|
|
{
|
|
$separator = $separators[0];
|
|
$partSeparators = substr($separators, 1);
|
|
|
|
$i = 0;
|
|
$partMatches = [];
|
|
$previousMatchWasSeparator = false;
|
|
foreach ($matches as $match) {
|
|
if (!$first && $previousMatchWasSeparator && isset($match['separator']) && $match['separator'] === $separator) {
|
|
$previousMatchWasSeparator = true;
|
|
$partMatches[$i][] = $match;
|
|
} elseif (isset($match['separator']) && $match['separator'] === $separator) {
|
|
$previousMatchWasSeparator = true;
|
|
++$i;
|
|
} else {
|
|
$previousMatchWasSeparator = false;
|
|
$partMatches[$i][] = $match;
|
|
}
|
|
}
|
|
|
|
$parts = [];
|
|
if ($partSeparators) {
|
|
foreach ($partMatches as $matches) {
|
|
$parts[] = self::groupParts($matches, $partSeparators, false);
|
|
}
|
|
} else {
|
|
foreach ($partMatches as $matches) {
|
|
$parts[] = self::unquote($matches[0][0]);
|
|
}
|
|
|
|
if (!$first && 2 < \count($parts)) {
|
|
$parts = [
|
|
$parts[0],
|
|
implode($separator, \array_slice($parts, 1)),
|
|
];
|
|
}
|
|
}
|
|
|
|
return $parts;
|
|
}
|
|
}
|