节点在线、应用在线、配置在线使用令牌查询
大石头 authored at 2021-12-16 19:49:30
5.28 KiB
Stardust
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using NewLife;
using NewLife.Cube;
using NewLife.Cube.Extensions;
using NewLife.Serialization;
using NewLife.Web;
using Stardust.Data.Configs;

namespace Stardust.Web.Areas.Configs.Controllers;

/// <summary>配置历史。记录配置的变更历史,支持版本追溯和回滚检查</summary>
[Menu(0, false)]
[ConfigsArea]
public class ConfigHistoryController : ConfigsEntityController<ConfigHistory>
{
    static ConfigHistoryController()
    {
        // 日志列表需要显示详细信息
        ListFields.AddDataField("Remark", null, "TraceId");

        //{
        //    var df = ListFields.GetField("TraceId") as ListField;
        //    df.DisplayName = "跟踪";
        //    df.Url = StarHelper.BuildUrl("{TraceId}");
        //    df.DataVisible = e => e is ConfigHistory entity && !entity.TraceId.IsNullOrEmpty();
        //}

        // 纯 API 后端:MVC 版 ListFields.TraceUrl() 依赖 NewLife.Cube.Web 的 StarUrlExtend,
        // 本地 NewLife.Cube(API版) 未提供该扩展,且 API 响应无需生成链路追踪 Url,故移除。
    }

    /// <summary>高级搜索。按条件分页查询</summary>
    /// <param name="p">分页参数</param>
    /// <returns>实体列表</returns>
    protected override IEnumerable<ConfigHistory> Search(Pager p)
    {
        var configId = p["configId"].ToInt(-1);
        var action = p["action"];
        var success = p["success"]?.ToBoolean();

        var start = p["dtStart"].ToDateTime();
        var end = p["dtEnd"].ToDateTime();

        return ConfigHistory.Search(configId, action, success, start, end, p["Q"], p);
    }

    /// <summary>获取配置历史与上一版本的差异。上一版为同配置更早的最近一次发布历史</summary>
    /// <param name="id">历史编号</param>
    /// <returns>返回Json,包含id、configId、newJson(本条Remark)、oldJson(上一版Remark,无则为null)、changedKeys(两JSON顶层键差异数组)</returns>
    public ActionResult Diff(Int32 id)
    {
        var hi = ConfigHistory.FindById(id);
        if (hi == null) return JsonRefresh("历史不存在!");

        // 找同一配置更早的最近一次发布历史,作为上一版本
        // 条件必须下推到数据库,避免 FindAll() 全表加载后在内存中过滤
        var old = ConfigHistory.FindAll(ConfigHistory._.ConfigId == hi.ConfigId & ConfigHistory._.Action == "Publish" & ConfigHistory._.CreateTime < hi.CreateTime)
            .OrderByDescending(e => e.CreateTime)
            .FirstOrDefault();

        var newJson = hi.Remark;
        var oldJson = old?.Remark;

        return Json(0, null, new
        {
            id = hi.Id,
            configId = hi.ConfigId,
            newJson,
            oldJson,
            changedKeys = GetDiffKeys(oldJson, newJson),
        }, 0);
    }

    /// <summary>回滚到历史版本。仅发布历史支持;恢复应用配置版本并重发布配置数据</summary>
    /// <param name="id">历史编号</param>
    public ActionResult Rollback(Int32 id)
    {
        var hi = ConfigHistory.FindById(id);
        if (hi == null) return JsonRefresh("历史不存在!");
        if (!hi.Action.EqualIgnoreCase("Publish")) return JsonRefresh("仅发布历史支持回滚!");

        var cfg = AppConfig.FindById(hi.ConfigId);
        if (cfg == null) return JsonRefresh("应用配置不存在!");

        var snap = hi.Remark.ToJsonEntity<AppConfig>();
        if (snap == null) return JsonRefresh("历史快照解析失败!");

        // 回滚到该次 Publish 引入的版本(NextVersion),无效(<=0)时用快照版本兜底
        cfg.Version = snap.NextVersion > 0 ? snap.NextVersion : snap.Version;
        cfg.NextVersion = snap.NextVersion;
        cfg.Update();

        ConfigHistory.Add(cfg.Id, "Rollback", true, cfg.ToJson());

        var rs = ConfigData.Publish(ConfigData.FindAllByApp(cfg.Id), cfg.Version);

        return JsonRefresh($"已回滚到版本 {cfg.Version},重发布 {rs} 项");
    }

    /// <summary>比较两个JSON的顶层键差异。包含新增、删除或值变化的键,按键名排序</summary>
    /// <param name="oldJson">上一版JSON</param>
    /// <param name="newJson">当前JSON</param>
    /// <returns>差异键数组</returns>
    static String[] GetDiffKeys(String oldJson, String newJson)
    {
        var oldDic = DecodeJson(oldJson);
        var newDic = DecodeJson(newJson);

        if (oldDic == null || oldDic.Count == 0) return newDic?.Keys.OrderBy(e => e).ToArray() ?? new String[0];
        if (newDic == null || newDic.Count == 0) return oldDic.Keys.OrderBy(e => e).ToArray();

        return oldDic.Keys.Union(newDic.Keys)
            .Where(e => !oldDic.ContainsKey(e) || !newDic.ContainsKey(e) || !String.Equals(oldDic[e]?.ToJson(), newDic[e]?.ToJson()))
            .OrderBy(e => e)
            .ToArray();
    }

    /// <summary>解析JSON为顶层字典</summary>
    /// <param name="json">JSON字符串</param>
    /// <returns>解析失败返回null</returns>
    static IDictionary<String, Object> DecodeJson(String json)
    {
        if (json.IsNullOrEmpty()) return null;

        try
        {
            return new JsonParser(json).Decode() as IDictionary<String, Object>;
        }
        catch
        {
            return null;
        }
    }
}