<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>阿柒の站</title><description>躺平程序员的自言自语</description><link>https://wc.sb/</link><language>zh_CN</language><item><title>C# 日志记录方案</title><link>https://wc.sb/posts/csharp-logging-solution/</link><guid isPermaLink="true">https://wc.sb/posts/csharp-logging-solution/</guid><pubDate>Thu, 11 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;一个不依赖框架、灵活的日志记录类。&lt;/p&gt;
&lt;p&gt;优点:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不依赖其他框架，极简灵活。&lt;/li&gt;
&lt;li&gt;分块存储，不怕单文件过大。&lt;/li&gt;
&lt;li&gt;写日志时自动检测创建文件，不担心删除文件后写入不了。&lt;/li&gt;
&lt;li&gt;随用随调，静态实现，任何地方直接调用就能记录日志。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;Logs.cs&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static class Logs
{
    private static readonly ConcurrentQueue&amp;lt;(string Level, string Message, DateTime Time)&amp;gt; LogQueue = new();
    private static readonly CancellationTokenSource TokenSource = new();
    private static readonly string LogRoot = Path.Combine(AppContext.BaseDirectory, &quot;Log&quot;); // 存储路径，默认程序根目录下的Log文件夹下
    private static readonly long MaxFileSize = 5 * 1024 * 1024; // 5MB
    private static DateTime _lastLogTime = DateTime.MinValue;
    private static readonly object WriteLock = new();

    static Logs()
    {
        Task.Factory.StartNew(ProcessQueue, TaskCreationOptions.LongRunning);
    }

    public static void Info(string message) =&amp;gt; Enqueue(&quot;info&quot;, message); // 详情记录
    public static void Error(string message) =&amp;gt; Enqueue(&quot;error&quot;, message); // 错误记录
	// 可继续扩展...

    private static void Enqueue(string level, string message)
    {
        LogQueue.Enqueue((level.ToLower(), message, DateTime.Now));
    }

    private static async Task ProcessQueue()
    {
        while (!TokenSource.IsCancellationRequested)
        {
            while (LogQueue.TryDequeue(out var entry))
            {
                try
                {
                    WriteLog(entry.Level, entry.Message, entry.Time);
                }
                catch
                {
                    // 可扩展重试逻辑或写入到 fallback 文件
                }
            }
            await Task.Delay(100); // 避免空转
        }
    }

    private static void WriteLog(string level, string message, DateTime time)
    {
        var basePath = Path.Combine(LogRoot, time.ToString(&quot;yyyy_MM&quot;));
        Directory.CreateDirectory(basePath);

        var filePrefix = Path.Combine(basePath, $&quot;{time:dd}_{level}&quot;);
        var filePath = GetAvailableLogFile(filePrefix);

        var content = $&quot;[{time:yyyy-MM-dd HH:mm:ss.fff}] {message}{Environment.NewLine}&quot;;

        lock (WriteLock)
        {
            File.AppendAllText(filePath, content, Encoding.UTF8);
        }

        _lastLogTime = time;
    }

    private static string GetAvailableLogFile(string basePath)
    {
        for (int i = 0; i &amp;lt; 1000; i++)
        {
            var filePath = i == 0 ? $&quot;{basePath}.txt&quot; : $&quot;{basePath}_{i}.txt&quot;;
            if (!File.Exists(filePath) || new FileInfo(filePath).Length &amp;lt; MaxFileSize)
                return filePath;
        }

        throw new IOException(&quot;日志文件已超过最大数量限制&quot;);
    }

    public static void Stop()
    {
        TokenSource.Cancel();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Logs.Info(&quot;记录详情日志&quot;);
Logs.Error(&quot;记录错误日志&quot;);
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>C# 读写配置文件方案</title><link>https://wc.sb/posts/csharp-config-file-read-write/</link><guid isPermaLink="true">https://wc.sb/posts/csharp-config-file-read-write/</guid><pubDate>Thu, 11 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;一个防修改、保持完整性的读写配置文件方案。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ConfigManager.cs&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class ConfigManager
{
    private static readonly string ConfigPath = Path.Combine(AppContext.BaseDirectory, &quot;config.dat&quot;); // 配置文件路径，默认存储在程序根目录
    private static readonly byte[] HmacKey = Encoding.UTF8.GetBytes(&quot;abcdefgh&quot;); // 加密密钥

    private Dictionary&amp;lt;string, object&amp;gt; _configData;
    private readonly object _lock = new();

    public static ConfigManager Instance { get; } = new ConfigManager();

    /// &amp;lt;summary&amp;gt;
    /// 构造函数
    /// &amp;lt;/summary&amp;gt;
    private ConfigManager()
    {
        Load();
    }

    /// &amp;lt;summary&amp;gt;
    /// 索引器
    /// &amp;lt;/summary&amp;gt;
    /// &amp;lt;param name=&quot;key&quot;&amp;gt;&amp;lt;/param&amp;gt;
    /// &amp;lt;returns&amp;gt;&amp;lt;/returns&amp;gt;
    public object? this[string key]
    {
        get =&amp;gt; Get(key);
        set =&amp;gt; Set(key, value);
    }

    /// &amp;lt;summary&amp;gt;
    /// 获取配置项
    /// &amp;lt;/summary&amp;gt;
    /// &amp;lt;param name=&quot;key&quot;&amp;gt;&amp;lt;/param&amp;gt;
    /// &amp;lt;returns&amp;gt;&amp;lt;/returns&amp;gt;
    public object? Get(string key)
    {
        var sections = key.Split(&apos;:&apos;, StringSplitOptions.RemoveEmptyEntries);
        object? current = _configData;

        foreach (var section in sections)
        {
            if (current is Dictionary&amp;lt;string, object&amp;gt; dict &amp;amp;&amp;amp; dict.TryGetValue(section, out var next))
                current = next;
            else
                return null;
        }

        return current;
    }

    /// &amp;lt;summary&amp;gt;
    /// 设置配置项
    /// &amp;lt;/summary&amp;gt;
    /// &amp;lt;param name=&quot;key&quot;&amp;gt;&amp;lt;/param&amp;gt;
    /// &amp;lt;param name=&quot;value&quot;&amp;gt;&amp;lt;/param&amp;gt;
    public void Set(string key, object? value)
    {
        var sections = key.Split(&apos;:&apos;, StringSplitOptions.RemoveEmptyEntries);
        lock (_lock)
        {
            var current = _configData;
            for (int i = 0; i &amp;lt; sections.Length - 1; i++)
            {
                var section = sections[i];
                if (!current.TryGetValue(section, out var sub) || sub is not Dictionary&amp;lt;string, object&amp;gt;)
                {
                    current[section] = new Dictionary&amp;lt;string, object&amp;gt;();
                }
                current = (Dictionary&amp;lt;string, object&amp;gt;)current[section];
            }

            current[sections.Last()] = value!;
        }
    }

    /// &amp;lt;summary&amp;gt;
    /// 存储配置文件
    /// &amp;lt;/summary&amp;gt;
    public void Save()
    {
        lock (_lock)
        {
            var json = JsonSerializer.Serialize(_configData);
            var compressed = Compress(Encoding.UTF8.GetBytes(json));
            var signature = HMACSHA256.HashData(HmacKey, compressed);
            var finalData = signature.Concat(compressed).ToArray();
            File.WriteAllBytes(ConfigPath, finalData);
        }
    }

    /// &amp;lt;summary&amp;gt;
    /// 加载配置文件
    /// &amp;lt;/summary&amp;gt;
    private void Load()
    {
        try
        {
            if (!File.Exists(ConfigPath))
            {
                // 如果配置文件不存在，可以选择创建一个默认配置
                _configData = [];
                Save();
                //throw new FileNotFoundException();
            }

            var bytes = File.ReadAllBytes(ConfigPath);
            if (bytes.Length &amp;lt; 32)
                throw new Exception(&quot;配置文件不完整&quot;);

            var signature = bytes.Take(32).ToArray();
            var content = bytes.Skip(32).ToArray();
            var actualSignature = HMACSHA256.HashData(HmacKey, content);

            if (!signature.SequenceEqual(actualSignature))
                throw new Exception(&quot;配置文件已被篡改&quot;);

            var decompressed = Decompress(content);
            _configData = JsonSerializer.Deserialize&amp;lt;Dictionary&amp;lt;string, object&amp;gt;&amp;gt;(decompressed)!;
        }
        catch
        {
            // 可以在这里加载默认配置

            _configData = [];
            System.Windows.MessageBox.Show(&quot;配置文件损坏或不存在，已加载默认配置&quot;, &quot;提示&quot;, MessageBoxButton.OK, MessageBoxImage.Warning);
        }
    }

    /// &amp;lt;summary&amp;gt;
    /// 压缩配置文件
    /// &amp;lt;/summary&amp;gt;
    /// &amp;lt;param name=&quot;data&quot;&amp;gt;&amp;lt;/param&amp;gt;
    /// &amp;lt;returns&amp;gt;&amp;lt;/returns&amp;gt;
    private static byte[] Compress(byte[] data)
    {
        using var output = new MemoryStream();
        using (var gz = new GZipStream(output, CompressionLevel.Optimal))
            gz.Write(data, 0, data.Length);
        return output.ToArray();
    }

    /// &amp;lt;summary&amp;gt;
    /// 解压缩配置文件
    /// &amp;lt;/summary&amp;gt;
    /// &amp;lt;param name=&quot;data&quot;&amp;gt;&amp;lt;/param&amp;gt;
    /// &amp;lt;returns&amp;gt;&amp;lt;/returns&amp;gt;
    private static string Decompress(byte[] data)
    {
        using var input = new MemoryStream(data);
        using var gz = new GZipStream(input, CompressionMode.Decompress);
        using var reader = new StreamReader(gz, Encoding.UTF8);
        return reader.ReadToEnd();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;SysConfig.cs&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/// &amp;lt;summary&amp;gt;
/// 系统配置管理
/// &amp;lt;/summary&amp;gt;
public static class SysConfig
{
    /// &amp;lt;summary&amp;gt;
    /// 获取配置项
    /// &amp;lt;/summary&amp;gt;
    /// &amp;lt;param name=&quot;key&quot;&amp;gt;&amp;lt;/param&amp;gt;
    /// &amp;lt;returns&amp;gt;&amp;lt;/returns&amp;gt;
    public static string? Get(string key)=&amp;gt; Get&amp;lt;string&amp;gt;(key);

    /// &amp;lt;summary&amp;gt;
    /// 获取配置项
    /// &amp;lt;/summary&amp;gt;
    public static T? Get&amp;lt;T&amp;gt;(string key)
    {
        var value = ConfigManager.Instance.Get(key);
        if (value == null) return default;

        try
        {
            if (value is JsonElement jsonElement)
            {
                value = jsonElement.ToString();
            }
            return (T)Convert.ChangeType(value, typeof(T));
        }
        catch
        {
            return default;
        }
    }

    /// &amp;lt;summary&amp;gt;
    /// 设置配置项
    /// &amp;lt;/summary&amp;gt;
    public static void Set(string key, object? value)
    {
        ConfigManager.Instance.Set(key, value);
    }

    /// &amp;lt;summary&amp;gt;
    /// 设置配置项并保存
    /// &amp;lt;/summary&amp;gt;
    /// &amp;lt;param name=&quot;key&quot;&amp;gt;&amp;lt;/param&amp;gt;
    /// &amp;lt;param name=&quot;value&quot;&amp;gt;&amp;lt;/param&amp;gt;
    public static void SetAndSave(string key, object? value)
    {
        ConfigManager.Instance.Set(key, value);
        ConfigManager.Instance.Save();
    }

    /// &amp;lt;summary&amp;gt;
    /// 保存配置
    /// &amp;lt;/summary&amp;gt;
    public static void Save()
    {
        ConfigManager.Instance.Save();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 获取
SysConfig.Get(&quot;FirstConfig&quot;);
SysConfig.Get&amp;lt;int&amp;gt;(&quot;ConvertIntConfig&quot;);
SysConfig.Get&amp;lt;bool&amp;gt;(&quot;Convert:FirstBoolVal&quot;);

// 设置
SysConfig.Set(&quot;FirstConfig&quot;, &quot;保存值&quot;);
SysConfig.Set(&quot;Convert:FirstBoolVal&quot;, true);

// 保存配置项
SysConfig.Save();

// 上述[设置]和[保存配置项]可使用下面一步完成
SysConfig.SetAndSave(&quot;ConvertIntConfig&quot;, 100);
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>C# Sqlite读写方案</title><link>https://wc.sb/posts/csharp-sqlite-simple-guide/</link><guid isPermaLink="true">https://wc.sb/posts/csharp-sqlite-simple-guide/</guid><pubDate>Thu, 11 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;一个简单的C#实现Sqlite数据库读写的方法嘞&lt;/p&gt;
&lt;p&gt;优点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;简单实用，静态实现，随调随用&lt;/li&gt;
&lt;li&gt;单例实现，合理的资源释放&lt;/li&gt;
&lt;li&gt;Entity First，不用管表，ORM的实现&lt;/li&gt;
&lt;li&gt;支持异步&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;AppDbContext.cs&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;using Microsoft.EntityFrameworkCore;

public class AppDbContext : DbContext
{
    /// &amp;lt;summary&amp;gt;
    /// 用户实体类
    /// &amp;lt;/summary&amp;gt;
    public DbSet&amp;lt;UserEntity&amp;gt; Users{ get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        if (!optionsBuilder.IsConfigured)
        {
            optionsBuilder.UseSqlite(&quot;Data Source=database.db&quot;); // 默认存储在程序根目录
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;SqliteSchemaUpdater.cs&lt;/code&gt; 这个主要是实现ORM模式，根据实体类自动CURD数据表&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;
using System.Data.Common;
using System.Text;

public static class SqliteSchemaUpdater
{
    public static void EnsureSchemaSynced(DbContext context, bool log = false)
    {
        var connection = context.Database.GetDbConnection();
        connection.Open();

        var entityTypes = context.Model.GetEntityTypes();
        var existingTables = GetExistingTableNames(connection);

        // Step 1: DROP tables that are no longer in model
        foreach (var oldTable in existingTables)
        {
            if (!entityTypes.Any(e =&amp;gt; e.GetTableName() == oldTable))
            {
                Log($&quot;Dropping table: {oldTable}&quot;, log);
                ExecuteNonQuery(connection, $&quot;DROP TABLE IF EXISTS [{oldTable}];&quot;);
            }
        }

        // Step 2: CREATE or ALTER
        foreach (var entityType in entityTypes)
        {
            var tableName = entityType.GetTableName()!;
            var props = entityType.GetProperties();

            if (!existingTables.Contains(tableName))
            {
                var createSql = BuildCreateTableSql(tableName, props);
                Log($&quot;Creating table: {tableName}&quot;, log);
                ExecuteNonQuery(connection, createSql);
            }
            else
            {
                var existingColumns = GetExistingColumnNames(connection, tableName);
                var modelColumns = props.Select(p =&amp;gt; p.GetColumnName()).ToList();

                // Add missing columns
                foreach (var p in props)
                {
                    var columnName = p.GetColumnName();
                    if (!existingColumns.Contains(columnName))
                    {
                        var columnSql = BuildAddColumnSql(p);
                        Log($&quot;Adding column: {columnName} to {tableName}&quot;, log);
                        ExecuteNonQuery(connection, $&quot;ALTER TABLE [{tableName}] ADD COLUMN {columnSql};&quot;);
                    }
                }

                // Drop obsolete columns
                foreach (var oldCol in existingColumns)
                {
                    if (!modelColumns.Contains(oldCol))
                    {
                        Log($&quot;Dropping column: {oldCol} from {tableName}&quot;, log);
                        RecreateTableWithoutColumn(connection, entityType, oldCol, log);
                    }
                }
            }
        }
    }

    private static HashSet&amp;lt;string&amp;gt; GetExistingTableNames(DbConnection conn)
    {
        var tables = new HashSet&amp;lt;string&amp;gt;();
        using var cmd = conn.CreateCommand();
        cmd.CommandText = &quot;SELECT name FROM sqlite_master WHERE type=&apos;table&apos; AND name NOT LIKE &apos;sqlite_%&apos;;&quot;;
        using var reader = cmd.ExecuteReader();
        while (reader.Read())
            tables.Add(reader.GetString(0));
        return tables;
    }

    private static HashSet&amp;lt;string&amp;gt; GetExistingColumnNames(DbConnection conn, string tableName)
    {
        var columns = new HashSet&amp;lt;string&amp;gt;();
        using var cmd = conn.CreateCommand();
        cmd.CommandText = $&quot;PRAGMA table_info([{tableName}]);&quot;;
        using var reader = cmd.ExecuteReader();
        while (reader.Read())
            columns.Add(reader.GetString(1));
        return columns;
    }

    private static string BuildCreateTableSql(string tableName, IEnumerable&amp;lt;IProperty&amp;gt; props)
    {
        var cols = props.Select(BuildColumnDefinition);
        return $&quot;CREATE TABLE IF NOT EXISTS [{tableName}] ({string.Join(&quot;, &quot;, cols)});&quot;;
    }

    private static string BuildColumnDefinition(IProperty prop)
    {
        var name = prop.GetColumnName();
        var type = prop.ClrType;
        var sqlType = GetSqliteType(type);
        var sb = new StringBuilder($&quot;[{name}] {sqlType}&quot;);

        if (prop.IsPrimaryKey()) sb.Append(&quot; PRIMARY KEY&quot;);
        if (prop.IsPrimaryKey() &amp;amp;&amp;amp; (type == typeof(int) || type == typeof(long))) sb.Append(&quot; AUTOINCREMENT&quot;);

        return sb.ToString();
    }

    private static string BuildAddColumnSql(IProperty prop) =&amp;gt; BuildColumnDefinition(prop);

    private static string GetSqliteType(Type type) =&amp;gt;
        type == typeof(int) || type == typeof(long) ? &quot;INTEGER&quot; :
        type == typeof(double) || type == typeof(float) ? &quot;REAL&quot; :
        type == typeof(string) ? &quot;TEXT&quot; :
        type == typeof(bool) ? &quot;INTEGER&quot; :
        type == typeof(DateTime) ? &quot;TEXT&quot; : &quot;TEXT&quot;;

    private static void ExecuteNonQuery(DbConnection conn, string sql)
    {
        using var cmd = conn.CreateCommand();
        cmd.CommandText = sql;
        cmd.ExecuteNonQuery();
    }

    private static void RecreateTableWithoutColumn(DbConnection conn, IEntityType entityType, string dropColumn, bool log)
    {
        var tableName = entityType.GetTableName()!;
        var tempTable = tableName + &quot;_temp&quot;;
        var props = entityType.GetProperties().Where(p =&amp;gt; p.GetColumnName() != dropColumn).ToList();

        Log($&quot;Recreating table {tableName} without column {dropColumn}&quot;, log);

        // 1. Create temp table
        var createTemp = BuildCreateTableSql(tempTable, props);
        ExecuteNonQuery(conn, createTemp);

        // 2. Copy data
        var columns = string.Join(&quot;, &quot;, props.Select(p =&amp;gt; p.GetColumnName()));
        ExecuteNonQuery(conn, $&quot;INSERT INTO [{tempTable}] ({columns}) SELECT {columns} FROM [{tableName}];&quot;);

        // 3. Drop original table
        ExecuteNonQuery(conn, $&quot;DROP TABLE [{tableName}];&quot;);

        // 4. Rename temp
        ExecuteNonQuery(conn, $&quot;ALTER TABLE [{tempTable}] RENAME TO [{tableName}];&quot;);
    }

    private static void Log(string message, bool enableLog)
    {
        if (enableLog)
            Logs.Info($&quot;[SchemaUpdater] {message}&quot;); // Logs类可以看我之前的日志方案文章：https://wc.sb/23
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用示例：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;首先要在App.xaml.cs中处理初始化&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// App.xaml.cs

/// &amp;lt;summary&amp;gt;
/// 程序启动后事件
/// &amp;lt;/summary&amp;gt;
/// &amp;lt;param name=&quot;e&quot;&amp;gt;&amp;lt;/param&amp;gt;
protected override void OnStartup(StartupEventArgs e)
{
    base.OnStartup(e);

    using var db = new AppDbContext();

    // 自动同步数据库结构，log: true 表示输出日志
    SqliteSchemaUpdater.EnsureSchemaSynced(db, log: true);

    // 下面可以写一些数据库初始化操作...

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;using var db = new AppDbContext(); // 单例实现，啥时候用啥时候创建，不用定义成全局什么的
var user = db.Users.FirstOrDefault(x=&amp;gt;x.Id == &quot;xxx&quot;); // 查数据库，ORM形式，Linq直接查
user.UserName = &quot;李四&quot;;
db.Update(user); // 更新数据

UserEntity addUser = new UserEntity();
addUser.UserName = &quot;张三&quot;;
db.Add(addUser); // 插入数据

db.Remove(user); // 删除数据

db.SaveChanges(); // 每次操作数据库后，都要执行这个保存操作
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>C# 经纬度的转换，百度、高德、腾讯、GPS互转</title><link>https://wc.sb/posts/csharp-coordinate-conversion/</link><guid isPermaLink="true">https://wc.sb/posts/csharp-coordinate-conversion/</guid><pubDate>Thu, 26 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;pre&gt;&lt;code&gt;
/// &amp;lt;summary&amp;gt;
/// 经纬度转换
/// &amp;lt;/summary&amp;gt;
public static class LocationUtil
{
    private const double Pi = 3.1415926535897932384626;
    private const double A = 6378245.0;
    private const double EE = 0.00669342162296594323;
    private const double X_PI = Pi * 3000.0 / 180.0;

    /// &amp;lt;summary&amp;gt;
    /// 是否在中国区域
    /// &amp;lt;/summary&amp;gt;
    /// &amp;lt;param name=&quot;lat&quot;&amp;gt;纬度(X，100多那个)&amp;lt;/param&amp;gt;
    /// &amp;lt;param name=&quot;lng&quot;&amp;gt;经度(Y，30多那个)&amp;lt;/param&amp;gt;
    /// &amp;lt;returns&amp;gt;&amp;lt;/returns&amp;gt;
    public static bool IsInChina(double lat, double lng)
    {
        return lng &amp;gt;= 72.004 &amp;amp;&amp;amp; lng &amp;lt;= 137.8347 &amp;amp;&amp;amp; lat &amp;gt;= 0.8293 &amp;amp;&amp;amp; lat &amp;lt;= 55.8271;
    }

    #region GPS &amp;lt;=&amp;gt; GCJ02

    /// &amp;lt;summary&amp;gt;
    /// 将WGS84坐标转换为GCJ02坐标（中国国测局坐标）
    /// &amp;lt;/summary&amp;gt;
    /// &amp;lt;param name=&quot;lat&quot;&amp;gt;纬度(X，100多那个)&amp;lt;/param&amp;gt;
    /// &amp;lt;param name=&quot;lng&quot;&amp;gt;经度(Y，30多那个)&amp;lt;/param&amp;gt;
    /// &amp;lt;returns&amp;gt;&amp;lt;/returns&amp;gt;
    public static (double lat, double lng) Wgs84ToGcj02(double lat, double lng)
    {
        if (!IsInChina(lat, lng)) return (lat, lng);
        var (dLat, dLng) = Delta(lat, lng);
        return (lat + dLat, lng + dLng);
    }

    /// &amp;lt;summary&amp;gt;
    /// 将GCJ02坐标转换为WGS84坐标（GPS坐标）
    /// &amp;lt;/summary&amp;gt;
    /// &amp;lt;param name=&quot;lat&quot;&amp;gt;纬度(X，100多那个)&amp;lt;/param&amp;gt;
    /// &amp;lt;param name=&quot;lng&quot;&amp;gt;经度(Y，30多那个)&amp;lt;/param&amp;gt;
    /// &amp;lt;returns&amp;gt;&amp;lt;/returns&amp;gt;
    public static (double lat, double lng) Gcj02ToWgs84(double lat, double lng)
    {
        if (!IsInChina(lat, lng)) return (lat, lng);
        var (dLat, dLng) = Delta(lat, lng);
        return (lat - dLat, lng - dLng);
    }

    private static (double, double) Delta(double lat, double lng)
    {
        double dLat = TransformLat(lng - 105.0, lat - 35.0);
        double dLng = TransformLng(lng - 105.0, lat - 35.0);
        double radLat = lat / 180.0 * Pi;
        double magic = Math.Sin(radLat);
        magic = 1 - EE * magic * magic;
        double sqrtMagic = Math.Sqrt(magic);
        dLat = (dLat * 180.0) / ((A * (1 - EE)) / (magic * sqrtMagic) * Pi);
        dLng = (dLng * 180.0) / (A / sqrtMagic * Math.Cos(radLat) * Pi);
        return (dLat, dLng);
    }

    private static double TransformLat(double x, double y)
    {
        double ret = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y +
                     0.1 * x * y + 0.2 * Math.Sqrt(Math.Abs(x));
        ret += (20.0 * Math.Sin(6.0 * x * Pi) +
                20.0 * Math.Sin(2.0 * x * Pi)) * 2.0 / 3.0;
        ret += (20.0 * Math.Sin(y * Pi) +
                40.0 * Math.Sin(y / 3.0 * Pi)) * 2.0 / 3.0;
        ret += (160.0 * Math.Sin(y / 12.0 * Pi) +
                320.0 * Math.Sin(y * Pi / 30.0)) * 2.0 / 3.0;
        return ret;
    }

    private static double TransformLng(double x, double y)
    {
        double ret = 300.0 + x + 2.0 * y + 0.1 * x * x +
                     0.1 * x * y + 0.1 * Math.Sqrt(Math.Abs(x));
        ret += (20.0 * Math.Sin(6.0 * x * Pi) +
                20.0 * Math.Sin(2.0 * x * Pi)) * 2.0 / 3.0;
        ret += (20.0 * Math.Sin(x * Pi) +
                40.0 * Math.Sin(x / 3.0 * Pi)) * 2.0 / 3.0;
        ret += (150.0 * Math.Sin(x / 12.0 * Pi) +
                300.0 * Math.Sin(x / 30.0 * Pi)) * 2.0 / 3.0;
        return ret;
    }

    #endregion

    #region GCJ02 &amp;lt;=&amp;gt; BD09

    /// &amp;lt;summary&amp;gt;
    /// 将GCJ02坐标转换为BD09坐标（百度坐标）
    /// &amp;lt;/summary&amp;gt;
    /// &amp;lt;param name=&quot;lat&quot;&amp;gt;纬度(X，100多那个)&amp;lt;/param&amp;gt;
    /// &amp;lt;param name=&quot;lng&quot;&amp;gt;经度(Y，30多那个)&amp;lt;/param&amp;gt;
    /// &amp;lt;returns&amp;gt;&amp;lt;/returns&amp;gt;
    public static (double lat, double lng) Gcj02ToBd09(double lat, double lng)
    {
        double x = lng, y = lat;
        double z = Math.Sqrt(x * x + y * y) + 0.00002 * Math.Sin(y * X_PI);
        double theta = Math.Atan2(y, x) + 0.000003 * Math.Cos(x * X_PI);
        double bdLng = z * Math.Cos(theta) + 0.0065;
        double bdLat = z * Math.Sin(theta) + 0.006;
        return (bdLat, bdLng);
    }

    /// &amp;lt;summary&amp;gt;
    /// 将BD09坐标转换为GCJ02坐标（百度坐标转国测局坐标）
    /// &amp;lt;/summary&amp;gt;
    /// &amp;lt;param name=&quot;lat&quot;&amp;gt;纬度(X，100多那个)&amp;lt;/param&amp;gt;
    /// &amp;lt;param name=&quot;lng&quot;&amp;gt;经度(Y，30多那个)&amp;lt;/param&amp;gt;
    /// &amp;lt;returns&amp;gt;&amp;lt;/returns&amp;gt;
    public static (double lat, double lng) Bd09ToGcj02(double lat, double lng)
    {
        double x = lng - 0.0065, y = lat - 0.006;
        double z = Math.Sqrt(x * x + y * y) - 0.00002 * Math.Sin(y * X_PI);
        double theta = Math.Atan2(y, x) - 0.000003 * Math.Cos(x * X_PI);
        double ggLng = z * Math.Cos(theta);
        double ggLat = z * Math.Sin(theta);
        return (ggLat, ggLng);
    }

    #endregion

    #region WGS84 &amp;lt;=&amp;gt; BD09 (组合)

    /// &amp;lt;summary&amp;gt;
    /// 将WGS84坐标转换为BD09坐标（百度坐标）
    /// &amp;lt;/summary&amp;gt;
    /// &amp;lt;param name=&quot;lat&quot;&amp;gt;纬度(X，100多那个)&amp;lt;/param&amp;gt;
    /// &amp;lt;param name=&quot;lng&quot;&amp;gt;经度(Y，30多那个)&amp;lt;/param&amp;gt;
    /// &amp;lt;returns&amp;gt;&amp;lt;/returns&amp;gt;
    public static (double lat, double lng) Wgs84ToBd09(double lat, double lng)
    {
        var (gLat, gLng) = Wgs84ToGcj02(lat, lng);
        return Gcj02ToBd09(gLat, gLng);
    }

    /// &amp;lt;summary&amp;gt;
    /// 将BD09坐标转换为WGS84坐标（GPS坐标）
    /// &amp;lt;/summary&amp;gt;
    /// &amp;lt;param name=&quot;lat&quot;&amp;gt;纬度(X，100多那个)&amp;lt;/param&amp;gt;
    /// &amp;lt;param name=&quot;lng&quot;&amp;gt;经度(Y，30多那个)&amp;lt;/param&amp;gt;
    /// &amp;lt;returns&amp;gt;&amp;lt;/returns&amp;gt;
    public static (double lat, double lng) Bd09ToWgs84(double lat, double lng)
    {
        var (gLat, gLng) = Bd09ToGcj02(lat, lng);
        return Gcj02ToWgs84(gLat, gLng);
    }

    #endregion
}

&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>使用Docker自动编译发布Net Core项目</title><link>https://wc.sb/posts/docker-auto-build-publish-dotnet/</link><guid isPermaLink="true">https://wc.sb/posts/docker-auto-build-publish-dotnet/</guid><pubDate>Tue, 18 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;前言&lt;/h1&gt;
&lt;p&gt;项目是使用的&lt;code&gt;Net Core 2.2&lt;/code&gt;的老项目，而编译服务器是&lt;code&gt;Debian12&lt;/code&gt;，&lt;code&gt;Debian12&lt;/code&gt;已不再支持&lt;code&gt;Net Core 2.2 SDK&lt;/code&gt;所以无法直接编译，故使用Docker实现编译项目。&lt;/p&gt;
&lt;h1&gt;环境介绍&lt;/h1&gt;
&lt;p&gt;系统版本：Debian GNU/Linux 12 (bookworm)
代码管理：Gitea 23.0.0
代码集成：Jenkins 2.492.1&lt;/p&gt;
&lt;h1&gt;Jenkins 配置&lt;/h1&gt;
&lt;p&gt;&lt;s&gt;主要是用来触发编译，感觉随便用一个WebHook就可以&lt;/s&gt;&lt;/p&gt;
&lt;h2&gt;1、安装必要插件&lt;/h2&gt;
&lt;p&gt;在&lt;code&gt;系统管理-&amp;gt;插件管理-&amp;gt;Available plugins&lt;/code&gt;中搜索&lt;code&gt;Generic Webhook Trigger Plugin&lt;/code&gt;并安装
&lt;img src=&quot;image1.png&quot; alt=&quot;安装插件&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;2、新建任务流程&lt;/h2&gt;
&lt;p&gt;新建一个自由风格的任务流程
&lt;img src=&quot;image2.png&quot; alt=&quot;新建任务&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;3、配置任务流程&lt;/h2&gt;
&lt;p&gt;在&lt;code&gt;源码管理&lt;/code&gt;中配置代码仓库
&lt;img src=&quot;image3.png&quot; alt=&quot;配置代码仓库&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在&lt;code&gt;Triggers&lt;/code&gt;中勾选&lt;code&gt;Generic Webhook Trigger&lt;/code&gt;，并设置下方&lt;code&gt;Token&lt;/code&gt;，其他不用操作
&lt;img src=&quot;image4.png&quot; alt=&quot;触发器&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在&lt;code&gt;Build Steps&lt;/code&gt;中添加&lt;code&gt;执行 shell&lt;/code&gt;步骤，用来执行命令(也可以直接写，不过我还是喜欢单独写文件中执行)
&lt;img src=&quot;image5.png&quot; alt=&quot;添加构建&quot; /&gt;&lt;/p&gt;
&lt;p&gt;填写完毕后保存。&lt;/p&gt;
&lt;h1&gt;Gitea 配置&lt;/h1&gt;
&lt;p&gt;配置&lt;code&gt;webhook&lt;/code&gt;，触发&lt;code&gt;jenkins&lt;/code&gt;构建&lt;/p&gt;
&lt;h2&gt;1、添加Web钩子&lt;/h2&gt;
&lt;p&gt;在仓库中的&lt;code&gt;设置-&amp;gt;Web钩子&lt;/code&gt;中，点击右上角&lt;code&gt;Web钩子&lt;/code&gt;按钮，选择&lt;code&gt;Gitea&lt;/code&gt;添加；
配置目标Url，格式为&lt;code&gt;Jenkins地址&lt;/code&gt; + &lt;code&gt;__generic-webhook-trigger/invoke?token=__&lt;/code&gt; + &lt;code&gt;Token&lt;/code&gt;，例如&lt;code&gt;http://192.168.1.100:8080/generic-webhook-trigger/invoke?token=123456&lt;/code&gt;
&lt;img src=&quot;image6.png&quot; alt=&quot;添加Web钩子&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;脚本编写&lt;/h1&gt;
&lt;p&gt;编写&lt;code&gt;Dockerfile&lt;/code&gt;与&lt;code&gt;编译脚本&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;Dockerfile文件&lt;/h2&gt;
&lt;p&gt;下述流程先使用&lt;code&gt;SDK&lt;/code&gt;进行编译发布，然后在&lt;code&gt;RunTime&lt;/code&gt;中运行项目；
因为微软官方提供的&lt;code&gt;RunTime&lt;/code&gt;并无&lt;code&gt;libgdiplus&lt;/code&gt;，所以项目中使用图形的地方都会报错，这里咱们修改下镜像源，手动安装&lt;code&gt;libgdiplus&lt;/code&gt;，如果项目还需安装其他的，自行追加安装即可。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 使用 .NET Core SDK 镜像来构建项目
FROM mcr.microsoft.com/dotnet/core/sdk:2.2 AS build

# 设置工作目录
WORKDIR /app

# 复制文件
COPY . ./

# 恢复 Test_Project 项目的依赖项
RUN dotnet restore Test_Project.csproj

# 发布 Test_Project 项目
RUN dotnet publish Test_Project.csproj -c Release -o /app/publish

# 使用 .NET Core 运行时镜像来运行应用程序
FROM mcr.microsoft.com/dotnet/core/aspnet:2.2 AS base

# 设置工作目录
WORKDIR /app

# 暴漏端口
EXPOSE 11081

# 修复软件源并安装 libgdiplus
RUN echo &quot;deb http://mirrors.tuna.tsinghua.edu.cn/debian-elts stretch main contrib non-free&quot; &amp;gt; /etc/apt/sources.list \
    &amp;amp;&amp;amp; curl -fsSL https://deb.freexian.com/extended-lts/archive-key.gpg -o /tmp/elts-archive-key.gpg \
    &amp;amp;&amp;amp; mv /tmp/elts-archive-key.gpg /etc/apt/trusted.gpg.d/freexian-archive-extended-lts.gpg \
    &amp;amp;&amp;amp; apt-get update &amp;amp;&amp;amp; apt-get install -y libgdiplus \
    &amp;amp;&amp;amp; apt-get clean \
    &amp;amp;&amp;amp; rm -rf /var/lib/apt/lists/*

# 将发布后的 Web API 文件复制到运行时容器中
COPY --from=build /app/publish .

# 配置应用程序 URL
ENV ASPNETCORE_URLS=http://*:11081

# 设置容器的启动命令
ENTRYPOINT [&quot;dotnet&quot;, &quot;Test_Project.dll&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;编译脚本&lt;/h2&gt;
&lt;p&gt;下述脚本主要是将配置文件&lt;code&gt;appsettings.json&lt;/code&gt;和&lt;code&gt;Dockerfile&lt;/code&gt;复制到&lt;code&gt;Jenkins&lt;/code&gt;编译的项目中(Jenkens需映射宿主机项目路径)，然后构建新镜像并删除旧容器(如果有)，最后运行新容器；
启动容器时我做了重启策略&lt;code&gt;--restart=on-failure:3&lt;/code&gt;，运行报错退出后自动重启，重启次数最多3次，然后映射资源目录&lt;code&gt;wwwroot&lt;/code&gt;和日志目录&lt;code&gt;logs&lt;/code&gt;，便于直接在宿主机上查看。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/bin/bash

set -e

# 变量
SRC_DIR=&quot;/home/project/Test_Project&quot;
DEST_DIR=&quot;.&quot;
DOCKER_IMAGE=&quot;test_project&quot;
DOCKER_CONTAINER=&quot;Test_Project&quot;
PORT_MAPPING=&quot;11081:11081&quot;

# 检查目录和文件
[ -d &quot;$SRC_DIR&quot; ] || { echo &quot;错误: 目录 $SRC_DIR 不存在！&quot;; exit 1; }
[ -f &quot;$SRC_DIR/Dockerfile&quot; ] || { echo &quot;错误: Dockerfile 不存在！&quot;; exit 1; }
mkdir -p &quot;$DEST_DIR&quot;
[ -f &quot;$SRC_DIR/appsettings.json&quot; ] &amp;amp;&amp;amp; cp &quot;$SRC_DIR/appsettings.json&quot; &quot;$DEST_DIR/&quot;

# 确保 Api 目录结构完整
mkdir -p &quot;$SRC_DIR/Api/wwwroot&quot; &quot;$SRC_DIR/Api/logs&quot;

# 构建 Docker 镜像
cp &quot;$SRC_DIR/Dockerfile&quot; .
docker build -t &quot;$DOCKER_IMAGE&quot; .

# 移除旧容器
docker rm -f &quot;$DOCKER_CONTAINER&quot; 2&amp;gt;/dev/null || true

# 运行新容器
docker run -d -p &quot;$PORT_MAPPING&quot; --name &quot;$DOCKER_CONTAINER&quot; \
  --restart=on-failure:3 \
  -v &quot;$SRC_DIR/Api/wwwroot:/app/wwwroot&quot; \
  -v &quot;$SRC_DIR/Api/logs:/app/logs&quot; \
  &quot;$DOCKER_IMAGE&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;处理流程&lt;/h1&gt;
&lt;p&gt;提交代码 -&amp;gt; Gitea WebHook触发Jenkins构建 -&amp;gt; Jenkins拉取Gitea代码 -&amp;gt; Jenkins执行编译脚本 -&amp;gt;脚本复制配置文件与Dockerfile -&amp;gt; 脚本使用Dockerfile内容构建镜像 -&amp;gt; Dockerfile先使用SDK进行编译发布 -&amp;gt; Docker使用AspNet作为运行环境并构建镜像 -&amp;gt; 脚本删除旧容器(如果有) -&amp;gt;脚本启动新容器&lt;/p&gt;
</content:encoded></item></channel></rss>