首先,禁止网站下所有 .php 等文件均不允许被访问到。
在 nginx 网站配置文件中,include enable-php-**.conf; 上方插入:
location ~ ^/.*\.(php|php5|py|sh|bash|out)$ { deny all; }
其中,^ 匹配开始,/.* 匹配所有目录和文件名,\.(php|php5|py|sh|bash|out) 匹配文件后缀名,$ 匹配结束。
即便如此,仍然忽略了 nginx 中 .php 文件名后加斜杠仍然能访问到的情况,譬如我们访问这个网址:
https://xoyozo.net/phpinfo.php/abc.html
nginx 仍然运行了 phpinfo.php,给了后门可趁之机,所以改进为:
location ~ ^/.*\.(php|php5|py|sh|bash|out)(/.*)?$ { deny all; }
Discuz! X3.4 需要写入权限的目录:
/自研目录/upload/
/config/
/data/
/uc_client/data/
/uc_server/data/
/source/plugin/
但,这些都不重要,我们已经禁止了所有目录的 .php 访问了。
第二步,解禁需要直接被访问到的文件路径。
Discuz! X3.4 根目录下的 .php 文件都是入口文件,需要能够被访问到:
/admin.php
/api.php
/connect.php
/forum.php
/group.php
/home.php
/index.php
/member.php
/misc.php
/plugin.php
/portal.php
/search.php
其它目录中需要被直接访问到的文件:
/archiver/index.php
/m/index.php
/uc_server/admin.php
/uc_server/avatar.php
/uc_server/index.php
部分插件文件需要能够被直接访问到:
/source/plugin/magmobileapi/magmobileapi.php
/source/plugin/smstong/accountinfo.php
/source/plugin/smstong/checkenv.php
另外如果有自建目录需要有 .php 访问权限,那么也需要在此处加白,本文以 /_/ 目录为例
最终拼成:工具
location ~ ^(?:(?!(/admin\.php|/api\.php|/connect\.php|/forum\.php|/group\.php|/home\.php|/index\.php|/member\.php|/misc\.php|/plugin\.php|/portal\.php|/search\.php|/archiver/index\.php|/m/index\.php|/uc_server/admin\.php|/uc_server/avatar\.php|/uc_server/index\.php|/source/plugin/magmobileapi/magmobileapi\.php|/source/plugin/smstong/accountinfo\.php|/source/plugin/smstong/checkenv\.php|/_/.*\.php))/.*\.(php|php5|py|sh|bash|out)(/.*)?)$ { deny all; }
这里用到正则表达式中的“不捕获”和“负向零宽断言”语法,格式为:
^(?:(?!(允许的目录或文件A|允许的目录或文件B))禁止的目录和文件)$
值得注意的是,此句负向零宽断言中的匹配内容是匹配前缀的,也就是说
https://xoyozo.net/index.php
https://xoyozo.net/index.php/abc.html
都可以访问到,而
https://xoyozo.net/forbidden.php
https://xoyozo.net/forbidden.php/abc.html
都访问不到,这是符合需求的。
如果自建目录 /_/ 下有写入需求,单独禁止即可,以 /_/upload/ 为例:
location ~ ^/_/upload/.*\.(php|php5|py|sh|bash|out)(/.*)?$ { deny all; }
ThinkPHP 网站有统一的访问入口,可按本文方法配置访问权限。
特别注意:上面的代码必须加在 PHP 引用配置(include enable-php-**.conf;)的上方才有效。
本文使用 ASP.NET 6 版本 Senparc.Weixin.Sample.MP 示例项目改造。
第一步 注册多公众号
方法一:打开 appsettings.json 文件,在 SenparcWeixinSetting 节点内添加数组节点 Items,该对象类型同 SenparcWeixinSetting。
//Senparc.Weixin SDK 设置
"SenparcWeixinSetting": {
"IsDebug": true,
"Token": "",
"EncodingAESKey": "",
"WeixinAppId": "",
"WeixinAppSecret": "",
"Items": [
{
"IsDebug": true,
"Token": "a",
"EncodingAESKey": "a",
"WeixinAppId": "a",
"WeixinAppSecret": "a"
},
{
"IsDebug": true,
"Token": "b",
"EncodingAESKey": "b",
"WeixinAppId": "b",
"WeixinAppSecret": "b"
}
]
}
方法二:修改 Program.cs 文件,在 UseSenparcWeixin 方法中注册多个公众号信息。
var registerService = app.UseSenparcWeixin(app.Environment,
null /* 不为 null 则覆盖 appsettings 中的 SenparcSetting 配置*/,
null /* 不为 null 则覆盖 appsettings 中的 SenparcWeixinSetting 配置*/,
register => { /* CO2NET 全局配置 */ },
(register, weixinSetting) =>
{
//注册公众号信息(可以执行多次,注册多个公众号)
//register.RegisterMpAccount(weixinSetting, "【盛派网络小助手】公众号");
foreach (var mp in 从数据库或配置文件中获取的公众号列表)
{
register.RegisterMpAccount(new SenparcWeixinSetting
{
//IsDebug = true,
WeixinAppId = mp.AppId,
WeixinAppSecret = mp.AppSecret,
Token = mp.Token,
EncodingAESKey = mp.EncodingAeskey,
}, mp.Name);
}
});
完成后,我们可以从 Config.SenparcWeixinSetting.Items 获取这些信息。
第二步 接入验证与消息处理
打开 WeixinController 控制器,将构造函数改写为:
public WeixinController(IHttpContextAccessor httpContextAccessor)
{
AppId = httpContextAccessor.HttpContext!.Request.Query["appId"];
var MpSetting = Services.MPService.MpSettingByAppId(AppId);
Token = MpSetting.Token;
EncodingAESKey = MpSetting.EncodingAESKey;
}
示例中 Services.MPService.MpSettingByAppId() 方法实现从 Config.SenparcWeixinSetting.Items 返回指定 appId 的公众号信息。
为使 IHttpContextAccessor 注入生效,打开 Program.cs,在行
builder.Services.AddControllersWithViews();
下方插入
builder.Services.AddHttpContextAccessor();
这样,我们就可以在构造函数中直接获取地址栏中的 appId 参数,找到对应的公众号进行消息处理。
第三步 消息自动回复中的 AppId
打开 CustomMessageHandler.cs,将
private string appId = Config.SenparcWeixinSetting.MpSetting.WeixinAppId;
private string appSecret = Config.SenparcWeixinSetting.MpSetting.WeixinAppSecret;
替换为:
private string appId = null!;
private string appSecret = null!;
并在构造函数中插入
appId = postModel.AppId;
appSecret = Services.MPService.MpSettingByAppId(postModel.AppId).WeixinAppSecret;
这样,本页中使用的 appId / appSecret 会从 postModel 中获取,而非默认公众号。postModel 已在 WeixinController 中赋值当前的 appId。
改造后,我们可以在 OnTextRequestAsync() 等处理消息的方法中可以判断 appId 来处理不同的消息。
第四步 其它功能和接口
其它功能和接口均可用指定的 AppId 和对应的 AppSecret 进行调用。
论坛出现动态页面打开或刷新非常慢(超过半分钟),甚至打不开,该页无法显示,502 Bad Gateway 等情况,TTFB 计时需要好几秒。
切换 nginx 版本、php 版本 等均无效果,甚至购买新的 ECS 重新搭建环境也是效果甚微。
最终在阿里云 RDS 管理中,手动主备库切换,问题得以解决。
因此大概率是原主库有问题(有损坏的表需要修复或锁等情况)。
查询故障时间段的慢 SQL,发现某表的锁等待时间特别长,不知道是不是这个表的原因导致的。
次日,提交工单得到回复是原主库(现备库)会自动修复,经再次主备库切换测试得到了肯定的结果。
再次日,又遇到同样的打开慢的情况,这次直接找到慢 SQL 中锁等待时间较长的表,优化(OPTIMIZE TABLE)(虽然是 InnoDB),立刻解决了问题。
按材料
铅酸电池 | 成本低、低温性好、性价比高;能量密度低、寿命短、体积大、安全性差。 |
镍氢电池 | 成本低、技术成熟、寿命长、耐用;能量密度低、体积大、电压低、有电池记忆效应。含有重金属,遗弃后对环境造成污染。 |
锰酸锂电池 | 成本低、安全性和低温性能好的正极材料,但是其材料本身并不太稳定,容易分解产生气体,因此多用于和其它材料混合使用,以降低电芯成本,但其循环寿命衰减较快,容易发生鼓胀,高温性能较差、寿命相对短。 |
磷酸铁锂电池 | 是一种使用磷酸铁锂(LiFePO4)作为正极材料,碳作为负极材料的锂离子电池。具有工作电压高、能量密度大、循环寿命长、安全性能好、自放电率小、无记忆效应的优点。电池温度处于500-600℃时,其内部化学成分才开始分解,并且穿刺、短路、高温都不会燃烧或者爆炸,使用寿命也较长。但车辆续航里程一般,当温度低于-5℃时,充电效率低,不适合北方在冬天充电的需求。 |
三元锂电池 | 是指正极材料使用镍钴锰酸锂(Li(NiCoMn)O2)或者镍钴铝酸锂的三元正极材料的锂电池。能量密度高、循环寿命长、不惧低温;高温下稳定不足。能量密度可达最高,但高温性相对较差,关于续航里程有要求的纯电动汽车,其是主流方向,且适合北方天气,低温时电池更加稳定。 |
按制造和封装形式
1865电池 | 18650是锂离子电池的鼻祖。日本SONY公司当年为了节省成本而定下的一种标准性的锂离子电池型号,其中18表示直径为18mm,65表示长度为65mm,0表示为圆柱形电池。优点:容量大、寿命长、安全性能高、电压高、没有记忆效应、内阻小、可串联或并联组合成18650锂电池组、使用范围广。 |
2170电池 | 2017年1月4日,特斯拉官方宣布新一代锂电池“2170”开始量产。直径为21mm、高度70mm。 |
4680电池 | 2020年9月特斯拉发布了的新型电池类型,据介绍,相较2170电池,该电池能量方面提高5倍,续航里程提高16%,动力方面提高6倍。2022年初开始量产。 |
刀片电池 | 是比亚迪于2020年3月29日发布的电池产品。该电池采用磷酸铁锂技术。“刀片电池”通过结构创新,在成组时可以跳过“模组”,大幅提高了体积利用率,最终达成在同样的空间内装入更多电芯的设计目标。相较传统电池包,“刀片电池”的体积利用率提升了50%以上,也就是说续航里程可提升50%以上,达到了高能量密度三元锂电池的同等水平。 |
麒麟电池 | 2022年3月26日,宁德时代首席科学家吴凯在2022年电动汽车百人会论坛上表示:宁德时代通过不断技术迭代,推出了第三代CTP技术,其系统重量、能量密度及体积能量密度继续引领行业最高水平。在相同的化学体系、同等电池包尺寸下,麒麟电池包的电量,相比4680系统可以提升13%。第三代CTP技术,兼顾极速、无损、安全、高效等优势,适用磷酸铁锂、三元电池,涵盖乘用车、商用车领域。 |
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace xxx.xxx.xxx.Controllers
{
public class DiscuzTinyintViewerController : Controller
{
public IActionResult Index()
{
using var context = new Data.xxx.xxxContext();
var conn = context.Database.GetDbConnection();
conn.Open();
using var cmd = conn.CreateCommand();
cmd.CommandText = "SELECT `TABLE_NAME` FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE();";
Dictionary<string, List<FieldType>?> tables = new();
using var r = cmd.ExecuteReader();
while (r.Read())
{
tables.Add((string)r["TABLE_NAME"], null);
}
conn.Close();
foreach (var table in tables)
{
var conn2 = context.Database.GetDbConnection();
conn2.Open();
using var cmd2 = conn2.CreateCommand();
cmd2.CommandText = "DESCRIBE " + table.Key;
using var r2 = cmd2.ExecuteReader();
List<FieldType> fields = new();
while (r2.Read())
{
if (((string)r2[1]).Contains("tinyint(1)"))
{
fields.Add(new()
{
Field = (string)r2[0],
Type = (string)r2[1],
Null = (string)r2[2],
});
}
}
conn2.Close();
tables[table.Key] = fields;
}
foreach (var table in tables)
{
foreach (var f in table.Value)
{
var conn3 = context.Database.GetDbConnection();
conn3.Open();
using var cmd3 = conn3.CreateCommand();
cmd3.CommandText = $"SELECT {f.Field} as F, COUNT({f.Field}) as C FROM {table.Key} GROUP BY {f.Field}";
using var r3 = cmd3.ExecuteReader();
List<FieldType.ValueCount> vs = new();
while (r3.Read())
{
vs.Add(new() { Value = Convert.ToString(r3["F"]), Count = Convert.ToInt32(r3["C"]) });
}
conn3.Close();
f.groupedValuesCount = vs;
}
}
return Json(tables.Where(c => c.Value != null && c.Value.Count > 0));
}
private class FieldType
{
public string Field { get; set; }
public string Type { get; set; }
public string Null { get; set; }
public List<ValueCount> groupedValuesCount { get; set; }
public class ValueCount
{
public string Value { get; set; }
public int Count { get; set; }
}
public string RecommendedType
{
get
{
if (groupedValuesCount == null || groupedValuesCount.Count < 2)
{
return "无建议";
}
else if (groupedValuesCount.Count == 2 && groupedValuesCount.Any(c => c.Value == "0") && groupedValuesCount.Any(c => c.Value == "1"))
{
return "bool" + (Null == "YES" ? "?" : "");
}
else
{
return "sbyte" + (Null == "YES" ? "?" : "");
}
}
}
}
}
}
[{
"key": "pre_forum_post",
"value": [{
"field": "first",
"type": "tinyint(1)",
"null": "NO",
"groupedValuesCount": [{
"value": "0",
"count": 1395501
}, {
"value": "1",
"count": 179216
}],
"recommendedType": "bool"
}, {
"field": "invisible",
"type": "tinyint(1)",
"null": "NO",
"groupedValuesCount": [{
"value": "-5",
"count": 9457
}, {
"value": "-3",
"count": 1412
}, {
"value": "-2",
"count": 1122
}, {
"value": "-1",
"count": 402415
}, {
"value": "0",
"count": 1160308
}, {
"value": "1",
"count": 3
}],
"recommendedType": "sbyte"
}, {
"field": "anonymous",
"type": "tinyint(1)",
"null": "NO",
"groupedValuesCount": [{
"value": "0",
"count": 1574690
}, {
"value": "1",
"count": 27
}],
"recommendedType": "bool"
}, {
"field": "usesig",
"type": "tinyint(1)",
"null": "NO",
"groupedValuesCount": [{
"value": "0",
"count": 162487
}, {
"value": "1",
"count": 1412230
}],
"recommendedType": "bool"
}, {
"field": "htmlon",
"type": "tinyint(1)",
"null": "NO",
"groupedValuesCount": [{
"value": "0",
"count": 1574622
}, {
"value": "1",
"count": 95
}],
"recommendedType": "bool"
}, {
"field": "bbcodeoff",
"type": "tinyint(1)",
"null": "NO",
"groupedValuesCount": [{
"value": "-1",
"count": 935448
}, {
"value": "0",
"count": 639229
}, {
"value": "1",
"count": 40
}],
"recommendedType": "sbyte"
}, {
"field": "smileyoff",
"type": "tinyint(1)",
"null": "NO",
"groupedValuesCount": [{
"value": "-1",
"count": 1359482
}, {
"value": "0",
"count": 215186
}, {
"value": "1",
"count": 49
}],
"recommendedType": "sbyte"
}, {
"field": "parseurloff",
"type": "tinyint(1)",
"null": "NO",
"groupedValuesCount": [{
"value": "0",
"count": 1572844
}, {
"value": "1",
"count": 1873
}],
"recommendedType": "bool"
}, {
"field": "attachment",
"type": "tinyint(1)",
"null": "NO",
"groupedValuesCount": [{
"value": "0",
"count": 1535635
}, {
"value": "1",
"count": 2485
}, {
"value": "2",
"count": 36597
}],
"recommendedType": "sbyte"
}, {
"field": "comment",
"type": "tinyint(1)",
"null": "NO",
"groupedValuesCount": [{
"value": "0",
"count": 1569146
}, {
"value": "1",
"count": 5571
}],
"recommendedType": "bool"
}]
}]
Regex.Escape(String) 方法:
通过替换为转义码来转义最小的字符集(\、*、+、?、|、{、[、(、)、^、$、.、# 和空白)。 这将指示正则表达式引擎按原义解释这些字符而不是解释为元字符。
示例:
string str = @"123\c\d\e";
string r1 = @"\d";
string r2 = Regex.Escape(r1);
return Json(new
{
m1 = Regex.Matches(str, r1).Select(c => c.Value),
m2 = Regex.Matches(str, r2).Select(c => c.Value)
});
结果:
{
"m1":[
"1",
"2",
"3"
],
"m2":[
"\\d"
]
}
一般地,我们使用通配符 a*c 在字符串 abcd 中查找:
string s = @"abcd";
string w = @"a*c";
string r = Regex.Escape(w).Replace(@"\*", @".*?").Replace(@"\?", @".?");
return Content(Regex.Match(s, r).Value);
结果:
abc
同理,使用通配符 \d*\f 在字符串 \a\b\c\d\e\f 中查找:
string s = @"\a\b\c\d\e\f";
string w = @"\d*\f";
string r = Regex.Escape(w).Replace(@"\*", @".*?").Replace(@"\?", @".?");
return Content(Regex.Match(s, r).Value);
结果:
\d\e\f
测试表数据量 152 万条,测试跳过 100 万取 10 条。
测试一:直接使用 limit 跳过 1000000 取 10,耗时 2.4s;
SELECT *
FROM `pre_forum_post`
ORDER BY `pid`
LIMIT 1000000, 10
> OK
> 时间: 2.394s
测试二:先使用 limit 跳过 1000000 取 1,再使用配合 where 条件,使用 limit 取 10,耗时 0.2s。
SELECT *
FROM `pre_forum_post`
WHERE `pid` >= (
SELECT `pid`
FROM `pre_forum_post`
ORDER BY `pid`
LIMIT 1000000, 1
)
ORDER BY `pid`
LIMIT 10
> OK
> 时间: 0.207s
两次测试耗时相差约 10 倍。测试取 100 条或取 1 条,耗时都相差约 10 倍。
但若测试跳过条数较小时,测试一效率更高,因此应按照项目的实际需要选择适当的查询方法。
第一步:下载安装 OBS Studio 插件:Advanced Scene Switcher
第二步:在 OBS 中预先添加两个可供直播的场景
第三步:在 OBS 菜单中打开:工具 - 高级场景切换器
切换器的功能十分强大,这里实现一种简单的随机场景切换。
切换到“随机场景列表”选项卡,点左下角“+”按钮添加现有场景,并设置转场特效和保留时间(单位“秒”)
切换到“通用”选项卡,点击“启动”
现在可以在主窗口中看到场景切换效果了。
如果设置特殊的转场特效,在主窗口的“转场特效”中添加后,可以“高级场景切换器”中选用。
前提:
监控摄像头有推流功能
抖音有直播权限(粉丝数超过 1000)
一个已备案的域名
名词解释:
推流地址:由阿里云生成的接收摄像头视频流推送的地址。
播流地址:由阿里云生成可用于终端播放的视频流地址。
各环节功能解释:
摄像头:负责采集视频信息,并推送到阿里云。
阿里云视频直播服务:接收视频推流并分发到用户端(收费)。
OBS 软件:负责将视频流转化为可供直播伴侣调用的视频源。
直播伴侣:将视频源分发到短视频终端用户。
第一步:配置阿里云视频直播服务
1.1 在阿里云控制台中开通视频直播,在域名管理中添加域名。
这里需要添加两个域名,一个是推流域名,一个是播流域名。顾名思义,“推流”指从摄像头将流推送到阿里云,“播流”是用户播放视频流。
以添加域名推流域名“rtmp.xoyozo.net”和播流域名“live.xoyozo.net”为例:
然后按列表中的 CNAME 地址对域名作解析即可。
解析生效后,点击任意一个域名进行配置,将推流域名和播流域名相互绑定。成功后,可以在推流域名中的播流信息中看到播流域名,在播流域名的推流信息中也能看到推流域名。
1.2 生成推流地址/播流地址
在工具箱中点击地址生成器。
选择推流域名和播流域名,AppName 和 StreamName 任意填写,生成成功后获取以下地址:
注:鉴权串是按照鉴权规则生成的串,默认有效期 30 分钟,超出时间即中断推流。可以在域名管理中修改该值,或直接关闭 URL 鉴权。
第二步:在摄像机控制面板中配置推流域名
该步骤因摄像机的品牌和型号不同而有所差异。本文以 IP CAMERA 为例。
首先使用 IP 搜索工具在局域网上搜索摄像机,找到摄像机的 IP 地址。
在浏览器上打开,输入用户名和密码进入管理面板。
在“参数设置”-“网络设置”- RTMP Publish 中,将推流地址填入到“预定网址”中,点击“应用”。
刷新后查看“直播状态”和“直播网址”是否已生效。
若配置成功,可将播流地址在视频播放器中打开观看,或直接在 iPhone 或安卓手机中打开播流地址。
第三步:使用 OBS Studio 将直播流信号转换为虚拟摄像头信号
因抖音直播伴侣无法使用播流地址作为视频源进行直播输出,故使用 OBS 将播流信号转化为虚拟的本机摄像头信号源供直播伴侣调用。
下载安装 OBS Studio,添加一个“场景”:
然后添加一个“媒体源”:
去掉“本地文件”前面的勾,将播流地址填到“输入”框中:
确定后即可预览到直播内容:
拖动视频区域可调整输出范围。
点击“启动虚拟摄像机”。
提示:OBS 创建的虚拟摄像机默认会从物理麦克风和桌面系统音频拾音,请按需在“混音器”中配置。
第四步:OBS 使用 VLC 视频源(此步可略过)
若 OBS 直接添加媒体源不稳定,可以使用 VLC 视频源。
下载安装 VLC 播放器。
在 OBS Studio 中添加来源,选择“VLC 视频源”:
点击“播放列表”右侧的“+”,选择“添加路径 /URL”:
填写播流地址并确定。
第五步:配置抖音直播伴侣使用 OBS 的虚拟摄像机信号源
下载安装抖音直播伴侣,在任意场景中添加素材,选择“摄像头”:
摄像头选择“OBS Virtual Camera”:
点击“开始直播”:
Linux 设置服务开机自动启动的方式有好多种,这里介绍一下通过 chkconfig 命令添加脚本为开机自动启动的方法。
编写脚本 ossftp(这里以开机启动 ossftp 服务为例),脚本内容如下:
#!/bin/sh #chkconfig: 2345 80 90 #description: 开机自动启动的脚本程序 # 开启 ossftp 服务 /root/ossftp-1.2.0-linux-mac/start.sh &
脚本第一行 “#!/bin/sh” 告诉系统使用的 shell;
脚本第二行 “#chkconfig: 2345 80 90” 表示在 2/3/4/5 运行级别启动,启动序号(S80),关闭序号(K90);
脚本第三行 表示的是服务的描述信息;
要执行的文件(示例中的 /root/ossftp-1.2.0-linux-mac/start.sh)必须设置“可执行”权限,命令结尾的“&”可使进程持续。
注意: 第二行和第三行必写,否则会出现如“服务 ossftp 不支持 chkconfig”这样的错误。
将写好的 ossftp 脚本移动到 /etc/rc.d/init.d/ 目录下
给脚本赋可执行权限
chmod +x /etc/rc.d/init.d/ossftp
添加脚本到开机自动启动项目中
chkconfig --add ossftp chkconfig ossftp on
执行命令“chkconfig --list”可列出开机启动的服务及当前的状态。
到这里就设置完成了,我们只需要重启一下我们的服务器,就能看到我们配置的 ossftp 服务已经可以开机自动启动了。