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,145 @@
/**
* 设备类型(dev_type)→ 面板规格
*
* 这是 App 侧「按 dev_type 分发面板」的唯一注册表:device-panel 的模板与逻辑里
* 不再出现任何品类专有的字面量(标题 / 单位 / 小数位 / 阈值文案),全部由
* devSpecOf() 返回的规格驱动。
*
* 为什么是「规格分发」而不是「一个品类一份 uvue」:
* 湿度 / 液位 / 气压 与温度**同构** —— 都是「若干位置(A/B/C)+ 一个数值 + 上下限」。
* 复用同一份上报协议(pos[] / t,键名见 device-panel 的 normalizePositions)之后,
* 服务端**零改动**,还能白拿结构化落库、曲线、云端阈值复核与配置下发;
* 而复制整页只会让 1550 行 × N 份各自漂移,改一处逻辑要改 N 遍。
* 若将来出现「交互形态根本不同」的品类(如开关/继电器:只有布尔状态、没有位置与阈值),
* 再按 dev_type 调到独立页面 —— 钩子在 device-panel 的 onLoad 里(setDevType 之后)。
*
* 加一个新品类 = 加一个 SPEC_xxx 常量 + 在 devSpecOf() 里加一行关键词匹配,无需改动任何页面逻辑。
*
* ⚠️ 协议复用说明:服务端解析的 `t`(位置数值)对非温度品类就是「该位置的读数」,
* `upper` / `lower` 就是上下限,`gap` 是位置内极差 —— 字段名带温度味,语义是通用的。
*/
export interface DevSpec {
key: string // 规格标识(调试/日志用)
label: string // 面板标题,如「温度」
unit: string // 单位后缀,'' = 无单位
decimals: number // 数值小数位
diffOn: boolean // 是否显示「位置内极差」这一路(多探头/多传感器场景才有意义)
diffLabel: string // 极差文案
diffTip: string // 配置弹窗里对极差字段的说明('' = 不显示)
highText: string // 越上限的提示词('超温' / '超上限')
highField: string // 配置弹窗表单标签(带单位)
lowField: string
gapField: string
probeOn: boolean // 是否显示「逐探头明细」与「配置 N 路 / 总线认到 M 颗」比对(OneWire 总线专用)
}
/** 温度 —— 当前在产型号,也是所有缺省行为的基准(改动它等于改动线上表现,慎动) */
export const SPEC_TEMP: DevSpec = {
key: 'temp',
label: '温度',
unit: '℃',
decimals: 2,
diffOn: true,
diffLabel: '温差',
diffTip: '温差阈值:同位置各探头极差 ≥ 该值即上报并告警(由探头侧计算)。',
highText: '超温',
highField: '上限(℃)',
lowField: '下限(℃)',
gapField: '温差阈值',
probeOn: true
}
/** 湿度(%RH 0~100) */
export const SPEC_HUMID: DevSpec = {
key: 'humid',
label: '湿度',
unit: '%RH',
decimals: 1,
diffOn: false,
diffLabel: '湿度差',
diffTip: '',
highText: '超上限',
highField: '上限(%RH)',
lowField: '下限(%RH)',
gapField: '差值阈值',
probeOn: false
}
/** 液位 / 水位(cm,位置即液位点) */
export const SPEC_LEVEL: DevSpec = {
key: 'level',
label: '液位',
unit: 'cm',
decimals: 1,
diffOn: false,
diffLabel: '差值',
diffTip: '',
highText: '超上限',
highField: '上限(cm)',
lowField: '下限(cm)',
gapField: '差值阈值',
probeOn: false
}
/** 气压(kPa) */
export const SPEC_PRESS: DevSpec = {
key: 'press',
label: '气压',
unit: 'kPa',
decimals: 2,
diffOn: false,
diffLabel: '差值',
diffTip: '',
highText: '超上限',
highField: '上限(kPa)',
lowField: '下限(kPa)',
gapField: '差值阈值',
probeOn: false
}
/** 兜底:未注册的品类不臆断单位,先按「多路数值」把数据显示出来 */
export const SPEC_GENERIC: DevSpec = {
key: 'generic',
label: '读数',
unit: '',
decimals: 2,
diffOn: false,
diffLabel: '差值',
diffTip: '',
highText: '超上限',
highField: '上限',
lowField: '下限',
gapField: '差值阈值',
probeOn: false
}
/**
* 按 dev_type 取面板规格
*
* 关键词匹配而非全等匹配:型号后缀(_v2 / _pro)与厂商前缀(espressif_)都不必再登记一次。
* **顺序即优先级**,先匹配者先返回 —— 温湿度一体的型号含 temp,会按温度展示。
*
* @param devType 设备注册时上报的类型(iot_device.dev_type)
*/
export function devSpecOf(devType: string | null): DevSpec {
const raw = devType == null ? '' : devType
const s = raw.trim().toLowerCase()
/* 空类型 = 老设备 / 从未声明 → 温度(当前唯一的基准行为,向后兼容) */
if (s.length === 0) {
return SPEC_TEMP
}
if (s.indexOf('temp') >= 0) {
return SPEC_TEMP
}
if (s.indexOf('hum') >= 0) {
return SPEC_HUMID
}
if (s.indexOf('level') >= 0 || s.indexOf('liqu') >= 0 || s.indexOf('depth') >= 0) {
return SPEC_LEVEL
}
if (s.indexOf('press') >= 0 || s.indexOf('baro') >= 0) {
return SPEC_PRESS
}
return SPEC_GENERIC
}