chore: 重写初始提交(清空历史,整理后全量提交)
This commit is contained in:
@@ -0,0 +1,388 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace addon\appmall\controller;
|
||||
|
||||
use think\facade\Db;
|
||||
use ywxapp\controller\FrontendBase;
|
||||
use ywxapp\library\Mailer;
|
||||
use ywxapp\model\BaseModel;
|
||||
use addon\appmall\library\AppmallSchema;
|
||||
|
||||
/**
|
||||
* 开发者中心(独立前台应用,非会员中心、非后台)
|
||||
* 开发者自助注册 / 查询令牌 / 收益查询 / 提现;完全公开(noNeedLogin=*),
|
||||
* 以开发者令牌(token)而非会员登录态认证。
|
||||
* 路由前缀:/appmall/developer/*(由 AppService::loadAddonRoutes 在 boot 阶段包 Route::group('appmall') 注册到全局路由表,不隶属于任何 thinkphp 应用)。
|
||||
* 视图:addon/appmall/view/frontend/developer/。
|
||||
*
|
||||
* 注:AddonFrontend 基类 _initialize 把 view_path 设成【主应用】view/frontend/(与插件目录不符),
|
||||
* 故此处必须在 initialize() 显式把 view_path 锁回插件目录,否则 fetch 找不到模板。
|
||||
*/
|
||||
class Developer extends FrontendBase
|
||||
{
|
||||
protected $noNeedLogin = ['*'];
|
||||
protected $noNeedVerify = ['*'];
|
||||
|
||||
/**
|
||||
* 锁定视图路径到插件目录(修复 AddonFrontend 设成主应用目录的坑)
|
||||
*/
|
||||
protected function initialize()
|
||||
{
|
||||
// 确保 appmall 运营表存在(开发者中心只读开发者/收益/提现等表,避免全新安装 1146)
|
||||
AppmallSchema::ensure();
|
||||
|
||||
// 仅中心站暴露:客户机(ywxapp.api_url 指向其他服务器,见 is_market_client())作为中心站客户端,
|
||||
// 不持有 appmall_developers / appmall_addon_revenues / appmall_addon_withdrawals 等运营表,
|
||||
// 访问开发者中心直接 404。即便路由层未拦住(如路由缓存陈旧、被直连)也在此兜底。
|
||||
if (is_market_client()) {
|
||||
abort(404, '开发者中心仅在中心站(市场服务端)可用');
|
||||
}
|
||||
|
||||
$this->view->config([
|
||||
'view_path' => ADDON_PATH . 'appmall' . DIRECTORY_SEPARATOR
|
||||
. 'view' . DIRECTORY_SEPARATOR . 'frontend' . DIRECTORY_SEPARATOR,
|
||||
]);
|
||||
// 供前端 <script src=".../wxapp.js" module="{$site.app}"> 使用(前台应用目录名)
|
||||
$this->view->assign('site', ['app' => 'frontend']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册页
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$this->view->assign('register_open', (bool) config('appmall.allow_register', true));
|
||||
$this->view->assign('need_approval', (bool) config('appmall.need_approval', true));
|
||||
return $this->view->fetch('developer/register');
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交注册(生成令牌,按配置决定是否需要审核)
|
||||
*/
|
||||
public function save()
|
||||
{
|
||||
if (! $this->request->isPost()) {
|
||||
$this->result->error('访问错误');
|
||||
}
|
||||
if (! (bool) config('appmall.allow_register', true)) {
|
||||
$this->result->error('当前未开放自助注册');
|
||||
}
|
||||
|
||||
$email = trim($this->request->param('email', ''));
|
||||
$name = trim($this->request->param('username', ''));
|
||||
if (! filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
$this->result->error('邮箱格式不正确');
|
||||
}
|
||||
if ($name === '') {
|
||||
$this->result->error('请填写开发者名称');
|
||||
}
|
||||
|
||||
if (Db::name('appmall_developers')->where('email', $email)->find()) {
|
||||
$this->result->error('该邮箱已申请过开发者令牌');
|
||||
}
|
||||
|
||||
$token = bin2hex(random_bytes(32));
|
||||
$needApproval = (bool) config('appmall.need_approval', true);
|
||||
$status = $needApproval ? 0 : 1;
|
||||
|
||||
Db::name('appmall_developers')->insert([
|
||||
'username' => $name,
|
||||
'email' => $email,
|
||||
'token' => $token,
|
||||
'status' => $status,
|
||||
'create_at' => time(),
|
||||
'update_at' => time(),
|
||||
]);
|
||||
|
||||
if ($needApproval) {
|
||||
Mailer::send($email, '开发者入驻申请已收到', $this->pendingMail($name));
|
||||
$this->result->success([], '注册成功,等待运营审核,通过后令牌将发送至您的邮箱');
|
||||
}
|
||||
|
||||
Mailer::send($email, '您的开发者令牌', $this->tokenMail($name, $token));
|
||||
$this->result->success(
|
||||
['token' => $token],
|
||||
'注册成功,请将下方令牌配置到开发者控制台的 APPMARKET_DEV_TOKEN'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询我的令牌
|
||||
*/
|
||||
public function mine()
|
||||
{
|
||||
return $this->view->fetch('developer/mine');
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据邮箱取回令牌(已激活才返回,并重发邮件)
|
||||
*/
|
||||
public function lookup()
|
||||
{
|
||||
if (! $this->request->isPost()) {
|
||||
$this->result->error('访问错误');
|
||||
}
|
||||
$email = trim($this->request->param('email', ''));
|
||||
$dev = Db::name('appmall_developers')->where('email', $email)->find();
|
||||
if (! $dev) {
|
||||
$this->result->error('未找到该邮箱的开发者记录');
|
||||
}
|
||||
$status = (int) $dev['status'];
|
||||
if ($status === 0) {
|
||||
$this->result->error('您的申请正在审核中,请耐心等待');
|
||||
}
|
||||
if ($status === -1) {
|
||||
$this->result->error('开发者账号已被禁用,请联系运营');
|
||||
}
|
||||
Mailer::send($email, '您的开发者令牌', $this->tokenMail($dev['username'], $dev['token']));
|
||||
$this->result->success(
|
||||
['token' => $dev['token'], 'username' => $dev['username']],
|
||||
'令牌如下(已重新发送至邮箱)'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 我的收益(令牌认证):返回余额与近期收益账本
|
||||
*/
|
||||
public function earnings()
|
||||
{
|
||||
$this->ensureTables();
|
||||
$token = $this->request->param('token', '');
|
||||
$dev = Db::name('appmall_developers')->where('token', $token)->where('status', 1)->find();
|
||||
if (empty($dev)) {
|
||||
return $this->result->error('令牌无效');
|
||||
}
|
||||
$revenues = Db::name('appmall_addon_revenues')->where('developer_id', $dev['id'])
|
||||
->order('id', 'desc')->limit(20)->select()->toArray();
|
||||
$this->result->success([
|
||||
'balance' => $dev['balance'] ?? 0,
|
||||
'frozen_balance' => $dev['frozen_balance'] ?? 0,
|
||||
'revenues' => $revenues,
|
||||
], '获取成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 发起提现(令牌认证):可提现余额 → 冻结,生成提现单待运营打款
|
||||
*/
|
||||
public function withdraw()
|
||||
{
|
||||
if (! $this->request->isPost()) {
|
||||
return $this->result->error('访问错误');
|
||||
}
|
||||
$this->ensureTables();
|
||||
$token = $this->request->param('token', '');
|
||||
$amount = (float) $this->request->param('amount', 0);
|
||||
$channel = $this->request->param('channel', 'alipay');
|
||||
$account = trim($this->request->param('account', ''));
|
||||
$dev = Db::name('appmall_developers')->where('token', $token)->where('status', 1)->find();
|
||||
if (empty($dev)) {
|
||||
return $this->result->error('令牌无效');
|
||||
}
|
||||
if ($amount <= 0) {
|
||||
return $this->result->error('提现金额必须大于 0');
|
||||
}
|
||||
if ($amount > (float) ($dev['balance'] ?? 0)) {
|
||||
return $this->result->error('可提现余额不足');
|
||||
}
|
||||
if ($account === '') {
|
||||
return $this->result->error('请填写收款账号');
|
||||
}
|
||||
Db::transaction(function () use ($dev, $amount, $channel, $account) {
|
||||
Db::name('appmall_developers')->where('id', $dev['id'])
|
||||
->dec('balance', $amount)->inc('frozen_balance', $amount)->update();
|
||||
Db::name('appmall_addon_withdrawals')->insert([
|
||||
'developer_id' => $dev['id'],
|
||||
'amount' => $amount,
|
||||
'channel' => $channel,
|
||||
'account' => $account,
|
||||
'status' => 0,
|
||||
'create_at' => time(),
|
||||
'update_at' => time(),
|
||||
]);
|
||||
});
|
||||
return $this->result->success([], '提现申请已提交,等待运营打款');
|
||||
}
|
||||
|
||||
/**
|
||||
* 开发者控制台入口(统一 DZ 风格布局):令牌认证后渲染控制台
|
||||
* 路由:GET /appmall/developer/console?token=xxx
|
||||
*/
|
||||
public function console()
|
||||
{
|
||||
$this->view->assign('token', $this->request->param('token', ''));
|
||||
return $this->view->fetch('developer/console');
|
||||
}
|
||||
|
||||
/**
|
||||
* 开发者控制台(兼容旧入口,重定向到统一控制台)
|
||||
*/
|
||||
public function dashboard()
|
||||
{
|
||||
$this->view->assign('token', $this->request->param('token', ''));
|
||||
return $this->view->fetch('developer/console');
|
||||
}
|
||||
|
||||
/**
|
||||
* 控制台概览数据(令牌认证):应用数 / 待审数 / 总下载 / 余额 / 累计收益 / 近期提交
|
||||
* 路由:POST /appmall/developer/overview
|
||||
*/
|
||||
public function overview()
|
||||
{
|
||||
$this->ensureTables();
|
||||
$dev = $this->authDev();
|
||||
if (empty($dev)) {
|
||||
return $this->result->error('令牌无效');
|
||||
}
|
||||
$devId = (int) $dev['id'];
|
||||
$appCount = Db::name('appmall_addon_list')->where('developer_id', $devId)->where('status', 1)->count();
|
||||
$pendingCount = Db::name('appmall_addon_submissions')->where('developer_id', $devId)->where('status', 0)->count();
|
||||
$totalDownload = (int) Db::name('appmall_addon_list')->where('developer_id', $devId)->sum('download_count');
|
||||
$totalIncome = (float) Db::name('appmall_addon_revenues')->where('developer_id', $devId)->where('status', 1)->sum('income');
|
||||
$recent = Db::name('appmall_addon_submissions')->where('developer_id', $devId)
|
||||
->field('title,version,status,update_at,type')->order('id', 'desc')->limit(5)->select()->toArray();
|
||||
$this->result->success([
|
||||
'username' => $dev['username'],
|
||||
'email' => $dev['email'],
|
||||
'balance' => $dev['balance'] ?? 0,
|
||||
'frozen_balance' => $dev['frozen_balance'] ?? 0,
|
||||
'app_count' => $appCount,
|
||||
'pending_count' => $pendingCount,
|
||||
'total_downloads' => $totalDownload,
|
||||
'total_income' => $totalIncome,
|
||||
'recent' => $recent,
|
||||
], '获取成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 我的应用(令牌认证):已上架/下架应用 + 提交记录(待审/驳回)合并返回
|
||||
* 路由:POST /appmall/developer/apps
|
||||
*/
|
||||
public function apps()
|
||||
{
|
||||
$this->ensureTables();
|
||||
$dev = $this->authDev();
|
||||
if (empty($dev)) {
|
||||
return $this->result->error('令牌无效');
|
||||
}
|
||||
$devId = (int) $dev['id'];
|
||||
$listed = Db::name('appmall_addon_list')->where('developer_id', $devId)
|
||||
->field('id,name,title,version,price,logo,category,download_count,status,update_at,type')
|
||||
->order('update_at', 'desc')->select()->toArray();
|
||||
$subs = Db::name('appmall_addon_submissions')->where('developer_id', $devId)
|
||||
->field('id,name,title,version,price,logo,category,status,reject_reason,update_at,type')
|
||||
->order('update_at', 'desc')->select()->toArray();
|
||||
$this->result->success(['listed' => $listed, 'submissions' => $subs], '获取成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改我的应用价格(令牌认证,仅能改自己名下的应用;无需重新打包/重新提审)
|
||||
* 路由:POST /appmall/developer/price {token, name, price}
|
||||
*/
|
||||
public function price()
|
||||
{
|
||||
if (! $this->request->isPost()) {
|
||||
return $this->result->error('访问错误');
|
||||
}
|
||||
$this->ensureTables();
|
||||
$dev = $this->authDev();
|
||||
if (empty($dev)) {
|
||||
return $this->result->error('令牌无效');
|
||||
}
|
||||
$name = trim((string) $this->request->param('name', ''));
|
||||
$price = (float) $this->request->param('price', -1);
|
||||
try {
|
||||
\addon\appmall\service\MarketService::instance()
|
||||
->updatePrice($name, $price, (int) $dev['id']);
|
||||
} catch (\addon\appmall\service\MarketException $e) {
|
||||
return $this->result->error($e->getMessage());
|
||||
}
|
||||
return $this->result->success([], '价格已更新,市场即时生效');
|
||||
}
|
||||
|
||||
/**
|
||||
* 令牌鉴权:返回 status=1 的开发者记录,否则 null(沿用 earnings/withdraw 的契约)
|
||||
*/
|
||||
private function authDev()
|
||||
{
|
||||
$token = $this->request->param('token', '');
|
||||
return Db::name('appmall_developers')->where('token', $token)->where('status', 1)->find();
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行时自愈建表(仅当表不存在时创建),保证开发者收益/提现闭环在当前库即可跑通。
|
||||
* 字段与 install.sql、中心站 Market::ensureTables 保持一致。
|
||||
*/
|
||||
private function ensureTables(): void
|
||||
{
|
||||
$prefix = Db::getConfig('prefix') ?: '';
|
||||
$tables = [
|
||||
'appmall_developers' => "CREATE TABLE IF NOT EXISTS `{$prefix}appmall_developers` (
|
||||
`id` int unsigned NOT NULL AUTO_INCREMENT,
|
||||
`username` varchar(100) NOT NULL COMMENT '开发者名称',
|
||||
`email` varchar(120) NOT NULL COMMENT '邮箱(接收令牌/通知)',
|
||||
`token` varchar(64) NOT NULL COMMENT '开发者令牌(发布插件时校验)',
|
||||
`status` tinyint DEFAULT 0 COMMENT '0=待审核 1=正常 -1=禁用',
|
||||
`balance` decimal(10,2) DEFAULT '0.00' COMMENT '可提现余额',
|
||||
`frozen_balance` decimal(10,2) DEFAULT '0.00' COMMENT '待结算/冻结金额',
|
||||
`create_at` int DEFAULT 0,
|
||||
`update_at` int DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_email` (`email`),
|
||||
UNIQUE KEY `uk_token` (`token`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='市场开发者表';",
|
||||
'appmall_addon_revenues' => "CREATE TABLE IF NOT EXISTS `{$prefix}appmall_addon_revenues` (
|
||||
`id` int unsigned NOT NULL AUTO_INCREMENT,
|
||||
`developer_id` int unsigned NOT NULL DEFAULT '0' COMMENT '受益开发者',
|
||||
`order_id` int unsigned NOT NULL DEFAULT '0' COMMENT 'appmall_addon_orders.id',
|
||||
`aid` int unsigned NOT NULL DEFAULT '0' COMMENT 'appmall_addon_list.id',
|
||||
`uid` int unsigned NOT NULL DEFAULT '0' COMMENT '购买用户',
|
||||
`amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '订单金额',
|
||||
`commission` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '平台抽成',
|
||||
`income` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '开发者应得',
|
||||
`status` tinyint DEFAULT 0 COMMENT '0=待结算 1=已结算',
|
||||
`create_at` int DEFAULT 0,
|
||||
`settle_at` int DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_developer` (`developer_id`),
|
||||
KEY `idx_order` (`order_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='插件销售收益账本';",
|
||||
'appmall_addon_withdrawals' => "CREATE TABLE IF NOT EXISTS `{$prefix}appmall_addon_withdrawals` (
|
||||
`id` int unsigned NOT NULL AUTO_INCREMENT,
|
||||
`developer_id` int unsigned NOT NULL DEFAULT '0',
|
||||
`amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '提现金额',
|
||||
`channel` varchar(20) DEFAULT 'alipay' COMMENT '提现渠道',
|
||||
`account` varchar(100) DEFAULT '' COMMENT '收款账号',
|
||||
`status` tinyint DEFAULT 0 COMMENT '0=待处理 1=已打款 -1=驳回',
|
||||
`remark` varchar(255) DEFAULT '',
|
||||
`create_at` int DEFAULT 0,
|
||||
`update_at` int DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_developer` (`developer_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='开发者提现单';",
|
||||
];
|
||||
foreach ($tables as $t => $sql) {
|
||||
BaseModel::ensureTable($prefix . $t, $sql);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 开发者令牌邮件正文
|
||||
*/
|
||||
private function tokenMail($name, $token): string
|
||||
{
|
||||
return "尊敬的 {$name}:<br>您的开发者令牌已生成:<br><br>"
|
||||
. "<code style=\"background:#f4f4f4;padding:4px 8px;border-radius:4px;\">{$token}</code><br><br>"
|
||||
. '请将其配置到开发者控制台的 <b>APPMARKET_DEV_TOKEN</b> 环境变量后,即可在「发布到官方市场」中使用。';
|
||||
}
|
||||
|
||||
/**
|
||||
* 审核中邮件正文
|
||||
*/
|
||||
private function pendingMail($name): string
|
||||
{
|
||||
return "尊敬的 {$name}:<br>我们已收到您的开发者入驻申请,将在 1-3 个工作日内完成审核,"
|
||||
. '通过后令牌会发送至本邮箱,请注意查收。';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
<?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 addon\appmall\controller;
|
||||
|
||||
use think\facade\Db;
|
||||
use ywxapp\controller\FrontendBase;
|
||||
|
||||
/**
|
||||
* 主框架发布/下载展示页(公开,仅中心站)
|
||||
*
|
||||
* 列出已发布的主框架版本,提供「整包 / 增量补丁 / 完整安装包」下载入口。
|
||||
* 下载走对外 API /appmall/api/framework/download(无需登录,可选签名)。
|
||||
* 路由前缀:/appmall/framework/*(由 AppService::loadAddonRoutes 外层包 Route::group('appmall'))。
|
||||
* 视图:addon/appmall/view/frontend/framework/。
|
||||
*/
|
||||
class Framework extends FrontendBase
|
||||
{
|
||||
protected $noNeedLogin = ['*'];
|
||||
protected $noNeedVerify = ['*'];
|
||||
|
||||
/**
|
||||
* 锁定视图路径到插件目录(AddonFrontend 默认指向主应用 view/frontend/,须覆盖)。
|
||||
* 仅中心站暴露:客户机不持有 framework_list 发布数据,访问即 404。
|
||||
*/
|
||||
protected function initialize()
|
||||
{
|
||||
if (is_market_client()) {
|
||||
abort(404, '框架下载页仅在中心站(市场服务端)可用');
|
||||
}
|
||||
$this->view->config([
|
||||
'view_path' => ADDON_PATH . 'appmall' . DIRECTORY_SEPARATOR
|
||||
. 'view' . DIRECTORY_SEPARATOR . 'frontend' . DIRECTORY_SEPARATOR,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发布列表展示页
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$this->ensureColumns();
|
||||
$rows = Db::name('appmall_framework_list')
|
||||
->where('status', 1)
|
||||
->order('id', 'desc')
|
||||
->field('id,version,title,changelog,file_path,patch_path,install_path,patch_from,from_version,download_count,create_at')
|
||||
->select()
|
||||
->toArray();
|
||||
foreach ($rows as &$r) {
|
||||
$r['has_full'] = !empty($r['file_path']);
|
||||
$r['has_patch'] = !empty($r['patch_path']);
|
||||
$r['has_install'] = !empty($r['install_path']);
|
||||
$r['patch_from'] = $r['patch_from'] ?: $r['from_version'];
|
||||
$r['create_at_fmt'] = $r['create_at'] ? date('Y-m-d', (int) $r['create_at']) : '';
|
||||
unset($r['file_path'], $r['patch_path'], $r['install_path']);
|
||||
}
|
||||
unset($r);
|
||||
|
||||
$this->view->assign('list', $rows);
|
||||
$this->view->assign('market_name', config('appmall.appmall_name', 'YwxApp 应用市场'));
|
||||
return $this->view->fetch('framework/index');
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行时自愈:补齐 install_path / install_hash 列(早期安装的中心站可能缺失)。
|
||||
*/
|
||||
private function ensureColumns(): void
|
||||
{
|
||||
$table = \ywxapp\model\BaseModel::currentPrefix() . 'appmall_framework_list';
|
||||
try {
|
||||
$defs = [
|
||||
'install_path' => "varchar(255) DEFAULT '' COMMENT '完整安装包 zip 物理路径'",
|
||||
'install_hash' => "varchar(64) DEFAULT '' COMMENT '完整安装包 SHA256 校验值'",
|
||||
];
|
||||
foreach ($defs as $col => $def) {
|
||||
$cols = Db::query("SHOW COLUMNS FROM `{$table}` LIKE '{$col}'");
|
||||
if (empty($cols)) {
|
||||
Db::execute("ALTER TABLE `{$table}` ADD COLUMN `{$col}` {$def}");
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
<?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 addon\appmall\controller;
|
||||
|
||||
use ywxapp\controller\FrontendBase;
|
||||
use addon\appmall\service\MarketService;
|
||||
|
||||
/**
|
||||
* 应用商店前台展示页(公开,仅中心站)
|
||||
*
|
||||
* 列出应用市场上线的插件 / 模板,卡片展示 logo、价格、评分、下载量等。
|
||||
* 数据来自中心站本地库(appmall_addon_list),复用 MarketService::catalog()。
|
||||
* 路由前缀:/appmall/store/*(由 AppService::loadAddonRoutes 外层包 Route::group('appmall'))。
|
||||
* 视图:addon/appmall/view/frontend/store/。
|
||||
*/
|
||||
class Store extends FrontendBase
|
||||
{
|
||||
protected $noNeedLogin = ['*'];
|
||||
protected $noNeedVerify = ['*'];
|
||||
|
||||
/**
|
||||
* 锁定视图路径到插件目录;仅中心站暴露(客户机不持有市场列表数据,访问即 404)。
|
||||
*/
|
||||
protected function initialize()
|
||||
{
|
||||
if (is_market_client()) {
|
||||
abort(404, '应用商店仅在中心站(市场服务端)可用');
|
||||
}
|
||||
$this->view->config([
|
||||
'view_path' => ADDON_PATH . 'appmall' . DIRECTORY_SEPARATOR
|
||||
. 'view' . DIRECTORY_SEPARATOR . 'frontend' . DIRECTORY_SEPARATOR,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 商店首页:上线插件列表
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$catalog = MarketService::instance()->catalog();
|
||||
$items = $catalog['list'] ?? [];
|
||||
$categories = $catalog['categories'] ?? [];
|
||||
|
||||
$plugins = [];
|
||||
foreach ($items as $it) {
|
||||
$price = (float) ($it['price'] ?? 0);
|
||||
$plugins[] = [
|
||||
'id' => $it['id'] ?? 0,
|
||||
'name' => $it['name'] ?? '',
|
||||
'title' => $it['title'] ?? ($it['name'] ?? ''),
|
||||
'desc' => $it['intro'] ?? '',
|
||||
'author' => $it['author'] ?? '',
|
||||
'logo' => $it['logo'] ?? '',
|
||||
'version' => $it['version'] ?? '',
|
||||
'price' => $price,
|
||||
'price_text' => $price > 0 ? '¥' . number_format($price, 2) : '免费',
|
||||
'rating' => (float) ($it['rating'] ?? 0),
|
||||
'downloads' => (int) ($it['download_count'] ?? 0),
|
||||
'category' => $it['category'] ?? '',
|
||||
'type' => $it['type'] ?? 'addon',
|
||||
];
|
||||
}
|
||||
|
||||
$this->view->assign('list', $plugins);
|
||||
$this->view->assign('categories', $categories);
|
||||
$this->view->assign('market_name', config('appmall.appmall_name', 'YwxApp 应用市场'));
|
||||
return $this->view->fetch('store/index');
|
||||
}
|
||||
|
||||
/**
|
||||
* 插件详情页(/appmall/store/detail/<name>)
|
||||
* 参考 FastAdmin 插件市场详情版式:左封面+截图、右元信息+版本历史。
|
||||
*/
|
||||
public function detail()
|
||||
{
|
||||
$name = input('name', '', 'trim');
|
||||
if ($name === '') {
|
||||
abort(404, '插件标识为空');
|
||||
}
|
||||
$catalog = MarketService::instance()->catalog();
|
||||
$hit = null;
|
||||
foreach (($catalog['list'] ?? []) as $it) {
|
||||
if (($it['name'] ?? '') === $name) {
|
||||
$hit = $it;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$hit) {
|
||||
abort(404, '插件不存在或未上架');
|
||||
}
|
||||
|
||||
$price = (float) ($hit['price'] ?? 0);
|
||||
$row = [
|
||||
'id' => $hit['id'] ?? 0,
|
||||
'name' => $hit['name'] ?? '',
|
||||
'title' => $hit['title'] ?? ($hit['name'] ?? ''),
|
||||
'intro' => $hit['intro'] ?? '',
|
||||
'author' => $hit['author'] ?? '',
|
||||
'logo' => $hit['logo'] ?? '',
|
||||
'version' => $hit['version'] ?? '',
|
||||
'price' => $price,
|
||||
'price_text' => $price > 0 ? '¥' . number_format($price, 2) : '免费',
|
||||
'rating' => (float) ($hit['rating'] ?? 0),
|
||||
'downloads' => (int) ($hit['download_count'] ?? 0),
|
||||
'category' => $hit['category'] ?? '',
|
||||
'tags' => $hit['tags'] ?? '',
|
||||
'type' => $hit['type'] ?? 'addon',
|
||||
'update_at' => (int) ($hit['update_at'] ?? 0),
|
||||
'update_fmt' => $hit['update_at'] ? date('Y-m-d', (int) $hit['update_at']) : '',
|
||||
'screenshots'=> $this->parseScreenshots($hit['screenshots'] ?? ''),
|
||||
'versions' => $this->normalizeVersions($hit['versions'] ?? []),
|
||||
];
|
||||
|
||||
$this->view->assign('addon', $row);
|
||||
$this->view->assign('market_name', config('appmall.appmall_name', 'YwxApp 应用市场'));
|
||||
return $this->view->fetch('store/detail');
|
||||
}
|
||||
|
||||
/**
|
||||
* screenshots 可能是逗号分隔字符串 / JSON 数组,归一化为数组。
|
||||
*/
|
||||
private function parseScreenshots($raw): array
|
||||
{
|
||||
if (is_array($raw)) {
|
||||
return array_values($raw);
|
||||
}
|
||||
$raw = trim((string) $raw);
|
||||
if ($raw === '') {
|
||||
return [];
|
||||
}
|
||||
if (strpos($raw, '[') === 0) {
|
||||
$dec = json_decode($raw, true);
|
||||
return is_array($dec) ? array_values($dec) : [];
|
||||
}
|
||||
return array_filter(array_map('trim', explode(',', $raw)), fn ($x) => $x !== '');
|
||||
}
|
||||
|
||||
/**
|
||||
* versions 子版本归一化(version/price/update_fmt)。
|
||||
*/
|
||||
private function normalizeVersions(array $vers): array
|
||||
{
|
||||
$out = [];
|
||||
foreach ($vers as $v) {
|
||||
$out[] = [
|
||||
'version' => (string) ($v['version'] ?? ''),
|
||||
'price' => (float) ($v['price'] ?? 0),
|
||||
'price_text'=> ((float) ($v['price'] ?? 0)) > 0 ? '¥' . number_format((float) $v['price'], 2) : '免费',
|
||||
'update_fmt'=> !empty($v['update_at']) ? date('Y-m-d', (int) $v['update_at']) : '',
|
||||
];
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
<?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 addon\appmall\controller\api;
|
||||
|
||||
use think\facade\Config;
|
||||
use think\facade\Db;
|
||||
use think\facade\Request;
|
||||
|
||||
/**
|
||||
* 主框架升级服务 API
|
||||
*
|
||||
* 契约对齐客户端 ywxapp\service\FrameworkService:
|
||||
* - GET /api/framework/version 返回最新版本 + 历史版本(客户端检测更新)
|
||||
* - GET /api/framework/download 下载核心包 zip(可选签名校验)
|
||||
* 原 upgrade 插件功能,已合并进 market(应用服务中心)。
|
||||
*/
|
||||
class Framework
|
||||
{
|
||||
/**
|
||||
* 版本列表 / 最新版本
|
||||
*/
|
||||
public function version()
|
||||
{
|
||||
$this->ensureColumns();
|
||||
try {
|
||||
$rows = Db::name('appmall_framework_list')
|
||||
->where('status', 1)
|
||||
->order('id', 'desc')
|
||||
->limit(20)
|
||||
->field('id,version,title,changelog,type,from_version,patch_from,file_path,patch_path,install_path,create_at')
|
||||
->select()
|
||||
->toArray();
|
||||
} catch (\Exception $e) {
|
||||
return json(['code' => 0, 'msg' => '读取版本列表失败:' . $e->getMessage()]);
|
||||
}
|
||||
|
||||
$latest = $rows[0] ?? null;
|
||||
$hasFull = !empty($latest['file_path']);
|
||||
$hasPatch = !empty($latest['patch_path']);
|
||||
$hasInstall = !empty($latest['install_path'] ?? '');
|
||||
$patchFrom = $latest['patch_from'] ?? '';
|
||||
if ($patchFrom === '' && $hasPatch) {
|
||||
$patchFrom = $latest['from_version'] ?? '';
|
||||
}
|
||||
|
||||
// 不向客户端泄露物理路径
|
||||
foreach ($rows as &$r) {
|
||||
$r['has_full'] = !empty($r['file_path']);
|
||||
$r['has_patch'] = !empty($r['patch_path']);
|
||||
$r['has_install'] = !empty($r['install_path']);
|
||||
unset($r['file_path'], $r['patch_path'], $r['install_path']);
|
||||
}
|
||||
unset($r);
|
||||
|
||||
return json([
|
||||
'code' => 1,
|
||||
'msg' => 'success',
|
||||
'data' => [
|
||||
'version' => $latest['version'] ?? '',
|
||||
'title' => $latest['title'] ?? '',
|
||||
'changelog' => $latest['changelog'] ?? '',
|
||||
// 兼容旧客户端:type 只表达「整包缺失时才是纯补丁」
|
||||
'type' => $hasFull ? 0 : ($hasPatch ? 1 : (int) ($latest['type'] ?? 0)),
|
||||
'from_version'=> $patchFrom,
|
||||
// 新协议:整包与补丁可同版本共存,客户端按自身版本择优
|
||||
'has_full' => $hasFull,
|
||||
'has_patch' => $hasPatch,
|
||||
'has_install' => $hasInstall,
|
||||
'patch_from' => $patchFrom,
|
||||
'release_at' => $latest['create_at'] ?? 0,
|
||||
'history' => $rows,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 兼容已安装库:补齐 type / from_version / patch_* 列,并迁移旧「补丁行」数据
|
||||
*/
|
||||
private function ensureColumns(): void
|
||||
{
|
||||
$table = \ywxapp\model\BaseModel::currentPrefix() . 'appmall_framework_list';
|
||||
try {
|
||||
$defs = [
|
||||
'type' => "tinyint DEFAULT 0 COMMENT '0=整包 1=补丁'",
|
||||
'from_version' => "varchar(20) DEFAULT '' COMMENT '补丁基础版本'",
|
||||
'patch_path' => "varchar(255) DEFAULT '' COMMENT '增量补丁 zip 物理路径'",
|
||||
'patch_hash' => "varchar(64) DEFAULT '' COMMENT '补丁 SHA256 校验值'",
|
||||
'patch_from' => "varchar(20) DEFAULT '' COMMENT '补丁适用的基础版本'",
|
||||
'install_path' => "varchar(255) DEFAULT '' COMMENT '完整安装包 zip 物理路径'",
|
||||
'install_hash' => "varchar(64) DEFAULT '' COMMENT '完整安装包 SHA256 校验值'",
|
||||
];
|
||||
foreach ($defs as $col => $def) {
|
||||
$cols = Db::query("SHOW COLUMNS FROM `{$table}` LIKE '{$col}'");
|
||||
if (empty($cols)) {
|
||||
Db::execute("ALTER TABLE `{$table}` ADD COLUMN `{$col}` {$def}");
|
||||
}
|
||||
}
|
||||
Db::execute(
|
||||
"UPDATE `{$table}` SET patch_path = file_path, patch_hash = file_hash, patch_from = from_version,"
|
||||
. " file_path = '', file_hash = ''"
|
||||
. " WHERE type = 1 AND (patch_path = '' OR patch_path IS NULL) AND file_path <> ''"
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载核心包 zip
|
||||
* 客户端 FrameworkService::download() GET ?version= :
|
||||
* - 直接返回 zip 二进制(首字节非 '{')。
|
||||
* 可选签名校验:config('appmall.addon_download_sign') = true 时,
|
||||
* 请求需带 sign=md5('framework'.version.ts.secret) & ts(5 分钟有效期)。
|
||||
*/
|
||||
public function download()
|
||||
{
|
||||
$version = Request::param('version', '');
|
||||
// 选包:type=install/2 下安装包;type=patch/1 下补丁;type=full/0 下整包;缺省整包(缺失时回退补丁/安装包)
|
||||
$typeRaw = (string) Request::param('type', '');
|
||||
$wantPatch = in_array($typeRaw, ['patch', '1'], true);
|
||||
$wantInstall = in_array($typeRaw, ['install', '2'], true);
|
||||
|
||||
if (Config::get('ywxapp.addon_download_sign', false)) {
|
||||
$sign = Request::param('sign', '');
|
||||
$ts = (int) Request::param('ts', 0);
|
||||
if (!$this->verifySign($version, $ts, $sign)) {
|
||||
return json(['code' => 0, 'message' => '签名校验失败']);
|
||||
}
|
||||
}
|
||||
|
||||
// 运行时自愈:已部署的中心站若早于本版本安装,可能缺下载日志表
|
||||
$this->ensureTables();
|
||||
|
||||
$query = Db::name('appmall_framework_list')->where('status', 1);
|
||||
if ($version !== '') {
|
||||
$query->where('version', $version);
|
||||
}
|
||||
$row = $query->order('id', 'desc')->find();
|
||||
if (empty($row)) {
|
||||
return json(['code' => 0, 'message' => '版本包不存在']);
|
||||
}
|
||||
|
||||
if ($wantInstall) {
|
||||
$path = $row['install_path'] ?? '';
|
||||
} elseif ($wantPatch) {
|
||||
$path = $row['patch_path'] ?? '';
|
||||
} else {
|
||||
$path = $row['file_path'] ?? '';
|
||||
// 兼容「仅补丁/仅安装包」版本行:未显式指定时整包缺失则回退
|
||||
if (($path === '' || !is_file($path)) && !empty($row['patch_path'])) {
|
||||
$path = $row['patch_path'];
|
||||
$wantPatch = true;
|
||||
}
|
||||
if (($path === '' || !is_file($path)) && !empty($row['install_path'])) {
|
||||
$path = $row['install_path'];
|
||||
$wantInstall = true;
|
||||
}
|
||||
}
|
||||
if (empty($path) || !is_file($path)) {
|
||||
$msg = $wantInstall ? '该版本无完整安装包或文件缺失'
|
||||
: ($wantPatch ? '该版本无增量补丁或文件缺失' : '版本包不存在或文件缺失');
|
||||
return json(['code' => 0, 'message' => $msg]);
|
||||
}
|
||||
|
||||
// 记录下载次数与日志(列/表缺失时忽略,不影响下载)
|
||||
try {
|
||||
Db::name('appmall_framework_list')->where('id', $row['id'])->inc('download_count')->update();
|
||||
Db::name('appmall_framework_download_log')->insert([
|
||||
'version' => $row['version'],
|
||||
'uid' => (int) Request::param('uid', 0),
|
||||
'ip' => Request::ip(),
|
||||
'create_at' => time(),
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
$downloadName = 'ywxapp-' . $row['version']
|
||||
. ($wantInstall ? '-install' : ($wantPatch ? '-patch' : '')) . '.zip';
|
||||
|
||||
// 流式下载响应(复用公共类):避免 ThinkPHP download() 内部 file_get_contents 将整文件读入内存,
|
||||
// 改用分块 fread 输出并支持 Range 续传;类内部已 set_time_limit(0)。
|
||||
return new \ywxapp\library\StreamZipResponse($path, $downloadName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行时自愈建表:保证 appmall_framework_download_log 存在(早期安装的中心站可能缺失)。
|
||||
*/
|
||||
private function ensureTables(): void
|
||||
{
|
||||
$prefix = \ywxapp\model\BaseModel::currentPrefix();
|
||||
$tables = [
|
||||
'appmall_framework_download_log' => "CREATE TABLE IF NOT EXISTS `{$prefix}appmall_framework_download_log` (
|
||||
`id` int unsigned NOT NULL AUTO_INCREMENT,
|
||||
`version` varchar(20) NOT NULL COMMENT '下载的版本号',
|
||||
`uid` int DEFAULT 0 COMMENT '下载用户ID(来自客户端)',
|
||||
`ip` varchar(45) DEFAULT '' COMMENT '下载来源IP',
|
||||
`create_at` int DEFAULT 0 COMMENT '下载时间',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_version` (`version`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='主框架下载日志表';",
|
||||
];
|
||||
foreach ($tables as $t => $sql) {
|
||||
try {
|
||||
if (empty(Db::query("SHOW TABLES LIKE '{$prefix}{$t}'"))) {
|
||||
Db::execute($sql);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// 忽略(如权限不足),由后续业务报错暴露
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载签名校验
|
||||
*/
|
||||
private function verifySign(string $version, int $ts, string $sign): bool
|
||||
{
|
||||
if ($sign === '' || abs(time() - $ts) > 300) {
|
||||
return false;
|
||||
}
|
||||
$secret = Config::get('ywxapp.addon_secret', 'ywxapp-addon-secret-change-me');
|
||||
$expect = md5('framework' . $version . $ts . $secret);
|
||||
return hash_equals($expect, $sign);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,92 @@
|
||||
<?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 addon\appmall\controller\backend;
|
||||
|
||||
use think\facade\Db;
|
||||
use think\facade\View;
|
||||
use Exception;
|
||||
use ywxapp\controller\BackendBase;
|
||||
|
||||
/**
|
||||
* 插件审核(运营后台,中心站)
|
||||
*
|
||||
* 审核开发者通过 Market::submit 提交的插件上架申请(appmall_addon_submissions)。
|
||||
* status: 0=待审核 1=已发布 2=已驳回。
|
||||
*/
|
||||
class AddonReview extends BackendBase
|
||||
{
|
||||
/**
|
||||
* 审核列表 / 页面
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
if ($this->request->isAjax()) {
|
||||
$status = $this->request->param('status', '');
|
||||
$query = Db::name('appmall_addon_submissions')->order('id', 'desc');
|
||||
if ($status !== '') {
|
||||
$query->where('status', (int) $status);
|
||||
}
|
||||
$list = $query->paginate([
|
||||
'page' => (int) $this->request->param('page', 1),
|
||||
'list_rows' => (int) $this->request->param('limit', 10),
|
||||
]);
|
||||
$this->result->setCount($list->total())->success($list->items(), '获取成功');
|
||||
}
|
||||
return View::fetch('addonreview/index');
|
||||
}
|
||||
|
||||
/**
|
||||
* 审核通过:标记已发布(status=1)
|
||||
* 注意:正式上架到 appmall_addon_list 由发布流程处理,此处仅切换审核态。
|
||||
*/
|
||||
public function audit()
|
||||
{
|
||||
try {
|
||||
$id = (int) input('id');
|
||||
$row = Db::name('appmall_addon_submissions')->where('id', $id)->find();
|
||||
if (empty($row)) {
|
||||
return $this->result->error('提交记录不存在');
|
||||
}
|
||||
if ((int) $row['status'] === 1) {
|
||||
return $this->result->error('该提交已发布');
|
||||
}
|
||||
Db::name('appmall_addon_submissions')->where('id', $id)
|
||||
->update(['status' => 1, 'reject_reason' => '', 'update_at' => time()]);
|
||||
return $this->result->success([], '已通过审核');
|
||||
} catch (Exception $e) {
|
||||
return $this->result->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 驳回:标记已驳回(status=2)并记录原因
|
||||
*/
|
||||
public function reject()
|
||||
{
|
||||
try {
|
||||
$id = (int) input('id');
|
||||
$reason = trim(input('reason', ''));
|
||||
if ($reason === '') {
|
||||
return $this->result->error('请填写驳回原因');
|
||||
}
|
||||
$row = Db::name('appmall_addon_submissions')->where('id', $id)->find();
|
||||
if (empty($row)) {
|
||||
return $this->result->error('提交记录不存在');
|
||||
}
|
||||
Db::name('appmall_addon_submissions')->where('id', $id)
|
||||
->update(['status' => 2, 'reject_reason' => $reason, 'update_at' => time()]);
|
||||
return $this->result->success([], '已驳回');
|
||||
} catch (Exception $e) {
|
||||
return $this->result->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
<?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 addon\appmall\controller\backend;
|
||||
|
||||
use think\facade\Db;
|
||||
use think\facade\View;
|
||||
use Exception;
|
||||
use ywxapp\controller\BackendBase;
|
||||
use ywxapp\library\Mailer;
|
||||
|
||||
/**
|
||||
* 开发者管理(运营后台,含审核)
|
||||
* 由原核心 app/backend/controller/Developer.php 剥离到 market 插件。
|
||||
*/
|
||||
class Developer extends BackendBase
|
||||
{
|
||||
|
||||
public function index()
|
||||
{
|
||||
if ($this->request->isAjax()) {
|
||||
$status = $this->request->param('status', '');
|
||||
$query = Db::name('appmall_developers');
|
||||
if ($status !== '') {
|
||||
$query->where('status', (int) $status);
|
||||
}
|
||||
$list = $query->order('status', 'asc')
|
||||
->order('id', 'desc')
|
||||
->paginate([
|
||||
'page' => (int) $this->request->param('page', 1),
|
||||
'list_rows' => (int) $this->request->param('limit', 10),
|
||||
]);
|
||||
$this->result->setCount($list->total())->success($list->items(), '获取成功');
|
||||
}
|
||||
return View::fetch('developer/index');
|
||||
}
|
||||
|
||||
/**
|
||||
* 审核通过:激活并邮件下发令牌
|
||||
*/
|
||||
public function approve()
|
||||
{
|
||||
try {
|
||||
$id = (int) input('id');
|
||||
$dev = Db::name('appmall_developers')->where('id', $id)->find();
|
||||
if (empty($dev)) {
|
||||
return $this->result->error('记录不存在');
|
||||
}
|
||||
Db::name('appmall_developers')->where('id', $id)->update(['status' => 1, 'update_at' => time()]);
|
||||
Mailer::send($dev['email'], '开发者申请已通过', $this->tokenMail($dev['username'], $dev['token']));
|
||||
return $this->result->success([], '已通过并发送令牌邮件');
|
||||
} catch (Exception $e) {
|
||||
return $this->result->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 禁用
|
||||
*/
|
||||
public function disable()
|
||||
{
|
||||
$id = (int) input('id');
|
||||
Db::name('appmall_developers')->where('id', $id)->update(['status' => -1, 'update_at' => time()]);
|
||||
return $this->result->success([], '已禁用');
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新发送令牌邮件(仅已激活)
|
||||
*/
|
||||
public function resend()
|
||||
{
|
||||
$id = (int) input('id');
|
||||
$dev = Db::name('appmall_developers')->where('id', $id)->find();
|
||||
if (empty($dev)) {
|
||||
return $this->result->error('记录不存在');
|
||||
}
|
||||
if ((int) $dev['status'] !== 1) {
|
||||
return $this->result->error('仅已激活账号可重发');
|
||||
}
|
||||
Mailer::send($dev['email'], '您的开发者令牌', $this->tokenMail($dev['username'], $dev['token']));
|
||||
return $this->result->success([], '已重发令牌邮件');
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置令牌(已激活则同步邮件通知)
|
||||
*/
|
||||
public function reset()
|
||||
{
|
||||
try {
|
||||
$id = (int) input('id');
|
||||
$dev = Db::name('appmall_developers')->where('id', $id)->find();
|
||||
if (empty($dev)) {
|
||||
return $this->result->error('记录不存在');
|
||||
}
|
||||
$token = bin2hex(random_bytes(32));
|
||||
Db::name('appmall_developers')->where('id', $id)->update(['token' => $token, 'update_at' => time()]);
|
||||
if ((int) $dev['status'] === 1) {
|
||||
Mailer::send($dev['email'], '您的开发者令牌已重置', $this->tokenMail($dev['username'], $token));
|
||||
}
|
||||
return $this->result->success(['token' => $token], '已重置令牌' . ((int) $dev['status'] === 1 ? '并邮件通知' : ''));
|
||||
} catch (Exception $e) {
|
||||
return $this->result->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private function tokenMail($name, $token): string
|
||||
{
|
||||
return "尊敬的 {$name}:<br>您的开发者令牌如下:<br><br>"
|
||||
. "<code style=\"background:#f4f4f4;padding:4px 8px;border-radius:4px;\">{$token}</code><br><br>"
|
||||
. '请将其配置到开发者控制台的 <b>APPMARKET_DEV_TOKEN</b> 环境变量。';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,296 @@
|
||||
<?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 addon\appmall\controller\backend;
|
||||
|
||||
use think\facade\Db;
|
||||
use think\facade\View;
|
||||
use Exception;
|
||||
use ywxapp\controller\BackendBase;
|
||||
use ywxapp\library\StreamZipResponse;
|
||||
|
||||
/**
|
||||
* 主框架版本管理(运营后台,中心站)
|
||||
*
|
||||
* 上传核心包 zip → 入库(草稿)→ 发布(status=1)→ 客户端可见可下载。
|
||||
* 原 upgrade 插件功能,已合并进 market(应用服务中心)。
|
||||
*/
|
||||
class Framework extends BackendBase
|
||||
{
|
||||
/**
|
||||
* 版本列表 / 页面
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
if ($this->request->isAjax()) {
|
||||
$this->ensurePatchColumns();
|
||||
$status = $this->request->param('status', '');
|
||||
$query = Db::name('appmall_framework_list');
|
||||
if ($status !== '') {
|
||||
$query->where('status', (int) $status);
|
||||
}
|
||||
$list = $query->order('id', 'desc')
|
||||
->paginate([
|
||||
'page' => (int) $this->request->param('page', 1),
|
||||
'list_rows' => (int) $this->request->param('limit', 10),
|
||||
]);
|
||||
$this->result->setCount($list->total())->success($list->items(), '获取成功');
|
||||
}
|
||||
return View::fetch('framework/index');
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传并登记一个主框架核心包(zip),默认草稿态
|
||||
*
|
||||
* 同一版本号对应一行记录:整包(type=0)写 file_path/file_hash,
|
||||
* 增量补丁(type=1)写 patch_path/patch_hash/patch_from,二者可先后上传共存。
|
||||
*/
|
||||
public function upload()
|
||||
{
|
||||
try {
|
||||
$this->ensurePatchColumns();
|
||||
$version = trim(input('version', ''));
|
||||
$title = trim(input('title', ''));
|
||||
$changelog = input('changelog', '');
|
||||
$type = (int) input('type', 0); // 0=整包 1=补丁 2=完整安装包
|
||||
$fromVer = trim(input('from_version', ''));
|
||||
if (!preg_match('/^\d+\.\d+\.\d+$/', $version)) {
|
||||
return $this->result->error('版本号格式不正确(需 x.y.z)');
|
||||
}
|
||||
if ($type === 1 && $fromVer === '') {
|
||||
return $this->result->error('增量补丁必须填写「基础版本」');
|
||||
}
|
||||
if ($type === 1 && !preg_match('/^\d+\.\d+\.\d+$/', $fromVer)) {
|
||||
return $this->result->error('基础版本号格式不正确(需 x.y.z)');
|
||||
}
|
||||
$file = $this->request->file('file');
|
||||
if (empty($file)) {
|
||||
return $this->result->error('请上传核心包 zip');
|
||||
}
|
||||
if (strtolower(substr($file->getOriginalName(), -4)) !== '.zip') {
|
||||
return $this->result->error('仅支持 zip 格式');
|
||||
}
|
||||
|
||||
// 整包需校验 ywxapp 核心结构;补丁只校验为合法 zip(含变更文件即可)
|
||||
$tmp = $file->getRealPath();
|
||||
$zip = new \ZipArchive();
|
||||
if ($zip->open($tmp) !== true) {
|
||||
return $this->result->error('zip 包无法打开');
|
||||
}
|
||||
if ($type === 0) {
|
||||
$hasCore = false;
|
||||
for ($i = 0; $i < $zip->numFiles; $i++) {
|
||||
$nm = $zip->statIndex($i)['name'];
|
||||
if (preg_match('#^(ywxapp/)?(service|controller|library|traits)/#', $nm)) {
|
||||
$hasCore = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
$zip->close();
|
||||
if (!$hasCore) {
|
||||
return $this->result->error('核心包结构不正确:应包含 ywxapp 核心目录(service/controller/...)');
|
||||
}
|
||||
} elseif ($type === 2) {
|
||||
// 完整安装包:必须含 config/ 与占位符 .env,且不得夹带 runtime/vendor/data 或真实 .env.*
|
||||
$hasConfig = false;
|
||||
$hasEnv = false;
|
||||
$bad = false;
|
||||
for ($i = 0; $i < $zip->numFiles; $i++) {
|
||||
$nm = $zip->statIndex($i)['name'];
|
||||
if ($nm === 'config' || strpos($nm, 'config/') === 0) {
|
||||
$hasConfig = true;
|
||||
}
|
||||
if ($nm === '.env') {
|
||||
$hasEnv = true;
|
||||
}
|
||||
if (preg_match('#^(runtime/|vendor/|data/|\.env\.)#', $nm)) {
|
||||
$bad = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
$zip->close();
|
||||
if ($bad) {
|
||||
return $this->result->error('安装包结构不正确:不应包含 runtime/vendor/data 或真实 .env.* 文件');
|
||||
}
|
||||
if (!$hasConfig || !$hasEnv) {
|
||||
return $this->result->error('安装包结构不正确:必须包含 config/ 目录与 .env 占位符');
|
||||
}
|
||||
} else {
|
||||
$zip->close();
|
||||
}
|
||||
|
||||
$dir = root_path() . 'runtime' . DIRECTORY_SEPARATOR . 'framework' . DIRECTORY_SEPARATOR;
|
||||
if (!is_dir($dir)) {
|
||||
@mkdir($dir, 0755, true);
|
||||
}
|
||||
$saveName = 'ywxapp-' . $version . ($type === 1 ? '-patch' : ($type === 2 ? '-install' : '')) . '.zip';
|
||||
$file->move($dir, $saveName);
|
||||
$absPath = realpath($dir . $saveName);
|
||||
if (!$absPath || !is_file($absPath)) {
|
||||
return $this->result->error('核心包保存失败');
|
||||
}
|
||||
$hash = hash_file('sha256', $absPath);
|
||||
|
||||
$exist = Db::name('appmall_framework_list')->where('version', $version)->find();
|
||||
// 公共字段:标题/日志仅在填写时覆盖,避免后传的包把先前信息清空
|
||||
$data = [
|
||||
'version' => $version,
|
||||
'status' => 0, // 重新上传任一包均回到草稿,需再次发布
|
||||
'update_at' => time(),
|
||||
];
|
||||
if ($title !== '' || !$exist) {
|
||||
$data['title'] = $title ?: ('主框架 ' . $version);
|
||||
}
|
||||
if (trim((string) $changelog) !== '' || !$exist) {
|
||||
$data['changelog'] = $changelog;
|
||||
}
|
||||
if ($type === 1) {
|
||||
// 增量补丁:只更新 patch 字段,不动整包
|
||||
$data['patch_path'] = $absPath;
|
||||
$data['patch_hash'] = $hash;
|
||||
$data['patch_from'] = $fromVer;
|
||||
$data['from_version'] = $fromVer; // 兼容旧客户端镜像
|
||||
} elseif ($type === 2) {
|
||||
// 完整安装包:只更新 install 字段,与整包/补丁独立共存
|
||||
$data['install_path'] = $absPath;
|
||||
$data['install_hash'] = $hash;
|
||||
} else {
|
||||
// 整包:只更新整包字段,不动补丁
|
||||
$data['file_path'] = $absPath;
|
||||
$data['file_hash'] = $hash;
|
||||
}
|
||||
if ($exist) {
|
||||
Db::name('appmall_framework_list')->where('id', $exist['id'])->update($data);
|
||||
$id = $exist['id'];
|
||||
// type 兼容标志:有整包即 0,仅补丁为 1
|
||||
$hasFull = ($type === 0) || !empty($exist['file_path']);
|
||||
Db::name('appmall_framework_list')->where('id', $id)->update(['type' => $hasFull ? 0 : 1]);
|
||||
} else {
|
||||
$data['type'] = $type;
|
||||
$data['create_at'] = time();
|
||||
$id = Db::name('appmall_framework_list')->insertGetId($data);
|
||||
}
|
||||
$tip = $type === 1 ? '增量补丁已上传' : ($type === 2 ? '完整安装包已上传' : '整包已上传');
|
||||
return $this->result->success(['id' => $id], $tip . ',请点击「发布」使其对外可下载');
|
||||
} catch (Exception $e) {
|
||||
return $this->result->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发布:将某版本置为已发布(status=1),客户端即可检测到
|
||||
*/
|
||||
public function publish()
|
||||
{
|
||||
try {
|
||||
$id = (int) input('id');
|
||||
if ($id <= 0) {
|
||||
return $this->result->error('缺少版本ID');
|
||||
}
|
||||
$row = Db::name('appmall_framework_list')->where('id', $id)->find();
|
||||
if (empty($row)) {
|
||||
return $this->result->error('版本不存在');
|
||||
}
|
||||
if (empty($row['file_path']) && empty($row['patch_path'])) {
|
||||
return $this->result->error('该版本尚未上传任何包,无法发布');
|
||||
}
|
||||
Db::name('appmall_framework_list')->where('id', $id)->update(['status' => 1, 'update_at' => time()]);
|
||||
$tip = empty($row['file_path'])
|
||||
? (empty($row['patch_path'])
|
||||
? (empty($row['install_path']) ? '已发布' : '已发布(完整安装包,可供全新部署下载)')
|
||||
: '已发布(仅增量补丁:只有停在 ' . ($row['patch_from'] ?: $row['from_version']) . ' 的客户端可升级,建议补传整包)')
|
||||
: (empty($row['patch_path']) ? '已发布(整包)' : '已发布(整包 + 增量补丁)');
|
||||
return $this->result->success([], $tip);
|
||||
} catch (Exception $e) {
|
||||
return $this->result->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除版本(同时删除整包与补丁物理文件)
|
||||
*/
|
||||
public function delete()
|
||||
{
|
||||
try {
|
||||
$id = (int) input('id');
|
||||
if ($id <= 0) {
|
||||
return $this->result->error('缺少版本ID');
|
||||
}
|
||||
$row = Db::name('appmall_framework_list')->where('id', $id)->find();
|
||||
foreach (['file_path', 'patch_path', 'install_path'] as $col) {
|
||||
if ($row && !empty($row[$col]) && is_file($row[$col])) {
|
||||
@unlink($row[$col]);
|
||||
}
|
||||
}
|
||||
Db::name('appmall_framework_list')->where('id', $id)->delete();
|
||||
return $this->result->success([], '已删除');
|
||||
} catch (Exception $e) {
|
||||
return $this->result->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载核心包(供运营本地校验)
|
||||
* ?type=patch 下载增量补丁,默认整包
|
||||
*/
|
||||
public function download()
|
||||
{
|
||||
$id = (int) input('id');
|
||||
$kind = input('type', 'full');
|
||||
$kind = in_array($kind, ['patch', 'install'], true) ? $kind : 'full';
|
||||
$row = Db::name('appmall_framework_list')->where('id', $id)->find();
|
||||
if ($kind === 'patch') {
|
||||
$path = $row['patch_path'] ?? '';
|
||||
$suffix = '-patch';
|
||||
} elseif ($kind === 'install') {
|
||||
$path = $row['install_path'] ?? '';
|
||||
$suffix = '-install';
|
||||
} else {
|
||||
$path = $row['file_path'] ?? '';
|
||||
$suffix = '';
|
||||
}
|
||||
if (empty($row) || empty($path) || !is_file($path)) {
|
||||
return json(['code' => 0, 'msg' => '文件不存在']);
|
||||
}
|
||||
return new StreamZipResponse($path, 'ywxapp-' . $row['version'] . $suffix . '.zip');
|
||||
}
|
||||
|
||||
/**
|
||||
* 兼容已安装库:在线补齐 patch_path / patch_hash / patch_from 列
|
||||
*/
|
||||
private function ensurePatchColumns(): void
|
||||
{
|
||||
$table = \ywxapp\model\BaseModel::currentPrefix() . 'appmall_framework_list';
|
||||
try {
|
||||
$defs = [
|
||||
'patch_path' => "varchar(255) DEFAULT '' COMMENT '增量补丁 zip 物理路径'",
|
||||
'patch_hash' => "varchar(64) DEFAULT '' COMMENT '补丁 SHA256 校验值'",
|
||||
'patch_from' => "varchar(20) DEFAULT '' COMMENT '补丁适用的基础版本'",
|
||||
'install_path' => "varchar(255) DEFAULT '' COMMENT '完整安装包 zip 物理路径'",
|
||||
'install_hash' => "varchar(64) DEFAULT '' COMMENT '完整安装包 SHA256 校验值'",
|
||||
];
|
||||
foreach ($defs as $col => $def) {
|
||||
$cols = Db::query("SHOW COLUMNS FROM `{$table}` LIKE '{$col}'");
|
||||
if (empty($cols)) {
|
||||
Db::execute("ALTER TABLE `{$table}` ADD COLUMN `{$col}` {$def}");
|
||||
}
|
||||
}
|
||||
// 历史数据迁移:旧「补丁行」(type=1 且 patch_path 为空) 把整包字段挪到补丁字段
|
||||
Db::execute(
|
||||
"UPDATE `{$table}` SET patch_path = file_path, patch_hash = file_hash, patch_from = from_version,"
|
||||
. " file_path = '', file_hash = ''"
|
||||
. " WHERE type = 1 AND (patch_path = '' OR patch_path IS NULL) AND file_path <> ''"
|
||||
);
|
||||
} catch (Exception $e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
<?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 addon\appmall\controller\backend;
|
||||
|
||||
use think\facade\Db;
|
||||
use think\facade\View;
|
||||
use Exception;
|
||||
use ywxapp\controller\BackendBase;
|
||||
|
||||
/**
|
||||
* 收益与结算管理(运营后台,中心站)
|
||||
*
|
||||
* - 收益账本:每笔已支付订单按平台抽成记账,归属对应开发者;
|
||||
* - 提现单:开发者发起(或运营代发)后在此审核打款/驳回。
|
||||
*/
|
||||
class Revenue extends BackendBase
|
||||
{
|
||||
/**
|
||||
* 收益账本列表
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
if ($this->request->isAjax()) {
|
||||
$devId = $this->request->param('developer_id', '');
|
||||
$query = Db::name('appmall_addon_revenues')->order('id', 'desc');
|
||||
if ($devId !== '') {
|
||||
$query->where('developer_id', (int) $devId);
|
||||
}
|
||||
$list = $query->paginate([
|
||||
'page' => (int) $this->request->param('page', 1),
|
||||
'list_rows' => (int) $this->request->param('limit', 10),
|
||||
]);
|
||||
// 汇总:总销售额 / 总抽成 / 总开发者收益
|
||||
$summary = Db::name('appmall_addon_revenues')
|
||||
->field('SUM(amount) as total_amount, SUM(commission) as total_commission, SUM(income) as total_income')
|
||||
->find();
|
||||
$this->result->setCount($list->total())->success([
|
||||
'list' => $list->items(),
|
||||
'summary' => [
|
||||
'total_amount' => $summary['total_amount'] ?? 0,
|
||||
'total_commission' => $summary['total_commission'] ?? 0,
|
||||
'total_income' => $summary['total_income'] ?? 0,
|
||||
],
|
||||
], '获取成功');
|
||||
}
|
||||
return View::fetch('revenue/index');
|
||||
}
|
||||
|
||||
/**
|
||||
* 提现单列表
|
||||
*/
|
||||
public function withdrawals()
|
||||
{
|
||||
if ($this->request->isAjax()) {
|
||||
$status = $this->request->param('status', '');
|
||||
$query = Db::name('appmall_addon_withdrawals')->order('id', 'desc');
|
||||
if ($status !== '') {
|
||||
$query->where('status', (int) $status);
|
||||
}
|
||||
$list = $query->paginate([
|
||||
'page' => (int) $this->request->param('page', 1),
|
||||
'list_rows' => (int) $this->request->param('limit', 10),
|
||||
]);
|
||||
$this->result->setCount($list->total())->success($list->items(), '获取成功');
|
||||
}
|
||||
return View::fetch('revenue/withdrawal');
|
||||
}
|
||||
|
||||
/**
|
||||
* 审核通过:标记已打款,释放冻结金额,并将对应收益置为已结算
|
||||
*/
|
||||
public function settle()
|
||||
{
|
||||
try {
|
||||
$id = (int) input('id');
|
||||
$w = Db::name('appmall_addon_withdrawals')->where('id', $id)->find();
|
||||
if (empty($w)) {
|
||||
return $this->result->error('提现单不存在');
|
||||
}
|
||||
if ((int) $w['status'] !== 0) {
|
||||
return $this->result->error('该单已处理');
|
||||
}
|
||||
Db::transaction(function () use ($w) {
|
||||
// 释放冻结(实际打款为线下/对接支付,此处仅结算记账)
|
||||
Db::name('appmall_developers')->where('id', $w['developer_id'])
|
||||
->dec('frozen_balance', $w['amount'])->update();
|
||||
Db::name('appmall_addon_withdrawals')->where('id', $w['id'])
|
||||
->update(['status' => 1, 'update_at' => time()]);
|
||||
// 同步将该开发者待结算收益置为已结算
|
||||
Db::name('appmall_addon_revenues')->where('developer_id', $w['developer_id'])
|
||||
->where('status', 0)
|
||||
->update(['status' => 1, 'settle_at' => time()]);
|
||||
});
|
||||
return $this->result->success([], '已结算打款');
|
||||
} catch (Exception $e) {
|
||||
return $this->result->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 驳回提现:冻结金额退回开发者可提现余额
|
||||
*/
|
||||
public function rejectWithdrawal()
|
||||
{
|
||||
try {
|
||||
$id = (int) input('id');
|
||||
$reason = input('reason', '');
|
||||
$w = Db::name('appmall_addon_withdrawals')->where('id', $id)->find();
|
||||
if (empty($w)) {
|
||||
return $this->result->error('提现单不存在');
|
||||
}
|
||||
if ((int) $w['status'] !== 0) {
|
||||
return $this->result->error('该单已处理');
|
||||
}
|
||||
Db::transaction(function () use ($w, $reason) {
|
||||
Db::name('appmall_developers')->where('id', $w['developer_id'])
|
||||
->dec('frozen_balance', $w['amount'])
|
||||
->inc('balance', $w['amount'])->update();
|
||||
Db::name('appmall_addon_withdrawals')->where('id', $w['id'])
|
||||
->update(['status' => -1, 'remark' => $reason, 'update_at' => time()]);
|
||||
});
|
||||
return $this->result->success([], '已驳回,金额已退回');
|
||||
} catch (Exception $e) {
|
||||
return $this->result->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user