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,7 @@
composer.phar
/vendor/
.idea/
# Commit your application's lock file https://getcomposer.org/doc/01-basic-usage.md#commit-your-composer-lock-file-to-version-control
# You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file
# composer.lock

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 腾讯云
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,109 @@
## 获取 SDK
- composer 安装
```
创建composer.json的文件内容如下
{
"require":{
"qcloud_sts/qcloud-sts-sdk": "3.0.*"
}
}
```
## 查看示例
请查看 [demo 示例](https://github.com/tencentyun/qcloud-cos-sts-sdk/tree/master/php/demo)里面描述了如何调用SDK。
## 接口说明
### getTempKeys
获取临时密钥接口
### 参数说明
|字段|类型|描述|
| ---- | ---- | ---- |
|secretId|string| 云 API 密钥 Id|
|secretKey|string| 云 API 密钥 key|
|durationSeconds|long| 要申请的临时密钥最长有效时间,单位秒,默认 1800最大可设置 7200 |
|bucket|string| 存储桶名称bucketName-appid, 如 test-125000000|
|region|string| 存储桶所属地域,如 ap-guangzhou|
|allowPrefix|string|资源的前缀,如授予操作所有资源,则为`*`如授予操作某个路径a下的所有资源,则为 `a/*`如授予只能操作特定的文件a/test.jpg, 则为`a/test.jpg`|
|allowActions|array| 授予 COS API 权限集合, 如简单上传操作name/cos:PutObject|
|policy|array| 策略:由 allowActions、bucket、region、allowPrefix字段组成的描述授权的具体信息|
### 返回值说明
|字段|类型|描述|
| ---- | ---- | ---- |
|credentials | string | 临时密钥信息 |
|tmpSecretId | string | 临时密钥 Id可用于计算签名 |
|tmpSecretKey | string | 临时密钥 Key可用于计算签名 |
|sessionToken | string | 请求时需要用的 token 字符串,最终请求 COS API 时,需要放在 Header 的 x-cos-security-token 字段 |
|startTime | string | 密钥的起始时间,是 UNIX 时间戳 |
|expiredTime | string | 密钥的失效时间,是 UNIX 时间戳 |
### 返回数据示例
```
{
"expiredTime": 1589258683,
"expiration": "2020-05-12T04:44:43Z",
"credentials": {
"sessionToken": "Biypn6exa48PpMe7wFerEnNMpBKKPQo180c57e0a5275ebae506d7851a85f36a4P0TV5UFR3FYJjsoZA1tk6uRKoDRzc6-60BmwLqdS75OhjHEa7GlVYpL_ofKQJTpPKziKX7FnI10D_6qtLdjzf2NdsyUtQEd5kPpDCOQJZn9-BpleqWQe8oyH_2u7xi2f0FtjCYaoGIZ_lUqlILXQwr0B0t3hLfL4xNE-EmVjUlUXa16HxVCn4_hJetqo9LmI0AgLOjCbYx9aVrsV10eDsRta-TQSIXmJNP3aJ6oz8d8GBTgTE1I2qSFDnv9pjtQKW8HZWI_glPIfmHXCCwAESxEFL_owGz839Va0qYhF6LkfVmsuoU1zNcvJR1w3cIE6izV3SKHaOtWaew3IOervuOPoN3S2oYGNwv2EavtDAWyUBIeI7X6nMVzlpnyJ-3bkIhOq9QVIQAs8wh5A0u9mvKWugT5t6qgyEgvEZSj9k6p-JjwxMgLC6s5uK1i_nnf4fN7ZQ6I-JAfHnH4jEDiVtJgXqfuWPX_vnzskyR2Co6E",
"tmpSecretId": "AKIDTRPc-oe6c_avPSRwFVsPDyy3IoAr3szMajlOGuoEXY1232YLy6j4f-xZ5zL-NBMG",
"tmpSecretKey": "2v29SZztGYk6SGwHYm\/chJXdD3zPRFasmPoJiCmlR\/I="
},
"requestId": "69ef6295-b981-464d-9816-9c2ef92189d1",
"startTime": 1589256883
}
```
### getRoleCredential
申请扮演角色
### 参数说明
|字段|类型|描述|必选|
| ---- | ---- | ---- | ----|
|roleArn|string|角色的资源描述,可在 [访问管理](https://console.cloud.tencent.com/cam/role) 点击角色名获取。| 是 |
|secretId|string| 云 API 密钥 Id| 是 |
|secretKey|string| 云 API 密钥 key| 是 |
|endpoint|string| 接入点,内网填写"internal.tencentcloudapi.com",外网填写"tencentcloudapi.com"| 是 |
|durationSeconds|long| 要申请的临时密钥最长有效时间,单位秒,默认 1800最大可设置 7200 | 否 |
|bucket|string| 存储桶名称bucketName-appid, 如 test-125000000| 是 |
|region|string| 存储桶所属地域,如 ap-guangzhou| 是 |
|allowPrefix|string|资源的前缀,如授予操作所有资源,则为`*`如授予操作某个路径a下的所有资源,则为 `a/*`如授予只能操作特定的文件a/test.jpg, 则为`a/test.jpg`| 是 |
|allowActions|array| 授予 COS API 权限集合, 如简单上传操作name/cos:PutObject| 是 |
|policy|array| 策略:由 allowActions、bucket、region、allowPrefix字段组成的描述授权的具体信息| 否 |
|externalId|string| 角色外部ID| 否 |
### 返回值说明
|字段|类型|描述|
| ---- | ---- | ---- |
|credentials | string | 临时密钥信息 |
|tmpSecretId | string | 临时密钥 Id可用于计算签名 |
|tmpSecretKey | string | 临时密钥 Key可用于计算签名 |
|sessionToken | string | 请求时需要用的 token 字符串,最终请求 COS API 时,需要放在 Header 的 x-cos-security-token 字段 |
|startTime | string | 密钥的起始时间,是 UNIX 时间戳 |
|expiredTime | string | 密钥的失效时间,是 UNIX 时间戳 |
### 返回数据示例
```
{
"Response": {
"Credentials": {
"Token": "da1e9d2ee9dda83506832d5ecb903b790132dfe340001",
"TmpSecretId": "AKID65zyIP0mpXtaI******WIQVMn1umNH58",
"TmpSecretKey": "q95K84wrzuEGoc*******52boxvp71yoh"
},
"ExpiredTime": 1543914376,
"Expiration": "2018-12-04T09:06:16Z",
"RequestId": "4daec797-9cd2-4f09-9e7a-7d4c43b2a74c"
}
}
```

View File

@@ -0,0 +1,28 @@
{
"name": "qcloud_sts/qcloud-sts-sdk",
"description": "PHP SDK for QCloud STS",
"keywords": [
"qcloud", "sts", "cos", "php"
],
"homepage": "https://github.com/tencentyun/qcloud-cos-sts-sdk",
"license": "MIT",
"authors": [
{
"name": "qcloudterminal",
"email": "qcloudterminal@gmail.com"
},
{
"name": "tuunalai",
"email": "550566181@qq.com"
}
],
"autoload": {
"psr-4": {
"QCloud\\COSSTS\\": "src"
}
},
"require": {
"php": ">=5.3.0",
"ext-curl": "*"
}
}

View File

@@ -0,0 +1,59 @@
<?php
namespace QCloud\COSSTS;
class Scope{
var $action;
var $bucket;
var $region;
var $resourcePrefix;
var $effect = 'allow';
function __construct($action, $bucket, $region, $resourcePrefix){
$this->action = $action;
$this->bucket = $bucket;
$this->region = $region;
$this->resourcePrefix = $resourcePrefix;
}
function set_effect($isAllow){
if($isAllow){
$this->effect = 'allow';
}else{
$this->effect = 'deny';
}
}
function get_action(){
if($this->action == null){
throw new \Exception("action == null");
}
return $this->action;
}
function get_resource(){
if($this->bucket == null){
throw new \Exception("bucket == null");
}
if($this->region == null){
throw new \Exception("region == null");
}
if($this->resourcePrefix == null){
throw new \Exception("resourcePrefix == null");
}
$index = strripos($this->bucket, '-');
if($index < 0){
throw new Exception("bucket is invalid: " . $this->bucket);
}
$appid = substr($this->bucket, $index + 1);
if(!(strpos($this->resourcePrefix, '/') === 0)){
$this->resourcePrefix = '/' . $this->resourcePrefix;
}
return 'qcs::cos:' . $this->region . ':uid/' . $appid . ':' . $this->bucket . $this->resourcePrefix;
}
function get_effect(){
return $this->effect;
}
}
?>

View File

@@ -0,0 +1,298 @@
<?php
namespace QCloud\COSSTS;
class Sts{
// 临时密钥计算样例
function _hex2bin($data) {
$len = strlen($data);
return pack("H" . $len, $data);
}
// obj 转 query string
function json2str($obj, $notEncode = false) {
ksort($obj);
$arr = array();
if(!is_array($obj)){
throw new \Exception('$obj must be an array, the actual value is:' . json_encode($obj));
}
foreach ($obj as $key => $val) {
array_push($arr, $key . '=' . ($notEncode ? $val : rawurlencode($val)));
}
return join('&', $arr);
}
// 计算临时密钥用的签名
function getSignature($opt, $key, $method, $config) {
$host = "sts.tencentcloudapi.com";
if (array_key_exists('domain', $config)) {
$host = $config['domain'];
}
if (array_key_exists('endpoint', $config)) {
$host = "sts." . $config['endpoint'];
}
$formatString = $method . $host . '/?' . $this->json2str($opt, 1);
$sign = hash_hmac('sha1', $formatString, $key);
$sign = base64_encode($this->_hex2bin($sign));
return $sign;
}
// v2接口的key首字母小写v3改成大写此处做了向下兼容
function backwardCompat($result) {
if(!is_array($result)){
throw new \Exception('$result must be an array, the actual value is:' . json_encode($result));
}
$compat = array();
foreach ($result as $key => $value) {
if(is_array($value)) {
$compat[lcfirst($key)] = $this->backwardCompat($value);
} elseif ($key == 'Token') {
$compat['sessionToken'] = $value;
} else {
$compat[lcfirst($key)] = $value;
}
}
return $compat;
}
// 获取临时密钥
function getTempKeys($config) {
$result = null;
try{
if(array_key_exists('policy', $config)){
$policy = $config['policy'];
}else{
if(array_key_exists('bucket', $config)){
$ShortBucketName = substr($config['bucket'],0, strripos($config['bucket'], '-'));
$AppId = substr($config['bucket'], 1 + strripos($config['bucket'], '-'));
}else{
throw new \Exception("bucket== null");
}
if(array_key_exists('allowPrefix', $config)){
if(!(strpos($config['allowPrefix'], '/') === 0)){
$config['allowPrefix'] = '/' . $config['allowPrefix'];
}
}else{
throw new \Exception("allowPrefix == null");
}
if(!array_key_exists('region', $config)) {
throw new \Exception("region == null");
}
$policy = array(
'version'=> '2.0',
'statement'=> array(
array(
'action'=> $config['allowActions'],
'effect'=> 'allow',
'resource'=> array(
'qcs::cos:' . $config['region'] . ':uid/' . $AppId . ':' . $config['bucket'] . $config['allowPrefix']
)
)
)
);
}
$policyStr = str_replace('\\/', '/', json_encode($policy));
$Action = 'GetFederationToken';
$Nonce = rand(10000, 20000);
$Timestamp = time();
$Method = 'POST';
if(array_key_exists('durationSeconds', $config)){
if(!(is_integer($config['durationSeconds']))){
throw new \Exception("durationSeconds must be a int type");
}
}
$params = array(
'SecretId'=> $config['secretId'],
'Timestamp'=> $Timestamp,
'Nonce'=> $Nonce,
'Action'=> $Action,
'DurationSeconds'=> $config['durationSeconds'],
'Version'=>'2018-08-13',
'Name'=> 'cos',
'Region'=> $config['region'],
'Policy'=> urlencode($policyStr)
);
$params['Signature'] = $this->getSignature($params, $config['secretKey'], $Method, $config);
$url = 'https://sts.tencentcloudapi.com/';
if(array_key_exists('url', $config)) {
$url = $config['url'];
}
if(!array_key_exists('url', $config) && array_key_exists('domain', $config)) {
$url = 'https://sts.' . $config['domain'];
}
if(array_key_exists('endpoint', $config)) {
$url = 'https://sts.' . $config['endpoint'];
}
$ch = curl_init($url);
if(array_key_exists('proxy', $config)){
$config['proxy'] && curl_setopt($ch, CURLOPT_PROXY, $config['proxy']);
}
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch,CURLOPT_SSL_VERIFYPEER,0);
curl_setopt($ch,CURLOPT_SSL_VERIFYHOST,0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $this->json2str($params));
$result = curl_exec($ch);
if(curl_errno($ch)) $result = curl_error($ch);
curl_close($ch);
$result = json_decode($result, 1);
if (isset($result['Response'])) {
$result = $result['Response'];
if(isset($result['Error'])){
throw new \Exception("get cam failed");
}
$result['startTime'] = $result['ExpiredTime'] - $config['durationSeconds'];
}
$result = $this->backwardCompat($result);
return $result;
}catch(\Exception $e){
if($result == null){
$result = "error: " . $e->getMessage();
}else{
$result = json_encode($result);
}
throw new \Exception($result);
}
}
//申请角色授权
function getRoleCredential($config) {
$result = null;
try{
if(array_key_exists('policy', $config)){
$policy = $config['policy'];
}else{
if(array_key_exists('bucket', $config)){
$ShortBucketName = substr($config['bucket'],0, strripos($config['bucket'], '-'));
$AppId = substr($config['bucket'], 1 + strripos($config['bucket'], '-'));
}else{
throw new \Exception("bucket== null");
}
if(array_key_exists('allowPrefix', $config)){
if(!(strpos($config['allowPrefix'], '/') === 0)){
$config['allowPrefix'] = '/' . $config['allowPrefix'];
}
}else{
throw new \Exception("allowPrefix == null");
}
if(!array_key_exists('region', $config)) {
throw new \Exception("region == null");
}
$policy = array(
'version'=> '2.0',
'statement'=> array(
array(
'action'=> $config['allowActions'],
'effect'=> 'allow',
'resource'=> array(
'qcs::cos:' . $config['region'] . ':uid/' . $AppId . ':' . $config['bucket'] . $config['allowPrefix']
)
)
)
);
}
if (array_key_exists('roleArn', $config)) {
$RoleArn = $config['roleArn'];
} else {
throw new \Exception("roleArn == null");
}
$policyStr = str_replace('\\/', '/', json_encode($policy));
$Action = 'AssumeRole';
$Nonce = rand(10000, 20000);
$Timestamp = time();
$Method = 'POST';
$ExternalId = "";
if (array_key_exists('externalId', $config)) {
$ExternalId = $config['externalId'];
}
if(array_key_exists('durationSeconds', $config)){
if(!(is_integer($config['durationSeconds']))){
throw new \Exception("durationSeconds must be a int type");
}
}
$params = array(
'SecretId'=> $config['secretId'],
'Timestamp'=> $Timestamp,
'RoleArn'=> $RoleArn,
'Action'=> $Action,
'Nonce'=> $Nonce,
'DurationSeconds'=> $config['durationSeconds'],
'Version'=>'2018-08-13',
'RoleSessionName'=> 'cos',
'Region'=> $config['region'],
'ExternalId' => $ExternalId,
'Policy'=> urlencode($policyStr)
);
$params['Signature'] = $this->getSignature($params, $config['secretKey'], $Method, $config);
$url = 'https://sts.internal.tencentcloudapi.com/';
if(array_key_exists('endpoint', $config)) {
$url = 'https://sts.' . $config['endpoint'];
}
$ch = curl_init($url);
if(array_key_exists('proxy', $config)){
$config['proxy'] && curl_setopt($ch, CURLOPT_PROXY, $config['proxy']);
}
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch,CURLOPT_SSL_VERIFYPEER,0);
curl_setopt($ch,CURLOPT_SSL_VERIFYHOST,0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $this->json2str($params));
$result = curl_exec($ch);
if(curl_errno($ch)) $result = curl_error($ch);
curl_close($ch);
$result = json_decode($result, 1);
if (isset($result['Response'])) {
$result = $result['Response'];
if(isset($result['Error'])){
throw new \Exception("get cam failed");
}
$result['startTime'] = $result['ExpiredTime'] - $config['durationSeconds'];
}
$result = $this->backwardCompat($result);
return $result;
}catch(\Exception $e){
if($result == null){
$result = "error: " . $e->getMessage();
}else{
$result = json_encode($result);
}
throw new \Exception($result);
}
}
// get policy
function getPolicy($scopes){
if (!is_array($scopes)){
return null;
}
$statements = array();
for($i=0, $counts=count($scopes); $i < $counts; $i++){
$actions=array();
$resources = array();
array_push($actions, $scopes[$i]->get_action());
array_push($resources, $scopes[$i]->get_resource());
$statement = array(
'action' => $actions,
'effect' => $scopes[$i]->get_effect(),
'resource' => $resources
);
array_push($statements, $statement);
}
$policy = array(
'version' => '2.0',
'statement' => $statements
);
return $policy;
}
}
?>