chore: 重写初始提交(清空历史,整理后全量提交)

This commit is contained in:
ywxapp
2026-08-16 16:54:14 +08:00
commit 6c1a106bc1
1808 changed files with 238144 additions and 0 deletions
+277
View File
@@ -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;
}
}
}