feat: 上线前安全加固与发布工程化;appmall 由插件迁移为独立应用

安全(P0)
- 发布包签名:新增 scripts/package_keygen.php、scripts/package_verify.php、
  ywxapp\service\PackageVerifier;FrameworkService::apply() 解压前强制验签
  (ywxapp.framework_package_verify,默认开启)
- JWT 收紧:config/jwt.php 移除公开默认密钥,JwtService::assertStrongSecret()
  校验非空 / 长度≥32 / 已知弱值;.env.production.example 键名由 JWT_SECRET_KEY 修正为 JWT_SECRET
- /api/v1/Article:免登录白名单收窄为只读接口,写操作字段白名单 + assertOwner() 越权校验
- 开发者令牌:查找接口不再回显明文、统一失败文案 + 限流
- TemplateInstaller:文件名白名单 + realpath 归一化 + 目录内断言,修复 Zip Slip;补 use ZipArchive
- 后台登录:接入验证码、失败锁定与登录限流(config/ratelimit.php)
- 缓存 / Session 驱动可由 .env 覆盖(CACHE_DRIVER / SESSION_TYPE),集群部署改 redis
- paycenter 支付日志开关改为 PAY_LOG,不再跟随 APP_DEBUG

工程化 / 交付
- 新增 CHANGELOG.md 与 .env.production.example;composer.lock 入库锁定依赖版本
- 插件离线安装 CLI 增加 --uid,后台上传安装透传 uid,付费插件授权校验可通过
- 授权拒绝文案区分「未携带 uid」与「未找到有效授权」,便于排障
- .gitignore 放行 .env.production.example / .env.addon.example / .gitattributes / public/.htaccess

结构
- appmall 由 addon/appmall 迁移为独立应用 app/appmall(市场 / 开发者中心 / 审核 / 分润)
This commit is contained in:
yangwx committed 2026-10-22 00:53:39 +08:00
1 parent 1d49e6f5ee
commit 18101cf11a
636 files changed
+58517 -9519

No files matched your search

@@ -0,0 +1,195 @@
<?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\appmall\controller\backend;
use think\facade\Db;
use think\facade\View;
use Exception;
use app\appmall\controller\AppmallBackend;
// 别名导入:本控制器类名也叫 AddonReview,直接 use 会与自身声明冲突
use app\appmall\model\AddonReview as AddonReviewModel;
/**
* 插件审核(运营后台,中心站)
*
* 审核开发者通过 Market::submit 提交的插件上架申请(appmall_addon_submissions)。
* status: 0=待审核 1=已发布 2=已驳回。
*/
class AddonReview extends AppmallBackend
{
/**
* 审核列表 / 页面
*/
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());
}
}
// ==================== 用户评价审核(appmall_addon_reviews) ====================
/**
* 评价审核列表 / 页面
*
* 与上架审核(submissions)分开:这里是用户安装使用后提交的评分与评论,先审后发。
*/
public function comments()
{
if ($this->request->isAjax()) {
$status = $this->request->param('status', '');
$keyword = trim((string) $this->request->param('keyword', ''));
$query = Db::name('appmall_addon_reviews')->alias('r')
->leftJoin('appmall_addon_list l', 'r.aid = l.id')
->field('r.*, l.title AS addon_title, l.name AS addon_name')
->order('r.id', 'desc');
if ($status !== '') {
$query->where('r.status', (int) $status);
}
if ($keyword !== '') {
$query->whereLike('l.name|l.title|r.content', '%' . $keyword . '%');
}
$list = $query->paginate([
'page' => (int) $this->request->param('page', 1),
'list_rows' => (int) $this->request->param('limit', 10),
]);
$items = $list->items();
foreach ($items as &$it) {
$it['create_fmt'] = date('Y-m-d H:i', (int) $it['create_at']);
$it['content'] = (string) $it['content'];
}
$this->result->setCount($list->total())->success($items, '获取成功');
}
// 该模块暂无后台 HTML 视图(应用市场的运营页面已下线),这里作为数据源接口存在:
// 后续接 UI 时补 addonreview/comments 视图即可,不再引入半成品页面。
return json([
'code' => 1,
'msg' => '请以 Ajax(X-Requested-With)请求该接口获取评价列表',
'data' => [],
]);
}
/**
* 评价通过:置为已发布并重算插件评分
*/
public function approveComment()
{
$id = (int) input('id');
try {
$row = Db::name('appmall_addon_reviews')->where('id', $id)->find();
if (empty($row)) {
return $this->result->error('评价不存在');
}
Db::name('appmall_addon_reviews')->where('id', $id)
->update(['status' => AddonReviewModel::STATUS_PUBLISHED, 'update_at' => time()]);
AddonReviewModel::syncRating((int) $row['aid']);
return $this->result->success([], '已通过');
} catch (Exception $e) {
return $this->result->error($e->getMessage());
}
}
/**
* 评价驳回:置为已驳回并重算评分(该条不再计入统计)
*/
public function rejectComment()
{
$id = (int) input('id');
try {
$row = Db::name('appmall_addon_reviews')->where('id', $id)->find();
if (empty($row)) {
return $this->result->error('评价不存在');
}
Db::name('appmall_addon_reviews')->where('id', $id)
->update(['status' => AddonReviewModel::STATUS_REJECTED, 'update_at' => time()]);
AddonReviewModel::syncRating((int) $row['aid']);
return $this->result->success([], '已驳回');
} catch (Exception $e) {
return $this->result->error($e->getMessage());
}
}
/**
* 删除评价(恶意/重复/用户撤回),删除后重算评分
*/
public function deleteComment()
{
$id = (int) input('id');
try {
$row = Db::name('appmall_addon_reviews')->where('id', $id)->find();
if (empty($row)) {
return $this->result->error('评价不存在');
}
Db::name('appmall_addon_reviews')->where('id', $id)->delete();
AddonReviewModel::syncRating((int) $row['aid']);
return $this->result->success([], '已删除');
} catch (Exception $e) {
return $this->result->error($e->getMessage());
}
}
}