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,30 @@
<?php
namespace Joypack\Tencent\Map;
/**
* 腾讯位置服务
* 基础类
*/
class Bundle
{
// 参数实例
protected $option;
// 请求实例
protected $request;
// 日志实例
public $logger;
public function __construct(Option $option, $log_root=null, $development=false)
{
$log_root = rtrim($log_root, '/\\');
// 参数实例
$this->option = $option;
// 实例化日志
$this->logger = new Logger("{$log_root}/joypack-tencent-map", $development);
// 实例化请求
$this->request = new Request($this->logger);
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace Joypack\Tencent\Map\Bundle;
use Joypack\Tencent\Map\Response;
use Joypack\Tencent\Map\Bundle;
/**
* 地址解析(地址转坐标)
* 本接口提供由地址描述到所述位置坐标的转换
* 与逆地址解析的过程正好相反。
*/
class Address extends Bundle
{
/**
* 地址解析(地址转坐标)
* @param boolean $using_sig 使用签名方式校验
* @return Response
*/
public function request($using_sig=false)
{
$uri = '/ws/geocoder/v1';
if($using_sig) {
$this->option->setSig($uri);
}
$data = $this->option->getAll();
//$this->request->logger->print($data, true);
$this->request->uri($uri);
$this->request->query($data);
return $this->request->get();
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Joypack\Tencent\Map\Bundle;
use Joypack\Tencent\Map\Option;
/**
* 地址解析(地址转坐标)
* 参数
*/
class AddressOption extends Option
{
/**
* 地址
* @param string $value
*/
public function setAddress($value)
{
$this->option['address'] = $value;
}
/**
* 指定地址所属城市
* @param string $value
*/
public function setRegion($value)
{
$this->option['region'] = $value;
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace Joypack\Tencent\Map\Bundle;
use Joypack\Tencent\Map\Response;
use Joypack\Tencent\Map\Bundle;
/**
* IP定位
* 通过终端设备IP地址获取其当前所在地理位置
* 精确到市级,常用于显示当地城市天气预报、初始化用户城市等非精确定位场景。
*/
class Ip extends Bundle
{
/**
* IP定位
* @param boolean $using_sig 使用签名方式校验
* @return Response
*/
public function request($using_sig=false)
{
$uri = '/ws/location/v1/ip';
if($using_sig) {
$this->option->setSig($uri);
}
$data = $this->option->getAll();
//$this->request->logger->print($data, true);
$this->request->uri($uri);
$this->request->query($data);
return $this->request->get();
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace Joypack\Tencent\Map\Bundle;
use Joypack\Tencent\Map\Option;
/**
* IP定位
* 参数
*/
class IpOption extends Option
{
/**
* IP地址
* @param string $value
*/
public function setIp($value)
{
$this->option['ip'] = $value;
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace Joypack\Tencent\Map\Bundle;
use Joypack\Tencent\Map\Response;
use Joypack\Tencent\Map\Bundle;
/**
* 逆地址解析(坐标位置描述)
* 本接口提供由坐标到坐标所在位置的文字描述的转换
* 输入坐标返回地理位置信息和附近poi列表。
*/
class Location extends Bundle
{
/**
* 逆地址解析(坐标位置描述)
* @param boolean $using_sig 使用签名方式校验
* @return Response
*/
public function request($using_sig=false)
{
$uri = '/ws/geocoder/v1';
if($using_sig) {
$this->option->setSig($uri);
}
$data = $this->option->getAll();
//$this->request->logger->print($data, true);
$this->request->uri($uri);
$this->request->query($data);
return $this->request->get();
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Joypack\Tencent\Map\Bundle;
use Joypack\Tencent\Map\Option;
/**
* 逆地址解析(坐标位置描述)
* 参数
*/
class LocationOption extends Option
{
public function setLocation($lat, $lng)
{
$this->option['location'] = "{$lat},{$lng}";
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace Joypack\Tencent\Map\Bundle;
use Joypack\Tencent\Map\Response;
use Joypack\Tencent\Map\Bundle;
/**
* 坐标转换
* 实现从其它地图供应商坐标系或标准GPS坐标系
* 批量转换到腾讯地图坐标系
*/
class Translate extends Bundle
{
/**
* 逆地址解析(坐标位置描述)
* @param boolean $using_sig 使用签名方式校验
* @return Response
*/
public function request($using_sig=false)
{
$uri = '/ws/coord/v1/translate';
if($using_sig) {
$this->option->setSig($uri);
}
$data = $this->option->getAll();
//$this->request->logger->print($data, true);
$this->request->uri($uri);
$this->request->query($data);
return $this->request->get();
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace Joypack\Tencent\Map\Bundle;
use Joypack\Tencent\Map\Option;
/**
* 坐标转换
* 参数
*/
class TranslateOption extends Option
{
const TYPE_GPS = 1;
const TYPE_SOGOU = 2;
const TYPE_BAIDU = 3;
const TYPE_MAPBAR = 4;
const TYPE_DEFAULT = 5;
const TYPE_SOGOU_MERCATOR = 6;
/**
* 预转换的坐标
* @param string $value
*/
public function setLocation($lat, $lng)
{
$this->option['locations'] = "{$lat},{$lng}";
}
/**
* 预转换的坐标,支持批量转换
* @param array $locations
* <p>['lat,lng', [lat,lng]]</p>
*/
public function setLocations(array $locations)
{
$pieces = [];
foreach ($locations as $item) {
if(is_array($item)) {
$pieces[] = "{$item[0]},{$item[1]}";
}
}
$this->option['locations'] = implode(';', $pieces);
}
/**
* 设置坐标类型
* @param number $value
* 1 GPS坐标
* 2 sogou经纬度
* 3 baidu经纬度
* 4 mapbar经纬度
* 5 腾讯、google、高德坐标[默认]
* 6 sogou墨卡托
*/
public function setType($value=self::TYPE_DEFAULT)
{
$this->option['type'] = $value;
}
}

View File

@@ -0,0 +1,121 @@
<?php
namespace Joypack\Tencent\Map;
/**
* 腾讯位置服务
* 日志管理
*/
class Logger
{
protected $rootPath;
protected $development = false;
public function __construct($root_path, $development)
{
$this->development = $development;
if($root_path) {
if(is_dir($root_path)) {
$this->rootPath = $root_path;
} else {
if(@mkdir($root_path, 0775, true)) {
$this->rootPath = $root_path;
}
}
}
}
public function __toString()
{
return __CLASS__;
}
/**
* 写入 debug 日志
* @param string $message
* @param string | array $data
*/
public function debug($message, $data=null)
{
$this->save($message, $data, 'DEBUG');
}
/**
* 写入 INFO 日志
* @param string $message
* @param string | array $data
*/
public function info($message, $data=null)
{
$this->save($message, $data, 'INFO');
}
/**
* 写入 ERROR 日志
* @param string $message
* @param string | array $data
*/
public function error($message, $data=null)
{
$this->save($message, $data, 'ERROR');
}
/**
* 打印变量
* @param mixed $args 打印列表
* 最后一个元素如果是 true 则 exit
*/
public function print(...$args)
{
$args = func_get_args();
$length = count($args);
$exit = false;
if(is_bool($last_argument = $args[$length-1])) {
if($last_argument) {
$exit = true;
array_pop($args);
}
}
echo '<pre>';
while ($argument = array_shift($args)) {
print_r($argument);
echo '<br/><br/>';
}
echo '</pre>';
if($exit) {
exit;
}
}
protected function save($message, $data, $level)
{
if(is_null($this->rootPath)) {
return;
}
// 生产环境时只记录错误信息
if(!$this->development) {
if($level != 'ERROR') {
return;
}
}
$date = date('Y-m-d');
$now = date('Y-m-d H:i:s');
$filename = "{$this->rootPath}/{$date}.log";
if(is_array($data)) {
$data = json_encode($data, JSON_UNESCAPED_UNICODE);
}
@file_put_contents($filename, "[{$level}] {$now} {$message} {$data}\r\n", FILE_APPEND);
}
}

View File

@@ -0,0 +1,98 @@
<?php
namespace Joypack\Tencent\Map;
/**
* 腾讯位置服务
* 公共参数
*/
class Option
{
const OUTPUT_JSON = 'json';
const OUTPUT_JSONP = 'jsonp';
protected $option = [];
protected $secret;
public function __construct($key=null, $secret=null)
{
$this->setKey($key);
$this->setSecret($secret);
}
public function setSecret($value)
{
$this->secret = $value;
}
/**
* 开发密钥
* @param string $value
*/
public function setKey($value)
{
$this->option['key'] = $value;
}
/**
* 返回格式支持JSON/JSONP默认JSON
* @param string $value
*/
public function setOutput($value=self::OUTPUT_JSON)
{
$this->option['output'] = $value;
}
/**
* JSONP方式回调函数
* @param string $value
*/
public function setCallback($value)
{
$this->option['callback'] = $value;
}
/**
* 签名
* @param string $uri
*/
public function setSig($uri)
{
$this->option['sig'] = $this->buildSig($uri, $this->getAll());
}
/**
* 获得所有参数
* @return array
*/
public function getAll()
{
return $this->option;
}
/**
* 生成签名
* @param string $uri
* @return string
*/
protected function buildSig($uri, $option)
{
ksort($option);
$pieces = [];
foreach ($option as $key => $val)
{
$pieces[] = "{$key}={$val}";
}
$str = sprintf('%s?%s', rtrim($uri, '/'), implode('&', $pieces));
/*
echo '<pre>';
print_r("{$str}{$this->secret}");
die;
//*/
return md5("{$str}{$this->secret}");
}
}

View File

@@ -0,0 +1,181 @@
<?php
namespace Joypack\Tencent\Map;
/**
* 腾讯位置服务
* 接口请求类
*/
class Request
{
// 接口地址
protected $url = 'https://apis.map.qq.com';
protected $query = [];
protected $field = [];
public $logger;
public function __construct(Logger $logger)
{
$this->logger = $logger;
}
public function uri($uri)
{
$this->url .= '/' . trim($uri, '/');
return $this;
}
public function query($name, $value=null)
{
if(is_array($name)) {
$this->query = array_merge($this->query, $name);
} else {
$this->query[$name] = $value;
}
return $this;
}
public function field($name, $value=null)
{
if(is_array($name)) {
$this->field = array_merge($this->field, $name);
} else {
$this->field[$name] = $value;
}
return $this;
}
/**
* method get
* @param array $query
* @return Response
*/
public function get(array $query=[])
{
if($query) {
$this->query($query);
}
$url = $this->mergeQuery($this->url, $this->query);
return $this->create($url);
}
public function post(array $fields=[])
{
if($fields) {
$this->field($fields);
}
$url = $this->mergeQuery($this->url, $this->query);
return $this->create($url, 'POST', $this->field);
}
/**
* <p>将参数合并到 URL</p>
* @param string $url 请求的地址
* @param array $query 请求参数
* @param bool $recursive 是否递归合并
* @return mixed
*/
protected function mergeQuery($url, array $query, bool $recursive=false)
{
if(empty($url)) {
return null;
}
// 没有设置参数时直接返回地址
if(empty($query)) {
return $url;
}
$parsed = parse_url($url);
// 合并参数
if(isset($parsed['query'])) {
$url = substr($url, 0, strpos($url, '?'));
$str_parsed = [];
parse_str($parsed['query'], $str_parsed);
if($recursive) {
$query = array_merge_recursive($str_parsed, $query);
} else {
$query = array_merge($str_parsed, $query);
}
} else {
$url = rtrim($url, '/?');
}
// 生成 query 字符串
$url = "{$url}?" . http_build_query($query);
return $url;
// 处理锚点
if(isset($parsed['fragment'])) {
$url .= "#{$parsed['fragment']}";
}
return $url;
}
/**
* <p>创建请求</p>
* @param string $url 请求地址
* @param string $method 请求方式
* @param array $data POST 数据
* @return Response
*/
protected function create($url, $data=null)
{
//$referer = "{$_SERVER['REQUEST_SCHEME']}://{$_SERVER['SERVER_NAME']}";
//$header = [
//"CLIENT-IP: {$_SERVER['REMOTE_ADDR']}",
//"X-FORWARDED-FOR: {$_SERVER['REMOTE_ADDR']}",
//"Content-Type: application/json; charset=utf-8",
//"Accept: */*"
//];
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
//curl_setopt($ch, CURLOPT_HEADER, $header);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
//curl_setopt($ch, CURLOPT_REFERER, $referer);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
if(!is_null($data)) {
curl_setopt($ch, CURLOPT_POST, true);
if($data) {
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
}
}
$original = curl_exec($ch);
$error = null;
if($errno = curl_errno($ch)) {
$error = curl_error($ch);
}
curl_close($ch);
$this->logger->info('请求地址', $url);
if($data) {
$this->logger->info('请求数据', $data);
}
$this->logger->info('响应数据', $original);
return new Response($errno, $error, $original, $this->logger);
}
}

View File

@@ -0,0 +1,159 @@
<?php
namespace Joypack\Tencent\Map;
/**
* 腾讯位置服务
* 接口响应类
*/
class Response
{
public $error;
public $logger;
protected $original;
protected $decode;
public function __construct($errno, $error, $original, Logger $logger)
{
$this->logger = $logger;
// 仅成功时
if(0 === $errno) {
$decode = json_decode($original, true);
if(is_null($decode)) {
// 错误
$this->setErrorMessage(99);
// 写入日志
$logger->error('解析失败');
} else {
$this->original = $original;
$this->decode = $decode;
}
} else {
// 错误
$this->setErrorMessage($errno);
// 写入日志
$logger->error($error);
}
}
/**
* 返回json
* @return string
*/
public function getOriginal()
{
return $this->original;
}
/**
* 返回数组
* @return array
*/
public function toArray()
{
return $this->decode;
}
/**
* 获得某属性时
* @param string $prop_name
* @return mixed
*/
public function __get($property)
{
return $this->decode[ $property ] ?? null;
}
public function __toString()
{
return $this->original;
}
protected function setErrorMessage($errno)
{
$errors = [
1=> 'UNSUPPORTED_PROTOCOL',
2=> 'FAILED_INIT',
3=> 'URL_MALFORMAT',
4=> 'URL_MALFORMAT_USER',
5=> 'COULDNT_RESOLVE_PROXY',
6=> 'COULDNT_RESOLVE_HOST',
7=> 'COULDNT_CONNECT',
8=> 'FTP_WEIRD_SERVER_REPLY',
9=> 'REMOTE_ACCESS_DENIED',
11=> 'FTP_WEIRD_PASS_REPLY',
13=> 'FTP_WEIRD_PASV_REPLY',
14=>'FTP_WEIRD_227_FORMAT',
15=> 'FTP_CANT_GET_HOST',
17=> 'FTP_COULDNT_SET_TYPE',
18=> 'PARTIAL_FILE',
19=> 'FTP_COULDNT_RETR_FILE',
21=> 'QUOTE_ERROR',
22=> 'HTTP_RETURNED_ERROR',
23=> 'WRITE_ERROR',
25=> 'UPLOAD_FAILED',
26=> 'READ_ERROR',
27=> 'OUT_OF_MEMORY',
28=> 'OPERATION_TIMEDOUT',
30=> 'FTP_PORT_FAILED',
31=> 'FTP_COULDNT_USE_REST',
33=> 'RANGE_ERROR',
34=> 'HTTP_POST_ERROR',
35=> 'SSL_CONNECT_ERROR',
36=> 'BAD_DOWNLOAD_RESUME',
37=> 'FILE_COULDNT_READ_FILE',
38=> 'LDAP_CANNOT_BIND',
39=> 'LDAP_SEARCH_FAILED',
41=> 'FUNCTION_NOT_FOUND',
42=> 'ABORTED_BY_CALLBACK',
43=> 'BAD_FUNCTION_ARGUMENT',
45=> 'INTERFACE_FAILED',
47=> 'TOO_MANY_REDIRECTS',
48=> 'UNKNOWN_TELNET_OPTION',
49=> 'TELNET_OPTION_SYNTAX',
51=> 'PEER_FAILED_VERIFICATION',
52=> 'GOT_NOTHING',
53=> 'SSL_ENGINE_NOTFOUND',
54=> 'SSL_ENGINE_SETFAILED',
55=> 'SEND_ERROR',
56=> 'RECV_ERROR',
58=> 'SSL_CERTPROBLEM',
59=> 'SSL_CIPHER',
60=> 'SSL_CACERT',
61=> 'BAD_CONTENT_ENCODING',
62=> 'LDAP_INVALID_URL',
63=> 'FILESIZE_EXCEEDED',
64=> 'USE_SSL_FAILED',
65=> 'SEND_FAIL_REWIND',
66=> 'SSL_ENGINE_INITFAILED',
67=> 'LOGIN_DENIED',
68=> 'TFTP_NOTFOUND',
69=> 'TFTP_PERM',
70=> 'REMOTE_DISK_FULL',
71=> 'TFTP_ILLEGAL',
72=> 'TFTP_UNKNOWNID',
73=> 'REMOTE_FILE_EXISTS',
74=> 'TFTP_NOSUCHUSER',
75=> 'CONV_FAILED',
76=> 'CONV_REQD',
77=> 'SSL_CACERT_BADFILE',
78=> 'REMOTE_FILE_NOT_FOUND',
79=> 'SSH',
80=> 'SSL_SHUTDOWN_FAILED',
81=> 'AGAIN',
82=> 'SSL_CRL_BADFILE',
83=> 'SSL_ISSUER_ERROR',
84=> 'FTP_PRET_FAILED',
84=> 'FTP_PRET_FAILED',
85=> 'RTSP_CSEQ_ERROR',
86=> 'RTSP_SESSION_ERROR',
87=> 'FTP_BAD_FILE_LIST',
88=> 'CHUNK_FAILED',
99=> 'DECODE_ERROR',
];
$this->error = $errors[ $errno ] ?? 'UNKNOWN_ERROR';
}
}