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());
}
}
}
@@ -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 app\appmall\controller\backend;
use think\facade\Db;
use think\facade\View;
use Exception;
use app\appmall\controller\AppmallBackend;
use ywxapp\library\Mailer;
/**
* 开发者管理(运营后台,含审核)
* 由原核心 app/backend/controller/Developer.php 剥离到 appmall 应用。
*/
class Developer extends AppmallBackend
{
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,285 @@
<?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;
use ywxapp\library\StreamZipResponse;
/**
* 主框架版本管理(运营后台,中心站)
*
* 上传核心包 zip → 入库(草稿)→ 发布(status=1)→ 客户端可见可下载。
* 原 upgrade 插件功能,已合并进 appmall(应用服务中心)。
*/
class Framework extends AppmallBackend
{
/**
* 版本列表 / 页面
*/
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 {
// 列兜底由 FrameworkList 模型负责(事实源 app/appmall/install.sql),此处只保留历史数据迁移
\app\appmall\model\FrameworkList::ensureSchema();
// 历史数据迁移:旧「补丁行」(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
}
}
}
+136
View File
@@ -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 app\appmall\controller\backend;
use think\facade\Db;
use think\facade\View;
use Exception;
use app\appmall\controller\AppmallBackend;
/**
* 收益与结算管理(运营后台,中心站)
*
* - 收益账本:每笔已支付订单按平台抽成记账,归属对应开发者;
* - 提现单:开发者发起(或运营代发)后在此审核打款/驳回。
*/
class Revenue extends AppmallBackend
{
/**
* 收益账本列表
*/
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());
}
}
}