Files

431 lines
16 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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 app\Api\Controller\V1;
use ywxapp\controller\ApiController;
use ywxapp\service\RemoteService;
use think\Request;
/**
* 插件商店接口(瘦分发器,不持有市场领域逻辑):
*
* - 客户机模式(is_market_client()=trueywxapp.api_url 指向中心站):
* 经 RemoteService 代理中心站 /appmall/api/addon/*。
* - 中心站模式(api_url 为空/指向本机):
* 进程内委托 appmall 插件的 MarketService(订单/授权/收益/支付回调全部在插件侧)。
* - 未安装 appmall 插件且非客户机:市场功能不可用,返回明确提示。
*
* 中心站领域数据(appmarket_* 表)已全部收敛到 addon/appmall,核心不再直接读写。
*/
class Addon extends ApiController
{
// notify/payResult 由支付平台回调/跳转,无需登录;info 公开
protected $noNeedLogin = ['info', 'notify', 'payResult'];
protected $needRight = ['*'];
/**
* 中心站模式下委托的插件市场服务
* @var \addon\appmall\service\MarketService|null
*/
protected $market = null;
public function initialize()
{
if (!is_market_client() && class_exists(\addon\appmall\service\MarketService::class)) {
$this->market = \addon\appmall\service\MarketService::instance();
$this->market->ensureTables();
}
}
/**
* 本机为中心站但未安装市场插件时的统一出口
*/
protected function marketUnavailable(): \think\response\Json
{
return $this->result->error('市场服务不可用:本机未配置中心站地址(ywxapp.api_url),也未安装 appmall 插件', 503);
}
/**
* 将插件领域异常转换为 Result 错误响应
*/
protected function marketError(\Throwable $e): \think\response\Json
{
$code = (int) $e->getCode();
$code = $code >= 400 && $code < 600 ? $code : 500;
if ($code === 404) {
return $this->result->setStatusCode(404)->error($e->getMessage(), 404);
}
return $this->result->error($e->getMessage(), $code);
}
/**
* 获取插件信息(支持列表和详情)
*/
public function info(Request $request): \think\response\Json
{
if (is_market_client()) {
return $this->remoteInfo($request);
}
if (!$this->market) {
return $this->marketUnavailable();
}
$id = (int) $request->get('id', 0);
if ($id > 0) {
try {
$data = $this->market->info($id, $this->getLoginUid($request) ?? 0);
} catch (\Throwable $e) {
return $this->marketError($e);
}
$data['installed'] = is_dir(ADDON_PATH . ($data['addon']['name'] ?? '') . DIRECTORY_SEPARATOR);
return $this->result->success($data, '获取成功');
}
$r = $this->market->lists(
max((int) $request->get('page', 1), 1),
(int) $request->get('per_page', 15)
);
$this->result->setCount($r['total']);
return $this->result->success(['data' => $r['items']], '获取成功');
}
/**
* 轻量解析请求中的 JWT,返回用户ID(不触发完整登录副作用)
*/
protected function getLoginUid(Request $request): ?int
{
$auth = $request->header('authorization');
if (!$auth || !str_starts_with($auth, 'Bearer ')) {
return null;
}
try {
$token = \ywxapp\service\JwtService::instance()->parseAndValidate(substr($auth, 7));
$uid = $token->claims()->get('uid');
return $uid ? (int) $uid : null;
} catch (\Throwable) {
return null;
}
}
/**
* 发起购买:创建/复用待支付订单并拉起支付(或模拟支付直接签发授权)
*/
public function buy(Request $request)
{
if (is_market_client()) {
return $this->remoteBuy($request);
}
if (!$this->market) {
return $this->marketUnavailable();
}
$userId = $this->auth->model->uid ?? null;
if (empty($userId)) {
return $this->result->setStatusCode(401)->error('请先登录后再购买');
}
try {
$r = $this->market->buy(
(int) $userId,
(int) $request->post('addon_id'),
(string) $request->post('method', 'alipay'),
(string) $request->root(true)
);
} catch (\Throwable $e) {
return $this->marketError($e);
}
return $this->result->success($r['data'], $r['message']);
}
/**
* 支付结果查询(前端轮询用)
*/
public function orderStatus(Request $request): \think\response\Json
{
if (is_market_client()) {
return $this->remoteOrderStatus($request);
}
if (!$this->market) {
return $this->marketUnavailable();
}
$userId = $this->auth->model->uid ?? null;
if (empty($userId)) {
return $this->result->setStatusCode(401)->error('请先登录');
}
try {
$data = $this->market->orderStatus((int) $userId, (string) $request->get('trade_no'));
} catch (\Throwable $e) {
return $this->marketError($e);
}
return $this->result->success($data, '获取成功');
}
/**
* 支付完成落地页(支付宝 return_url 跳转,无需登录,仅展示状态)
*/
public function payResult(Request $request): \think\response\Json
{
if (is_market_client()) {
$r = RemoteService::instance()->payResult($request->get());
return $this->result->success($r['data'] ?? ['status' => 0], $r['message'] ?: '获取成功');
}
if (!$this->market) {
return $this->marketUnavailable();
}
$r = $this->market->payResult((string) $request->get('out_trade_no', ''));
return $this->result->success($r['data'], $r['message']);
}
/**
* 退款 / 吊销授权(买家或运营)
* POST /api/v1/addon/refund {trade_no 或 order_id}
*/
public function refund(Request $request): \think\response\Json
{
if (is_market_client()) {
return $this->remoteRefund($request);
}
if (!$this->market) {
return $this->marketUnavailable();
}
$userId = $this->auth->model->uid ?? null;
if (empty($userId)) {
return $this->result->setStatusCode(401)->error('请先登录');
}
try {
$r = $this->market->refund(
(int) $userId,
(string) $request->post('trade_no', ''),
(int) $request->post('order_id', 0)
);
} catch (\Throwable $e) {
return $this->marketError($e);
}
return $this->result->success($r['data'], $r['message']);
}
/**
* 我的已购插件(买家视角):列出已购付费插件 + 授权状态 + 是否可退款
*/
public function mine(Request $request): \think\response\Json
{
if (is_market_client()) {
return $this->remoteMine($request);
}
if (!$this->market) {
return $this->marketUnavailable();
}
$userId = $this->auth->model->uid ?? null;
if (empty($userId)) {
return $this->result->setStatusCode(401)->error('请先登录');
}
return $this->result->success($this->market->mine((int) $userId), '获取成功');
}
/**
* 支付宝/微信异步回调:验签 -> 校验金额 -> 订单置已付 -> 生成授权
*/
public function notify()
{
if (is_market_client()) {
return response(RemoteService::instance()->notify(input()));
}
if (!$this->market) {
return response('fail');
}
return $this->market->notify((string) input('method', 'alipay'));
}
/**
* 用户下载已购插件(需登录 + 有效授权 + 次数限制)
*/
public function download(Request $request)
{
if (is_market_client()) {
return $this->remoteDownload($request);
}
if (!$this->market) {
return $this->marketUnavailable();
}
$userId = $this->auth->model->uid ?? null;
if (empty($userId)) {
return $this->result->setStatusCode(401)->error('请先登录');
}
try {
$r = $this->market->download((int) $userId, (int) $request->get('addon_id'), $request);
} catch (\Throwable $e) {
return $this->marketError($e);
}
return download($r['file'], $r['filename']);
}
// ============ 客户机模式(ywxapp.api_url 指向其他服务器,见 is_market_client())下的代理实现 ============
/**
* 远程:插件详情 / 列表(代理中心站)
*/
protected function remoteInfo(Request $request): \think\response\Json
{
$id = (int) $request->get('id', 0);
$uid = $this->getLoginUid($request) ?? 0;
if ($id > 0) {
$r = RemoteService::instance()->info($id, $uid);
if (!$r['success']) {
return $this->result->setStatusCode(404)->error($r['message'] ?: '获取失败', 404);
}
$addon = $r['data']['addon'] ?? [];
$installed = is_dir(ADDON_PATH . ($addon['name'] ?? '') . DIRECTORY_SEPARATOR);
return $this->result->success([
'addon' => $addon,
'purchased' => $r['data']['purchased'] ?? false,
'installed' => $installed,
], '获取成功');
}
// 透传分页/筛选参数给中心站,由服务端完成分页(避免全量拉回后本地切片)
$params = [
'keyword' => $request->get('keyword', ''),
'category' => $request->get('category', ''),
'order' => $request->get('order', ''),
'page' => max((int) $request->get('page', 1), 1),
'page_size' => min(max((int) $request->get('per_page', 15), 1), 50),
];
$r = RemoteService::instance()->lists($params);
if (!$r['success']) {
return $this->result->error($r['message'] ?: '获取失败');
}
// 优先消费中心站服务端分页(Market::lists 在 page>0 时返回 data.list/data.total);
// 兼容历史 data.count 键;老中心站未分页时降级为本地切片。
if (isset($r['data']['list']) && (isset($r['data']['total']) || isset($r['data']['count']))) {
$items = $r['data']['list'];
$total = (int) ($r['data']['total'] ?? $r['data']['count']);
} else {
$rows = $r['data']['list'] ?? [];
$perPage = min(max((int) $request->get('per_page', 15), 1), 50);
$page = max((int) $request->get('page', 1), 1);
$total = count($rows);
$items = array_slice($rows, ($page - 1) * $perPage, $perPage);
}
$this->result->setCount($total);
return $this->result->success(['data' => $items], '获取成功');
}
/**
* 远程:发起购买(代理中心站)
*/
protected function remoteBuy(Request $request): \think\response\Json
{
$userId = $this->auth->model->uid ?? null;
if (empty($userId)) {
return $this->result->setStatusCode(401)->error('请先登录后再购买');
}
$addonId = (int) $request->post('addon_id');
$method = $request->post('method', 'alipay');
$r = RemoteService::instance()->buy($addonId, $userId, $method);
if (!$r['success']) {
return $this->result->error($r['message'] ?: '购买失败');
}
return $this->result->success($r['data'] ?? [], $r['message'] ?: '操作成功');
}
/**
* 远程:订单状态(代理中心站)
*/
protected function remoteOrderStatus(Request $request): \think\response\Json
{
$tradeNo = $request->get('trade_no');
$userId = $this->auth->model->uid ?? null;
if (empty($userId)) {
return $this->result->setStatusCode(401)->error('请先登录');
}
$r = RemoteService::instance()->orderStatus($tradeNo, $userId);
if (!$r['success']) {
return $this->result->error($r['message'] ?: '查询失败');
}
return $this->result->success($r['data'] ?? [], '获取成功');
}
/**
* 远程:下载已购插件(代理中心站 /appmall/api/index,付费插件需已购买)
*/
protected function remoteDownload(Request $request)
{
$userId = $this->auth->model->uid ?? null;
if (empty($userId)) {
return $this->result->setStatusCode(401)->error('请先登录');
}
$addonId = (int) $request->get('addon_id');
$r = RemoteService::instance()->info($addonId, $userId);
if (!$r['success']) {
return $this->result->error($r['message'] ?: '插件信息获取失败');
}
$addon = $r['data']['addon'] ?? [];
$name = $addon['name'] ?? '';
$version = $addon['version'] ?? '';
if ($name === '' || $version === '') {
return $this->result->error('插件信息不完整', 404);
}
if (!empty($addon['price']) && (float) $addon['price'] > 0 && empty($r['data']['purchased'])) {
return $this->result->error('您尚未购买该插件', 403);
}
try {
$domain = (string) $request->domain();
$srcFile = RemoteService::instance()->downloadBinary($name, $version, $userId, $domain);
} catch (\Exception $e) {
return $this->result->error($e->getMessage(), 404);
}
$tmpDir = root_path() . 'runtime' . DIRECTORY_SEPARATOR . 'addon_download' . DIRECTORY_SEPARATOR;
if (!is_dir($tmpDir)) {
@mkdir($tmpDir, 0755, true);
}
$tmpFile = $tmpDir . $name . '-' . $version . '.zip';
if (is_file($srcFile)) {
copy($srcFile, $tmpFile);
@unlink($srcFile);
} else {
return $this->result->error('下载失败:未获取到文件', 404);
}
return new \ywxapp\library\StreamZipResponse($tmpFile, $name . '-' . $version . '.zip');
}
/**
* 远程:退款 / 吊销授权(代理中心站)
*/
protected function remoteRefund(Request $request): \think\response\Json
{
$userId = $this->auth->model->uid ?? null;
if (empty($userId)) {
return $this->result->setStatusCode(401)->error('请先登录');
}
$r = RemoteService::instance()->refund(
(string) $request->post('trade_no', ''),
(int) $request->post('order_id', 0),
$userId
);
if (!$r['success']) {
return $this->result->error($r['message'] ?: '退款失败');
}
return $this->result->success($r['data'] ?? [], $r['message'] ?: '退款成功');
}
/**
* 远程:我的已购插件(代理中心站)
*/
protected function remoteMine(Request $request): \think\response\Json
{
$userId = $this->auth->model->uid ?? null;
if (empty($userId)) {
return $this->result->setStatusCode(401)->error('请先登录');
}
$r = RemoteService::instance()->my($userId);
if (!$r['success']) {
return $this->result->error($r['message'] ?: '获取失败');
}
return $this->result->success($r['data'] ?? ['list' => []], $r['message'] ?: '获取成功');
}
}