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

+264
View File
@@ -0,0 +1,264 @@
-- ============================================================
-- 插件应用市场(服务端)建表脚本
-- 由框架插件安装流程(AddonService::executeInstall)自动导入。
-- 部署「插件服务器(中心站)」并启用 market 插件后即生效。
-- 接口契约见 app/appmall/controller/api/Market.php
-- ============================================================
CREATE TABLE IF NOT EXISTS `__PREFIX__appmall_addon_list` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(50) NOT NULL COMMENT '插件标识符,如 iot',
`type` varchar(20) NOT NULL DEFAULT 'addon' COMMENT '商品类型:addon=插件 template=模板',
`developer_id` int unsigned DEFAULT '0' COMMENT '发布者(wxapp_appmall_developers.id),用于收益归属',
`title` varchar(100) NOT NULL COMMENT '插件名称',
`version` varchar(20) NOT NULL COMMENT '版本号',
`price` decimal(10,2) NOT NULL DEFAULT 0 COMMENT '价格,0 为免费',
`author` varchar(100) DEFAULT '' COMMENT '作者',
`description` text COMMENT '简介/描述',
`changelog` text COMMENT '更新日志',
`require_framework` varchar(20) DEFAULT '' COMMENT '最低兼容框架版本,如 1.2.0',
`logo` varchar(255) DEFAULT '' COMMENT '封面图 URL',
`category` varchar(50) DEFAULT '' COMMENT '分类(如:工具/电商/社交)',
`tags` varchar(255) DEFAULT '' COMMENT '标签(逗号分隔)',
`screenshots` text COMMENT '截图 URL(逗号分隔或 JSON 数组)',
`rating` decimal(2,1) DEFAULT '0.0' COMMENT '评分(0.0~5.0)',
`file_path` varchar(255) NOT NULL COMMENT 'zip 物理路径(非 Web 可访问目录)',
`file_hash` varchar(64) NOT NULL COMMENT 'SHA256 校验值',
`license_period` int unsigned NOT NULL DEFAULT 0 COMMENT '授权有效天数:0=买断永久;>0=订阅制(30 月付 / 365 年付)',
`trial_days` int unsigned NOT NULL DEFAULT 0 COMMENT '免费试用天数:0=不提供试用',
`upgrade_policy` tinyint NOT NULL DEFAULT 0 COMMENT '大版本升级策略:0=买断式(已购覆盖全部大版本) 1=跨大版本按原价重购 2=跨大版本按 upgrade_price 优惠价',
`upgrade_price` decimal(10,2) NOT NULL DEFAULT 0 COMMENT '跨大版本升级优惠价(upgrade_policy=2 时生效)',
`seats` int unsigned NOT NULL DEFAULT 1 COMMENT '单份授权默认可绑定站点数(多站点套餐)',
`status` tinyint DEFAULT 1 COMMENT '1=上架 0=下架',
`download_count` int DEFAULT 0 COMMENT '下载次数',
`create_at` int DEFAULT 0 COMMENT '创建时间',
`update_at` int DEFAULT 0 COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_name_version` (`name`, `version`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='插件市场商品表';
CREATE TABLE IF NOT EXISTS `__PREFIX__appmall_addon_submissions` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`developer_id` int unsigned DEFAULT '0' COMMENT '提交者(wxapp_appmall_developers.id)',
`name` varchar(50) NOT NULL COMMENT '插件标识符',
`type` varchar(20) NOT NULL DEFAULT 'addon' COMMENT '商品类型:addon=插件 template=模板',
`title` varchar(100) NOT NULL COMMENT '插件名称',
`author` varchar(100) DEFAULT '' COMMENT '作者',
`description` text COMMENT '简介/描述',
`changelog` text COMMENT '更新日志',
`require_framework` varchar(20) DEFAULT '' COMMENT '最低兼容框架版本,如 1.2.0',
`version` varchar(20) NOT NULL COMMENT '版本号',
`price` decimal(10,2) NOT NULL DEFAULT 0 COMMENT '价格',
`category` varchar(50) DEFAULT '' COMMENT '分类',
`tags` varchar(255) DEFAULT '' COMMENT '标签(逗号分隔)',
`logo` varchar(255) DEFAULT '' COMMENT '封面图',
`file_path` varchar(255) NOT NULL COMMENT '待审 zip 物理路径',
`file_hash` varchar(64) NOT NULL COMMENT 'SHA256',
`status` tinyint DEFAULT 0 COMMENT '0=待审核 1=已通过 2=已驳回',
`reject_reason` varchar(255) DEFAULT '' COMMENT '驳回原因',
`create_at` int DEFAULT 0,
`update_at` int DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_name_version` (`name`, `version`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='插件市场提交审核表';
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='市场开发者表';
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 'wxapp_appmall_addon_orders.id',
`aid` int unsigned NOT NULL DEFAULT '0' COMMENT 'wxapp_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='插件销售收益账本';
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='开发者提现单';
-- ============================================================
-- 以下三张表是「购买/授权闭环」的运行表(订单 / 授权 / 下载日志)。
-- 旧版 install.sql 漏建,导致新装中心站一调 buy/notify/下载就报「表不存在」。
-- 与 docs/appmall_schema.sql、客户端 app/api/controller/v1/Addon.php::ensureTables() 字段保持一致。
-- ============================================================
CREATE TABLE IF NOT EXISTS `__PREFIX__appmall_addon_licenses` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`uid` int unsigned NOT NULL COMMENT '用户ID',
`aid` int unsigned NOT NULL COMMENT '购买时对应的 appmall_addon_list.id(版本行)',
`addon_name` varchar(50) NOT NULL DEFAULT '' COMMENT '插件标识:授权跨版本生效的判定主键(同名插件所有版本共享一份授权)',
`license_key` varchar(64) NOT NULL COMMENT '授权码',
`site_id` int unsigned NOT NULL DEFAULT '0' COMMENT '绑定站点(wxapp_appmall_sites.id),0=未绑定',
`domain` varchar(255) DEFAULT NULL COMMENT '绑定域名(站点授权,对齐 Discuz!),多个用逗号分隔',
`status` tinyint NOT NULL DEFAULT '1' COMMENT '1=有效 0=已吊销(退款)',
`expire_time` int unsigned DEFAULT '0' COMMENT '过期时间:0 为永久(买断或订阅到期前)',
`max_major` smallint unsigned NOT NULL DEFAULT 0 COMMENT '已购覆盖的大版本上限:0=不限(买断式);N=可用到 N.x(跨大版本需付费升级)',
`seats` int unsigned NOT NULL DEFAULT 1 COMMENT '可绑定站点数(多站点套餐)',
`trial_end` int unsigned NOT NULL DEFAULT 0 COMMENT '试用到期时间:0=非试用或已转正',
`period` int unsigned NOT NULL DEFAULT 0 COMMENT '订阅周期天数快照(0=买断),续费时据此延长',
`download_count` int DEFAULT '0' COMMENT '下载次数限制',
`create_at` int DEFAULT 0 COMMENT '创建时间',
`update_at` int DEFAULT 0 COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `license_key` (`license_key`),
KEY `uk_uid_aid` (`uid`, `aid`),
KEY `idx_uid_name` (`uid`, `addon_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='插件授权表';
CREATE TABLE IF NOT EXISTS `__PREFIX__appmall_addon_orders` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`uid` int unsigned NOT NULL DEFAULT '0',
`aid` int unsigned NOT NULL DEFAULT '0' COMMENT 'appmall_addon_list.id',
`site_id` int unsigned NOT NULL DEFAULT '0' COMMENT '下单站点',
`amount` decimal(10,2) NOT NULL DEFAULT '0' COMMENT '订单金额',
`status` tinyint NOT NULL DEFAULT '0' COMMENT '0=待支付 1=已支付 2=已退款',
`type` tinyint NOT NULL DEFAULT '0' COMMENT '订单类型:0=首次购买 1=订阅续费 2=跨大版本升级 3=增加站点座席',
`major` smallint unsigned NOT NULL DEFAULT 0 COMMENT '本次购买涉及的大版本(type=2 时为目标大版本)',
`trade_no` varchar(64) NOT NULL DEFAULT '' COMMENT '商户订单号',
`pay_time` int unsigned DEFAULT '0' COMMENT '支付时间',
`create_at` int unsigned DEFAULT 0,
`update_at` int unsigned DEFAULT 0,
PRIMARY KEY (`id`),
KEY `idx_uid` (`uid`),
KEY `idx_trade` (`trade_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='插件购买订单表';
CREATE TABLE IF NOT EXISTS `__PREFIX__appmall_addon_download_logs` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`license_id` int unsigned NOT NULL DEFAULT '0',
`uid` int unsigned NOT NULL DEFAULT '0',
`aid` int unsigned NOT NULL DEFAULT '0',
`operator_id` int unsigned NOT NULL DEFAULT '0' COMMENT '运营后台下载审计(Addons::downloadInstall 透传,0=会员自助下载)',
`ip` varchar(45) DEFAULT '',
`create_at` int unsigned DEFAULT 0,
PRIMARY KEY (`id`),
KEY `idx_license` (`license_id`),
KEY `idx_operator` (`operator_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='插件下载日志表';
-- 市场客户端站点(授权绑定的维度,对齐 Discuz!「授权绑定站点域名」)
CREATE TABLE IF NOT EXISTS `__PREFIX__appmall_sites` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`site_token` varchar(64) NOT NULL DEFAULT '' COMMENT '站点令牌(客户端 install 时生成/上报)',
`domain` varchar(255) NOT NULL DEFAULT '' COMMENT '主域名',
`owner_uid` int unsigned NOT NULL DEFAULT '0' COMMENT '站点所有者用户ID',
`status` tinyint NOT NULL DEFAULT '1' COMMENT '1=正常 0=禁用',
`create_at` int DEFAULT 0,
`update_at` int DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_domain` (`domain`),
KEY `idx_token` (`site_token`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='市场客户端站点(授权绑定维度)';
-- ============================================================
-- 主框架升级(原 upgrade 插件,已合并进 market)
-- 客户端 ywxapp\service\FrameworkService 经 /api/framework/* 拉取版本与核心包。
-- ============================================================
CREATE TABLE IF NOT EXISTS `__PREFIX__appmall_framework_list` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`version` varchar(20) NOT NULL COMMENT '框架版本号',
`title` varchar(100) DEFAULT '' COMMENT '版本标题',
`changelog` text COMMENT '更新日志',
`file_path` varchar(255) DEFAULT '' COMMENT '整包 zip 物理路径(非 Web 可访问目录)',
`file_hash` varchar(64) DEFAULT '' COMMENT '整包 SHA256 校验值',
`patch_path` varchar(255) DEFAULT '' COMMENT '增量补丁 zip 物理路径(与整包同版本共存)',
`patch_hash` varchar(64) DEFAULT '' COMMENT '补丁 SHA256 校验值',
`patch_from` varchar(20) DEFAULT '' COMMENT '补丁适用的基础版本',
`type` tinyint DEFAULT 0 COMMENT '兼容旧客户端:0=含整包 1=仅补丁(逻辑以 patch_path 是否为空为准)',
`from_version` varchar(20) DEFAULT '' COMMENT '兼容旧客户端:补丁基础版本镜像',
`install_path` varchar(255) DEFAULT '' COMMENT '完整安装包 zip 物理路径(全新部署用,含占位符 .env,非 Web 可访问目录)',
`install_hash` varchar(64) DEFAULT '' COMMENT '完整安装包 SHA256 校验值',
`status` tinyint DEFAULT 0 COMMENT '0=草稿 1=已发布(可下载)',
`download_count` int DEFAULT 0 COMMENT '下载次数',
`create_at` int DEFAULT 0 COMMENT '创建时间',
`update_at` int DEFAULT 0 COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_version` (`version`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='主框架版本表';
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='主框架下载日志表';
-- 接口频率限制(buy/notify/download/submit 等防刷)
CREATE TABLE IF NOT EXISTS `__PREFIX__appmall_throttle` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`action` varchar(30) NOT NULL COMMENT '限流动作(buy/notify/download/submit)',
`ident` varchar(80) NOT NULL COMMENT '限流标识(uid 或 IP)',
`count` int unsigned NOT NULL DEFAULT 0 COMMENT '窗口内已请求次数',
`expire_at` int unsigned NOT NULL DEFAULT 0 COMMENT '窗口过期时间戳',
`create_at` int unsigned DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_action_ident` (`action`, `ident`),
KEY `idx_expire` (`expire_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='接口频率限制表';
CREATE TABLE IF NOT EXISTS `__PREFIX__appmall_addon_review_log` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`submission_id` int unsigned NOT NULL DEFAULT 0 COMMENT 'appmall_addon_submissions.id',
`operator_id` int unsigned NOT NULL DEFAULT 0 COMMENT '操作管理员ID',
`action` varchar(20) NOT NULL COMMENT '操作:submit/approve/reject',
`from_status` tinyint NOT NULL DEFAULT 0 COMMENT '变更前状态',
`to_status` tinyint NOT NULL DEFAULT 0 COMMENT '变更后状态',
`reason` varchar(255) NOT NULL DEFAULT '' COMMENT '审核意见/驳回理由',
`create_at` int unsigned NOT NULL DEFAULT 0 COMMENT '操作时间',
PRIMARY KEY (`id`),
KEY `idx_submission` (`submission_id`),
KEY `idx_create` (`create_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='插件审核历史日志(可视化时间线数据源)';
CREATE TABLE IF NOT EXISTS `__PREFIX__appmall_addon_reviews` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`aid` int unsigned NOT NULL DEFAULT 0 COMMENT 'appmall_addon_list.id',
`site_id` int unsigned NOT NULL DEFAULT 0 COMMENT '来源站点 ID(多租户隔离)',
`uid` int unsigned NOT NULL DEFAULT 0 COMMENT '评价用户 ID(0=匿名,按 IP 去重)',
`rating` tinyint unsigned NOT NULL DEFAULT 5 COMMENT '评分 1~5',
`content` varchar(1000) NOT NULL DEFAULT '' COMMENT '评价内容',
`version` varchar(20) NOT NULL DEFAULT '' COMMENT '评价时所装的插件版本',
`status` tinyint NOT NULL DEFAULT 0 COMMENT '状态:0=待审核 1=已发布 2=已驳回',
`ip` varchar(45) NOT NULL DEFAULT '' COMMENT '来源 IP(风控与匿名去重)',
`create_at` int unsigned NOT NULL DEFAULT 0,
`update_at` int unsigned NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
KEY `idx_addon_status` (`aid`,`status`),
KEY `idx_user` (`site_id`,`uid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='插件用户评价(评分+评论,审核后公开展示)';