以下是 Spectre.Console 的核心功能示例,涵盖最常用的输出和交互场景:
1. 基础富文本输出
using Spectre.Console;
// 使用 Markup 语法(类似 BBCode)
AnsiConsole.Markup("[bold green]成功![/] 文件已保存。\n");
AnsiConsole.Markup("[red]错误:[/] 无法连接到服务器。\n");
// 混合样式
AnsiConsole.Markup("[underline blue]https://example.com[/]\n");
// 自动换行写
AnsiConsole.Write(new Panel("[yellow]警告[/] 磁盘空间不足")
.Header("系统通知")
.Border(BoxBorder.Rounded));
2. 表格
var table = new Table();
table.AddColumn("[u]ID[/]");
table.AddColumn(new TableColumn("[u]名称[/]").Centered());
table.AddColumn("[u]状态[/]");
table.AddRow("1", "订单服务", "[green]运行中[/]");
table.AddRow("2", "支付网关", "[red]离线[/]");
table.AddRow("3", "消息队列", "[yellow]警告[/]");
AnsiConsole.Write(table);
3. 进度条 / 状态指示

// 带进度条的循环任务
await AnsiConsole.Progress()
.StartAsync(async ctx =>
{
var task1 = ctx.AddTask("[green]下载文件[/]", maxValue: 100);
var task2 = ctx.AddTask("[green]处理数据[/]", maxValue: 100);
while (!ctx.IsFinished)
{
task1.Increment(1.5);
task2.Increment(0.8);
await Task.Delay(50);
}
});
// 不确定时长的旋转状态
await AnsiConsole.Status()
.StartAsync("正在连接...", async ctx =>
{
await Task.Delay(3000); // 模拟工作
AnsiConsole.MarkupLine("[green]连接成功![/]");
});
4. 交互式提示
// 确认
if (AnsiConsole.Confirm("是否继续安装?"))
{
// 执行安装
}
// 文本输入(带验证)
var name = AnsiConsole.Prompt(
new TextPrompt<string>("请输入用户名:")
.ValidationErrorMessage("[red]用户名不能为空[/]")
.Validate(input => !string.IsNullOrWhiteSpace(input)));
// 选择列表
var fruit = AnsiConsole.Prompt(
new SelectionPrompt<string>()
.Title("请选择最喜欢的水果")
.AddChoices(new[] { "苹果", "香蕉", "橙子", "葡萄" }));
AnsiConsole.MarkupLine($"你选择了:[green]{fruit}[/]");
// 多选
var colors = AnsiConsole.Prompt(
new MultiSelectionPrompt<string>()
.Title("请选择颜色")
.AddChoices(new[] { "红色", "绿色", "蓝色", "黄色" }));
5. 树形结构
var root = new Tree("[yellow]项目结构[/]");
var src = root.AddNode("src");
src.AddNode("Program.cs");
src.AddNode("Services");
src.AddNode("Models");
var tests = root.AddNode("tests");
tests.AddNode("UnitTests");
AnsiConsole.Write(root);
6. 布局 / 网格
var layout = new Layout("Root")
.SplitColumns(
new Layout("Left").Size(30),
new Layout("Right")
.SplitRows(
new Layout("Top"),
new Layout("Bottom")));
layout["Left"].Update(new Panel("导航栏"));
layout["Top"].Update(new Panel("主内容区"));
layout["Bottom"].Update(new Panel("日志输出"));
AnsiConsole.Write(layout);
7. 实时更新
var table = new Table().AddColumn("Time").AddColumn("Message");
table.AddRow("10:00", "系统启动");
await AnsiConsole.Live(table)
.StartAsync(async ctx =>
{
for (int i = 1; i <= 5; i++)
{
await Task.Delay(1000);
table.AddRow($"10:0{i}", $"事件 {i}");
ctx.Refresh(); // 刷新显示
}
});
8. 带样式的异常显示
try
{
throw new InvalidOperationException("操作失败");
}
catch (Exception ex)
{
AnsiConsole.WriteException(ex, ExceptionFormats.ShortenPaths);
}
AutoUpdater.NET 是一个开源库,专为 .NET 桌面应用程序设计,支持 Windows Forms 和 WPF 应用。它通过从服务器获取 XML 文件来检测新版本信息,当发现新版本时向用户显示更新对话框。
相对于 ClickOnce,AutoUpdater.NET 的配置更简单一些。
首先通过 NuGet 包管理器安装 Autoupdater.NET.Official,然后在应用程序入口点添加以下代码:
AutoUpdater.Start("http://yourserver.com/path/to/update.xml");XML 文件结构如下:
<?xml version="1.0" encoding="UTF-8"?>
<item>
<version>2.0.0.0</version> <!--必填:最新版本号-->
<url>http://yourserver.com/path/to/updatefile.zip</url> <!--必填:更新文件下载地址-->
<changelog>http://yourserver.com/path/to/changelog.txt</changelog> <!--可选:更新日志-->
<mandatory>False</mandatory> <!--可选:是否强制更新-->
</item>Windows Forms 在 Program.cs 文件的 Main() 方法中,WPF 在 App.xaml.cs 的 OnStartup() 中添加:
AutoUpdater.Start("http://yourserver.com/path/to/update.xml");
AutoUpdater.CheckForUpdateEvent += (e) =>
{
if (e.Error != null)
{
MessageBox.Show($"检查版本更新失败:{e.Error.Message}");
}
if (e.IsUpdateAvailable)
{
if (e.Mandatory.Value)
{
// 强制更新
AutoUpdater.DownloadUpdate(e);
}
else
{
// 可选更新
if (MessageBox.Show("发现新版本,是否立即更新?", "更新提示", MessageBoxButtons.YesNo, MessageBoxIcon.Information) == DialogResult.Yes)
{
AutoUpdater.DownloadUpdate(e);
}
}
}
};这样就能简单实现打开应用时判断是否有更新。具体用法参:GitHub
对于控制台应用程序,AutoUpdater.NET 并不直接支持。可以使用 GeneralUpdate 等轻量级自动更新库。
其它:
若希望通过标准安装流程(如添加到开始菜单),优先选择 ClickOnce,适合长期维护的内部工具。
若追求快速集成和无感更新,AutoUpdater.NET 更灵活。
无论哪种方案,务必对程序进行代码签名(如购买企业证书或生成测试证书),否则系统可能拦截安装。(你要允许来自未知发布者的此应用对你的设备进行更改吗)
可以购买“OV 代码签名”证书或“EV 代码签名”证书,注意:“代码签名证书”与“域名证书”互不通用。
AsNoTracking 设置未追踪查询
var customers = dbContext.Customers.AsNoTracking().ToList();这对于只读查询非常有用,因为它可以减少内存使用并提高性能,因为它不需要维护实体的更改跟踪信息。
ExecuteDelete 和 ExecuteUpdate 批量操作
context.Logs.Where(c => c.Time < new DateTime(2000, 1, 1)).ExecuteDelete();从 EF Core 7 开始,ExecuteDelete 和 ExecuteUpdate 是官方原生支持的批量操作方法。直接操作数据库,不需要调用 SaveChanges():不加载实体到内存,减少内存消耗和网络往返。
若需要分页删除和大批量插入,或在高频、大规模场景,推荐使用 Zack.EFCore.Batch:
context.Logs.Where(c => c.Time < new DateTime(2000, 1, 1)).DeleteRangeAsync(batchSize: 1000);考虑用 Union 代替 OR
// Where 后行数多时(如分页前)用 OR
var q = db.dt_crm__contract.AsNoTracking();
q = q.Where(c => c.dt_crm__customer.SalesmanId == uid || myIns.Contains(c.IndustryId));
// 用于合并的 q1、q2 的行数少时用 Union
var q1 = db.dt_crm__contract.AsNoTracking().Where(c => c.dt_crm__customer.SalesmanId == uid);
var q2 = db.dt_crm__contract.AsNoTracking().Where(c => myIns.Contains(c.IndustryId));
var q = q1.Union(q2);“ToDictionary + Count”之前先 Select
// 不推荐
var dic = q.GroupBy(c => c.Date)
.ToDictionary(k => k.Key, v => v.Count());
// 推荐
var dic = q.GroupBy(c => c.Date)
.Select(g => new { g.Key, Count = g.Count() })
.ToDictionary(k => k.Key, v => v.Count);临界区与 lock 关键字
核心作用:
通过将多线程访问串行化,保护共享资源或代码段。lock 关键字是 Monitor 类的语法糖,提供异常安全的临界区实现。
实现示例:
// 创建私有静态只读对象 // private static readonly object _lockObj = new object(); private static readonly System.Threading.Lock _locker = new(); // .NET 9+ 推荐使用 Lock 类型,避免传统 object 的性能损耗 public void ThreadSafeMethod() { lock (_lockObj) { // 临界区代码(每次仅一个线程可进入) } }超时机制:
高并发场景可结合 Monitor.TryEnter 设置超时,避免无限等待:
if (Monitor.TryEnter(lockObject, TimeSpan.FromSeconds(1))) { try { /* 操作 */ } finally { Monitor.Exit(lockObject); } }关键特性:
用户态锁(无内核切换开销)
自动调用Monitor.Enter和Monitor.Exit
必须使用专用私有对象作为锁标识
注意事项:
❌ 避免锁定this、Type实例或字符串(易引发死锁)
❌ 避免嵌套锁(需严格按顺序释放)
✅ 推荐readonly修饰锁对象
❌ lock 不适用于异步代码(async/await),需使用 SemaphoreSlim 实现异步锁
互斥锁(Mutex)
核心作用:
系统级内核锁,支持跨进程同步,但性能开销较高(用户态/内核态切换)。
实现示例:
using var mutex = new Mutex(false, "Global\\MyAppMutex"); try { // 等待锁(最大等待时间500ms) if (mutex.WaitOne(500)) { // 临界区代码 } } finally { if (mutex != null) { mutex.ReleaseMutex(); } }关键特性:
支持跨应用程序域同步
线程终止时自动释放锁
支持命名互斥体(系统全局可见)
适用场景:
单实例应用程序控制
进程间共享文件访问
硬件设备独占访问
信号量(Semaphore)
核心作用:
通过许可计数器控制并发线程数,SemaphoreSlim为轻量级版本(用户态实现)。
实现对比:
类型 跨进程 性能 最大许可数 Semaphore ✔️ 低 系统限制 SemaphoreSlim ❌ 高 Int32.Max 代码示例:
// 创建初始3许可、最大5许可的信号量 var semaphore = new SemaphoreSlim(3, 5); semaphore.Wait(); // 获取许可 try { // 资源访问代码 } finally { semaphore.Release(); }异步编程
private readonly SemaphoreSlim _asyncLock = new(1, 1); public async Task UpdateAsync() { await _asyncLock.WaitAsync(); try { /* 异步操作 */ } finally { _asyncLock.Release(); } }典型应用:
数据库连接池(限制最大连接数)
API 请求限流
批量任务并发控制
事件(Event)
核心机制:
通过信号机制实现线程间通知,分为两种类型:
类型 信号重置方式 唤醒线程数 AutoResetEvent 自动 单个 ManualResetEvent 手动 所有 使用示例:
var autoEvent = new AutoResetEvent(false); // 等待线程 Task.Run(() => { autoEvent.WaitOne(); // 收到信号后执行 }); // 信号发送线程 autoEvent.Set(); // 唤醒一个等待线程高级用法:
配合WaitHandle.WaitAll实现多事件等待
使用ManualResetEventSlim提升性能
读写锁(ReaderWriterLockSlim)
核心优势:
实现读写分离的并发策略,适合读多写少场景(如缓存系统)。
锁模式对比:
模式 并发性 升级支持 读模式(EnterReadLock) 多线程并发读 ❌ 写模式(EnterWriteLock) 独占访问 ❌ 可升级模式 单线程读→写 ✔️ 代码示例:
var rwLock = new ReaderWriterLockSlim(); // 读操作 rwLock.EnterReadLock(); try { // 只读访问 } finally { rwLock.ExitReadLock(); } // 写操作 rwLock.EnterWriteLock(); try { // 排他写入 } finally { rwLock.ExitWriteLock(); }最佳实践:
优先使用ReaderWriterLockSlim(旧版有死锁风险)
避免长时间持有读锁(可能饿死写线程)
原子操作(Interlocked)
原理:
通过CPU指令实现无锁线程安全操作。
常用方法:
int counter = 0; Interlocked.Increment(ref counter); // 原子递增 Interlocked.Decrement(ref counter); // 原子递减 Interlocked.CompareExchange(ref value, newVal, oldVal); // CAS操作适用场景:
简单计数器
标志位状态切换
无锁数据结构实现
自旋锁(SpinLock)
核心特点:
通过忙等待(busy-wait)避免上下文切换,适用极短临界区(<1微秒)。
实现示例:
private SpinLock _spinLock = new SpinLock(); public void CriticalOperation() { bool lockTaken = false; try { _spinLock.Enter(ref lockTaken); // 极短临界区代码 } finally { if (lockTaken) _spinLock.Exit(); } }优化技巧:
单核CPU需调用Thread.SpinWait或Thread.Yield
配合SpinWait结构实现自适应等待
同步机制对比指南
机制 跨进程 开销级别 最佳适用场景 lock ❌ 低 通用临界区保护 Mutex ✔️ 高 进程间资源独占 Semaphore ✔️ 中 并发数限制(跨进程) SemaphoreSlim ❌ 低 并发数限制(进程内) ReaderWriterLockSlim ❌ 中 读多写少场景 SpinLock ❌ 极低 纳秒级临界区 Interlocked - 无锁 简单原子操作
选择原则:
优先考虑用户态锁(lock/SpinLock/SemaphoreSlim)
跨进程需求必须使用内核对象(Mutex/Semaphore)
读写比例超过10:1时考虑读写锁
自旋锁仅用于高频短操作(如链表指针修改)
通过以上结构化的分类和对比,开发者可以更精准地选择适合特定场景的线程同步方案。建议在实际使用中配合性能分析工具(如BenchmarkDotNet)进行量化验证。
💡 ASP.NET 的异步编程(async/await)本质是单进程内的线程调度,不算“跨进程”。每个 IIS 应用程序池对应一个独立的工作进程(w3wp.exe),不同用户访问同一应用程序池下的 ASP.NET 网站,两者的请求均由同一个 w3wp.exe 进程处理。可能跨进程的场景有:Web Garden 配置、多应用程序池部署等。
在 C# 中,除了常规锁机制(如 lock、Mutex、Semaphore 等),还有一些内置类型通过内部锁或无锁设计实现线程安全。以下是常见的几类:
线程安全集合(System.Collections.Concurrent)这些集合通过细粒度锁或无锁算法(如 CAS)实现线程安全,适合高并发场景。
ConcurrentDictionary:分段锁机制,将数据分片存储,每个分片独立加锁,减少锁竞争。
ConcurrentQueue / ConcurrentStack:基于原子操作(Interlocked)保证线程安全。
ConcurrentBag:每个线程维护本地存储,减少争用,适合频繁添加和移除的场景。
BlockingCollection:基于 ConcurrentQueue 和信号量(SemaphoreSlim)实现生产-消费者模式,支持阻塞和超时。
不可变集合(System.Collections.Immutable) 通过数据不可变性实现线程安全(无需锁),每次修改返回新对象。
Lazy 的线程安全初始化(Lazy<T>) 通过锁或 Interlocked 确保延迟初始化的线程安全。
通道(System.Threading.Channels)用于异步生产-消费者模型,内部通过锁和信号量管理容量限制。
内存缓存(System.Runtime.Caching.MemoryCache)内部使用锁保护共享状态,确保线程安全。
原子操作类型(Interlocked 类、Volatile 关键字、Unsafe 类)通过 CPU 指令实现无锁线程安全。
其他同步工具(Barrier、CountdownEvent)虽然不是严格意义上的锁,但用于协调线程。
| 字段 | MySQL 中的类型和精度 | MySQL 中占用字节 | 原因说明 |
| 股票价格 | DECIMAL(10,2) | 5 | 股票价格四舍五入保留两位小数,避免浮点型精度丢失。 即使最贵的股票价格并没有超过 9999.99 元,但考虑到复权价和扩展性,建议用更大的类型。 |
| 基金价格 | 基金价格四舍五入保留三位小数,避免浮点型精度丢失。 | ||
| 精确的成交金额(元)、成交量(手) | BIGINT UNSIGNED | 8 | 取值范围 0 到 1844,6744,0737,0955,1615(即一千多个亿亿)。 2024年A股总成交金额为257万亿元。 |
| 不精确的成交金额、成交量 | float | 4 | 取值范围足够大,有效数字约为6-7位。 |
| 交易日 | DATE | 3 | 包括年、月、日 |
| 交易时间 | DATETIME | 8 | 精确到分钟 |
| 6位股票代码 | CHAR(6) | 6 | 6个半角字符占6个字节 |
| 股票名称 | VARCHAR(10) | utf-8 的一个汉字占3个字节 |
打开 .csproj 项目文件,在 <PropertyGroup> 标签内添加:
<Version>
1.0.$([System.Math]::Floor($([System.DateTime]::Now.Subtract($([System.DateTime]::Parse('2000-01-01T00:00:00Z'))).TotalDays))).$([MSBuild]::Divide($([System.Math]::Floor($([System.DateTime]::Now.TimeOfDay.TotalSeconds))), 2))
</Version>最终生成的版本号示例: 1.0.9238.28518
其中,Major 与 Minor 是固定的,Build 是2000年1月1日至今的天数,Revision 是今天的秒数 / 2 所得的值。(为了防止数值超过 65535)
程序中获取版本号:
var version = Assembly.GetExecutingAssembly().GetName().Version!; // 当前类库
var version = Assembly.GetEntryAssembly()?.GetName().Version!; // 入口项目从版本号获取发布时间:
DateTime versionTime = new DateTime(2000, 1, 1).AddDays(version.Build).AddSeconds(version.Revision * 2);在解决方案资源管理器中找到 Properties/AssemblyInfo.cs 文件。该文件存放程序集版本信息。
修改版本号格式
将以下代码片段中的 AssemblyVersion 改为使用星号通配符(建议保留主版本和次版本号):
[assembly: AssemblyVersion("1.0.*")] // 自动生成构建号和修订号 // [assembly: AssemblyFileVersion("1.0.0.0")] // 注释或删除此行关闭确定性构建
用文本编辑器打开 .csproj 项目文件,在 <PropertyGroup> 标签内添加:
<Deterministic>false</Deterministic>此设置允许 MSBuild 生成动态版本号。
最终生成的版本号示例: 1.0.9238.28518
其中,Major 与 Minor 是固定的,Build 是2000年1月1日至今的天数,Revision 是今天的秒数 / 2 所得的值。(为了防止数值超过 65535)
程序中获取版本号:
var version = Assembly.GetExecutingAssembly().GetName().Version;从版本号获取发布时间:
DateTime versionTime = new DateTime(2000, 1, 1).AddDays(version.Build).AddSeconds(version.Revision * 2);查看 .NET Core / .NET 5+ 实现自动版本号的方法
通常,我们在更新一条数据库记录时,EF 先取出这一条记录:
var log = DbContext.MyTable.Find(id);然后赋值字段并保存:
log.Result = "OK";
DbContext.SaveChanges();这样就会产生两次数据库查询。
我们尝试用 Attach 方法。
先创建一个仅包含主键的对象:
var log = new MyTable { Id = id };将对象附加到上下文:
DbContext.MyTable.Attach(log);然后更新需要更新的字段:
log.Result = "OK";
DbContext.SaveChanges();这样,能在保留其它字段值的前提下,减少一次数据库查询。
但是需要注意的是:
当某非 null 字段需要恢复默认值时,EF 会忽略这个更改。(可能会因 EF 版本等原因有不同的结论)
举个例子:
某记录有个 int 型字段 a,在数据库中这个记录的 a 的值为 1,但 C# 中 int 型的默认值为 0,所以当 Attach 附加这个对象后,如果重新设置 log.a 为 0,那么保存后 a 的值仍为 1。
还有一种写法,利用 ExecuteUpdate 方法:
var affectedRows = DbContext.MyTable
.Where(c => c.Id == id)
.ExecuteUpdate(setters => setters
.SetProperty(c => c.Result, "OK")
);返回匹配的行数。
这种写法不会遇到“恢复为默认值不生效”的问题,推荐使用。
-
在 access_by_lua_block 代码块中实现远程鉴权:
#鉴权-START #resolver 223.5.5.5; # cat /etc/resolv.conf access_by_lua_block { local http = require("resty.http") local httpc = http.new() httpc:set_timeout(500) -- 连接超时 local res, err = httpc:request_uri("https://鉴权地址/", { method = "GET", headers = { ["X-Real-IP"] = ngx.var.remote_addr, ["User-Agent"] = ngx.var.http_user_agent, ["X-Forwarded-Host"] = ngx.var.host, ["X-Forwarded-Uri"] = ngx.var.request_uri, }, ssl_verify = false, -- 禁用 SSL 验证 timeout = 500, -- 读取超时 }) if not res then ngx.log(ngx.ERR, "Failed to request: " .. err) end if res and res.status == 403 then ngx.exit(ngx.HTTP_FORBIDDEN) -- return ngx.redirect("https://一个显示403友好信息的页面.html") end } #鉴权-END注意更改接口地址和友情显示 403 页面地址。
本示例仅捕获 403 状态码,500、408 等其它异常情况视为允许访问,请根据业务需求自行添加状态码的判断。
若超时也会进入
if not res then代码块。建议将此代码部署在 nginx 主配置文件的 http 代码块中(宝塔面板中的路径:/www/server/nginx/nginx/conf/nginx.conf),如果你只想为单个网站鉴权,也可以放在网站配置文件的 server 块中。
-
若鉴权接口在私网中,建议将鉴权接口域名和私网 IP 添加到 hosts 文件中。
附
-
直接输出字符串
ngx.header.content_type = "text/plain"; ngx.say("hello world!") -
输出到日志
ngx.log(ngx.ERR, "Response status: " .. res.status)日志在网站的 站名.error.log 中查看。
宝塔面板查看方式:日志 - 网站日志 - 异常
-
若想获取服务器 CPU 使用率等信息并传递给远程鉴权接口,请参考此文。
-
常见问题
no resolver defined to resolve
原因:没有定义 DNS 解析器
解决方法:在 http 块或 server 块中添加
resolver 8.8.8.8 valid=30s;,推荐使用接入商自己的公共 DNS,如果走内网,推荐使用本机 DNS。unable to get local issuer certificate
原因:没有配置 SSL 证书信息
解决方法:添加 request_uri 参数:
ssl_verify = true, -- 启用 SSL 验证 ssl_trusted_certificate = "证书路径", -- 指定 CA 证书路径或
ssl_verify = false, -- 禁用 SSL 验证 -
若您不想用 lua,可以用 nginx 原生自带的 auth_request 模块来实现。
-
若 WAF 返回拒绝访问时需要 302 跳转(即 return ngx.redirect(...)),那么最好加上禁止缓存的标识,防止 CDN 污染:
if res and res.status == 403 then -- ===== 禁止 CDN 缓存此响应 ===== ngx.header["Cache-Control"] = "no-store, no-cache, must-revalidate, proxy-revalidate" ngx.header["Pragma"] = "no-cache" ngx.header["Expires"] = "0" -- 可选:添加标记头,方便调试 ngx.header["X-WAF-Action"] = "blocked" return ngx.redirect("https://...") end
本文记录于 2024 年 11 月。
| 升级前 | 期望(最新正式版) | 最终选择 | |
| 操作系统 | Alibaba Cloud Linux 3 | Alibaba Cloud Linux 3 | Alibaba Cloud Linux 3 |
| 管理面板 | 宝塔面板 Linux 版 9.2.0 | 宝塔面板 Linux 版 9.2.0 | 宝塔面板 Linux 版 9.2.0 |
| Web 服务 | nginx 1.24 | nginx 1.24 | nginx 1.24 |
| 脚本语言 | PHP 7.4 | PHP 8.2 | PHP 8.2 |
| 数据库 | PolarDB MySQL 5.6 | RDS MySQL 9.1 | PolarDB MySQL 8.0 |
| 论坛程序 | Discuz! X3.4 GBK | Discuz! X5.0 | Discuz! X3.5 UTF8 |
版本选择原因:
Discuz! X5 刚刚发布,生态尚未成熟,考虑到是老论坛升级,所以选择 X3.5。
Discuz! X3.5 目前最高支持 PHP 8.2、MySQL 8.0。
完整升级步骤:
购买新的 ECS、PolarDB,具体环境配置步骤参此文;
安装宝塔面板并配置;
安装 nginx 及 PHP;
创建网站、配置 SSL、伪静态、防盗链、可写目录禁执行、仅允许部分入口文件执行等(.conf);
配置 hosts;
如果是正式升级阶段,关闭论坛,防止产生新数据。
备份原网站程序、PolarDB 数据库;
PolarDB 创建快照。
测试阶段:从快照还原到新实例(MySQL 5.6 不能直接恢复到 MySQL 8.0),然后从 5.6 迁移到 8.0.x;
正式阶段:全量模式直接从原实例迁移到 8.0.x,若增量模式且存在触发器,建议从快照还原;
上传原网站程序到新的站点目录下;
按 Discuz! X 升级文档升级 X3.4 至 X3.5;详情见下文 ↓;
升级完成后切换到 PHP 8;
配置 OSS、Redis、更新缓存等;
测试论坛基本功能是否正常;检查附件是否能够正常上传;检查附件是否正常显示;全面检查控制台配置;
逐个开启插件并检查兼容性(短信通、马甲引擎等);
按二开备忘录逐个按需进行二开;
逐个修改调用论坛接口的项目及直接调用论坛数据库的项目;
调试 MAGAPP 接口;
尝试强制 https 访问;
将以上所有修改后的程序保留备份;发布升级公告并关闭论坛;重复以上步骤;修改域名解析;开启论坛;
配置 IP 封禁、定时器、日志、自动备份、配置其它 ECS 的 hosts 等;
查看搜索引擎中收录的地址,是否有无法访问的情况;
尝试将历史遗留的本地附件全部转移到 OSS;
建议 在新服上创建 3 个网站:
第 1 个用来尝试迁移、升级、二开,并测试所有功能;
第 2 个用来再次重复这些步骤,最终保留程序代码及 UGC 文件,作为最终的网站程序,以正式域名作为网站根目录路径;
第 3 个用来正式升级,主要功能是升级最新数据库。完成后修改第 2 个网站的数据库连接指向到最新的数据库,差异化同步新增的 UGC 文件到第 2 个网站。
Discuz! X 升级步骤及注意点:
因 PolarDB MySQL 不支持压缩,所以应移除 Discuz! 和 UCenter 代码中所有的 MYSQLI_CLIENT_COMPRESS,将 , MYSQLI_CLIENT_COMPRESS 替换为 /*, MYSQLI_CLIENT_COMPRESS*/。
升级前务必先修改 ./config/ 目录下的数据库/缓存连接信息,以防出现新站连接老库的情况;
按官方文档进行升级,升级前先修改一下:
【UC】先升级 UCenter 1.6 至 1.7。
将 /uc_server/data 权限改为 777 并递归。
打开 update_ucenter_adult.php 修改 $limit 值为 10000 以免执行超时。
因通信失败的“发送通知失败”可以直接改 URL 参数跳过就完成了,原因可能是因数据量大,发送改名通知执行改名时超时。等DZ升级完成后UC会自动重试改名通知,或单独写个 php 文件将 UCenter 的用户名同步到应用的数据库。
【DZ】运行到 /install/update_adult.php?step=innodb&table=pre_common_member_grouppm 时报错:Duplicate key name 'gpmid' ALTER TABLE common_member_grouppm ADD INDEX gpmid(gpmid);
解决方法:先删除索引,因保存时提示失败,应同时取消 gpmid 字段的自增,转换成功后再设置自增。
【DZ】运行到 /install/update_adult.php?step=file 一片空白而停止
正在解决
【问题】common_menber 表用户名字段编码转换失败
原因是部分用户名包含特殊字符(如全角空格),用以下语句查看两个表同一 uid 的不同用户:
SELECT u.uid, u.username AS ucenter_username, c.username AS common_username FROM pre_ucenter_members u JOIN pre_common_member c ON u.uid = c.uid WHERE u.username <> c.usernameDiscuz! 用户登录都是以 UCenter 中的用户名为主,所以可以写个小程序直接将 pre_ucenter_members 的用户名同步到 pre_common_member 中,另外 pre_ucenter_members 的用户数少于 pre_common_member 是正常的。若 pre_common_member_archive 表遇到错误同理。
如果用户登录慢,或打开 UCenter 慢,是因为 UC 正在通知 DZ 改用户名,每次打开一个页面会更改一个用户名,具体可以查看 pre_ucenter_notelist 中 closed 为 0 的队列,或进入 UC 后台-数据列表-通知列表 查看。
【问题】发布主题遇到错误:(1062) Duplicate entry '*' for key 'pid'
【原因】forum_post 中的 pid 不是自动增长的,而是由表 forum_post_tableid 中自动增长的 pid 生成的。如果生成的 pid 值已在 forum_post 表中存在,则会出现此错误。
【解决】迁移数据库时应关闭论坛,以防止 forum_post 表有新数据插入。
【问题】打开帖子页面 ./thread-***-1-1.html 显示 404 Not Found,而 ./forum.php?mod=viewthread&tid=*** 可以正常打开
【原因】未配置伪静态(可在宝塔面板中选择)
【问题】打开 UCenter 时报错:UCenter info: MySQL Query Error SQL:SELECT value FROM [Table]vars WHERE name='noteexists'
【解决】打开文件 ./uc_server/data/config.inc.php 配置数据库连接
【问题】打开登录 UCenter 后一片空白
【解决】将目录 ./uc_server/data/ 设为可写
需要将原来安装的插件文件移回 ./source/plugin/ 目录,并设置可写;
界面-表情管理,界面-编辑器设置-Discuz!代码
后续 Discuz! X3.5 小版本升级注意事项:
确认插件是否支持新版本(如短信通)
先创建一个新网站测试二开代码
保留 /config/、/data/、/uc_client/data/、/uc_server/data/、/source/plugin/,其它移入 old
上传文件
移回其它需要的文件,如:
-- 勋章/loading/logo/nv 等:/static/image/common/
-- 表情:/static/image/smiley/
-- 水印:/static/image/common/watermark.*
-- 风格:/template/default/style/t2/nv.png 等
-- 默认头像:/uc_server/images/noavatar_***.gif
-- 根目录 favicon.ico 等
-- 及其它非 DZ 文件
再次检查可写目录的写入权限和禁止运行 PHP 效果。