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
@@ -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());
}
}
}