chore: 重写初始提交(清空历史,整理后全量提交)
This commit is contained in:
@@ -0,0 +1,277 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ywxapp\service;
|
||||
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\TransferException;
|
||||
use think\facade\Config;
|
||||
use think\facade\Log;
|
||||
|
||||
/**
|
||||
* 远程市场服务(购买链路远程化)
|
||||
*
|
||||
* 当 ywxapp.api_url 指向其他服务器(见 is_market_client(),本机即客户机)时,客户端插件商店的
|
||||
* 目录/授权/购买/订单/下载均通过本服务代理到中心站 market 插件 API,
|
||||
* 实现「多租户分库」——客户端不再依赖本地 appmarket_addon_list/appmarket_addon_orders/
|
||||
* appmarket_addon_licenses/appmarket_addon_download_logs 等市场表。
|
||||
*
|
||||
* 鉴权:所有写/读敏感请求携带共享密钥 rtoken(与中心站 appmarket_remote_token 一致),
|
||||
* 中心站 Market::authRemote() 校验。中心站响应约定 code=1 为成功。
|
||||
*
|
||||
* @package ywxapp\service
|
||||
*/
|
||||
class RemoteService
|
||||
{
|
||||
const TIMEOUT = 30;
|
||||
|
||||
|
||||
public static function instance(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一解析中心站响应(中心站成功 code=1)
|
||||
* @return array ['success'=>bool,'message'=>string,'data'=>array]
|
||||
*/
|
||||
private function parse($resp): array
|
||||
{
|
||||
$body = (string) $resp->getBody();
|
||||
$json = json_decode($body, true) ?: [];
|
||||
$ok = ((int) ($json['code'] ?? 0)) === 1;
|
||||
return [
|
||||
'success' => $ok,
|
||||
'message' => $json['msg'] ?? ($json['message'] ?? '未知错误'),
|
||||
'data' => $json['data'] ?? [],
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
private function rtoken(): string
|
||||
{
|
||||
return (string) Config::get('appmall.remote_token', '');
|
||||
}
|
||||
|
||||
|
||||
private function client(): Client
|
||||
{
|
||||
return new Client([
|
||||
'base_uri' => rtrim(Config::get('ywxapp.api_url', ''), '/'),
|
||||
'timeout' => self::TIMEOUT,
|
||||
'connect_timeout' => 10,
|
||||
'verify' => (bool) Config::get('appmall.ssl_verify', true),
|
||||
'http_errors' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 插件列表(应用市场)
|
||||
* @param array $params 可选检索参数:keyword/category/order/page/page_size
|
||||
*/
|
||||
public function lists(array $params = []): array
|
||||
{
|
||||
try {
|
||||
$query = [];
|
||||
foreach (['keyword', 'category', 'order', 'page', 'page_size'] as $k) {
|
||||
if (isset($params[$k]) && $params[$k] !== '') {
|
||||
$query[$k] = $params[$k];
|
||||
}
|
||||
}
|
||||
$resp = $this->client()->get('/appmall/api/addon/lists', ['query' => $query]);
|
||||
return $this->parse($resp);
|
||||
} catch (TransferException $e) {
|
||||
Log::error('RemoteMarket lists failed: ' . $e->getMessage());
|
||||
return ['success' => false, 'message' => '连接中心站失败', 'data' => []];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 插件详情(买家视角)
|
||||
*/
|
||||
public function info(int $id, int $uid): array
|
||||
{
|
||||
try {
|
||||
$resp = $this->client()->get('/appmall/api/addon/info', [
|
||||
'query' => ['id' => $id, 'uid' => $uid, 'rtoken' => $this->rtoken()],
|
||||
]);
|
||||
return $this->parse($resp);
|
||||
} catch (TransferException $e) {
|
||||
Log::error('RemoteMarket info failed: ' . $e->getMessage());
|
||||
return ['success' => false, 'message' => '连接中心站失败', 'data' => []];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发起购买
|
||||
*/
|
||||
public function buy(int $addonId, int $uid, string $method): array
|
||||
{
|
||||
try {
|
||||
$resp = $this->client()->post('/appmall/api/addon/buy', [
|
||||
'form_params' => [
|
||||
'addon_id' => $addonId,
|
||||
'uid' => $uid,
|
||||
'method' => $method,
|
||||
'rtoken' => $this->rtoken(),
|
||||
],
|
||||
]);
|
||||
return $this->parse($resp);
|
||||
} catch (TransferException $e) {
|
||||
Log::error('RemoteMarket buy failed: ' . $e->getMessage());
|
||||
return ['success' => false, 'message' => '连接中心站失败', 'data' => []];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 订单状态查询
|
||||
*/
|
||||
public function orderStatus(string $tradeNo, int $uid): array
|
||||
{
|
||||
try {
|
||||
$resp = $this->client()->get('/appmall/api/addon/orderStatus', [
|
||||
'query' => ['trade_no' => $tradeNo, 'uid' => $uid, 'rtoken' => $this->rtoken()],
|
||||
]);
|
||||
return $this->parse($resp);
|
||||
} catch (TransferException $e) {
|
||||
return ['success' => false, 'message' => '连接中心站失败', 'data' => []];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 支付异步回调(转发到中心站,由中心站完成订单)
|
||||
*/
|
||||
public function notify(array $params): string
|
||||
{
|
||||
try {
|
||||
$params['rtoken'] = $this->rtoken();
|
||||
$resp = $this->client()->post('/appmall/api/addon/notify', ['form_params' => $params]);
|
||||
return (string) $resp->getBody();
|
||||
} catch (TransferException $e) {
|
||||
return 'fail';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 支付完成落地页
|
||||
*/
|
||||
public function payResult(array $params): array
|
||||
{
|
||||
try {
|
||||
$params['rtoken'] = $this->rtoken();
|
||||
$resp = $this->client()->get('/appmall/api/addon/payResult', ['query' => $params]);
|
||||
return $this->parse($resp);
|
||||
} catch (TransferException $e) {
|
||||
return ['success' => false, 'message' => '连接中心站失败', 'data' => []];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 退款 / 吊销授权(代理中心站 /appmall/api/addon/refund)
|
||||
*/
|
||||
public function refund(string $tradeNo, int $orderId, int $uid): array
|
||||
{
|
||||
try {
|
||||
$resp = $this->client()->post('/appmall/api/addon/refund', [
|
||||
'form_params' => [
|
||||
'trade_no' => $tradeNo,
|
||||
'order_id' => $orderId,
|
||||
'uid' => $uid,
|
||||
'rtoken' => $this->rtoken(),
|
||||
],
|
||||
]);
|
||||
return $this->parse($resp);
|
||||
} catch (TransferException $e) {
|
||||
return ['success' => false, 'message' => '连接中心站失败', 'data' => []];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 我的已购插件(买家视角,运行在中心站)
|
||||
*/
|
||||
public function my(int $uid): array
|
||||
{
|
||||
try {
|
||||
$resp = $this->client()->get('/appmall/api/addon/my', [
|
||||
'query' => ['uid' => $uid, 'rtoken' => $this->rtoken()],
|
||||
]);
|
||||
return $this->parse($resp);
|
||||
} catch (TransferException $e) {
|
||||
Log::error('RemoteMarket my failed: ' . $e->getMessage());
|
||||
return ['success' => false, 'message' => '连接中心站失败', 'data' => []];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载插件 zip 二进制(中心站 /appmall/api/index,含可选签名)
|
||||
* @param int $uid 已登录用户ID(>0 时中心站做授权校验,纵深防御)
|
||||
* @param string $domain 当前站点域名(站点授权绑定校验)
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function downloadBinary(string $name, string $version, int $uid = 0, string $domain = ''): string
|
||||
{
|
||||
$query = ['name' => $name, 'version' => $version];
|
||||
if ($uid > 0) {
|
||||
$query['uid'] = $uid;
|
||||
}
|
||||
if ($domain !== '') {
|
||||
$query['domain'] = $domain;
|
||||
}
|
||||
if (Config::get('ywxapp.addon_download_sign') && $secret = Config::get('ywxapp.addon_secret')) {
|
||||
$ts = time();
|
||||
$query['ts'] = $ts;
|
||||
$query['sign'] = md5($name . $version . $ts . $secret);
|
||||
}
|
||||
|
||||
// 流式落地到临时文件(不整包入内存),放宽超时与读取超时
|
||||
set_time_limit(0);
|
||||
$tmpFile = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR
|
||||
. 'addon_dl_' . uniqid('', true) . '.zip';
|
||||
try {
|
||||
$resp = $this->client()->get('/appmall/api/index', [
|
||||
'query' => $query,
|
||||
'sink' => $tmpFile,
|
||||
'timeout' => 3600,
|
||||
'read_timeout' => 600,
|
||||
]);
|
||||
|
||||
// 服务端错误(HTTP >= 400)或返回 JSON 错误时,sink 写入的是错误内容而非 zip
|
||||
if ($resp->getStatusCode() >= 400) {
|
||||
$err = (string) @file_get_contents($tmpFile);
|
||||
throw new \Exception('下载失败(HTTP ' . $resp->getStatusCode() . '):' . mb_substr($err, 0, 200));
|
||||
}
|
||||
$fh = fopen($tmpFile, 'rb');
|
||||
$first = $fh ? fread($fh, 1) : '';
|
||||
if ($fh) {
|
||||
fclose($fh);
|
||||
}
|
||||
if ($first === '{') {
|
||||
$json = json_decode((string) @file_get_contents($tmpFile), true);
|
||||
throw new \Exception($json['message'] ?? ($json['msg'] ?? '下载失败'));
|
||||
}
|
||||
|
||||
// 完整性校验:比对 Content-Length 与实际落盘字节数,不一致即判定下载被截断
|
||||
$size = is_file($tmpFile) ? filesize($tmpFile) : 0;
|
||||
$contentLength = (int) $resp->getHeaderLine('Content-Length');
|
||||
if ($size <= 0) {
|
||||
throw new \Exception('下载失败:未获取到文件内容');
|
||||
}
|
||||
if ($contentLength > 0 && $size !== $contentLength) {
|
||||
throw new \Exception('下载插件包不完整(期望 ' . $contentLength . ' 字节,实际 ' . $size . ' 字节)');
|
||||
}
|
||||
return $tmpFile;
|
||||
} catch (\Throwable $e) {
|
||||
// 任一环节失败都清理临时文件,避免磁盘泄漏(成功路径由调用方 copy+unlink)
|
||||
@unlink($tmpFile);
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user