博客 (36)

首先我们要获取公众号的“__biz”值

在电脑浏览器上打开该公众号的任何一篇历史文章,在源代码中搜索“__biz=”就可以找到,该值以“==”结尾,例如找到:

__biz=MzIwNDcwODQ2NQ==

那么就可以拼成网址:(只能在微信中打开)

https://mp.weixin.qq.com/mp/profile_ext?action=home&__biz=MzIwNDcwODQ2NQ==#wechat_redirect

早期这个地址是这样的:

https://mp.weixin.qq.com/mp/getmasssendmsg?__biz=MzIwNDcwODQ2NQ==#wechat_redirect

现在它重定向到第一个地址去了,所以直接用新的就可以了。

若提示“页面无法打开”,可能的原因是没有加“#wechat_redirect”。

xoyozo 1 年前
1,187

以 ASP.NET 6 返回 BadRequestObjectResult 为例。

public IActionResult Post()
{
    return BadRequest("友好的错误提示语");
}


axios

请求示例:

axios.post(url, {})
.then(function (response) {
    console.log(response.data);
})
.catch(function (error) {
    console.log(error.response.data);
});

error 对象:

{
  "message": "Request failed with status code 400",
  "name": "AxiosError",
  "code": "ERR_BAD_REQUEST",
  "status": 400,
  "response": {
    "data": "友好的错误提示语",
    "status": 400
  }
}


jQuery

请求示例:

$.ajax 对象形式

$.ajax({
    url: url,
    type: 'POST',
    data: {},
    success: function (data) {
        console.log(data)
    },
    error: function (jqXHR) {
        console.log(jqXHR.responseText)
    },
    complete: function () {
    },
});

$.ajax 事件处理形式(Event Handlers)jQuery 3+

$.ajax({
    url: url,
    type: 'POST',
    data: {},
})
    .done(function (data) {
        console.log(data)
    })
    .fail(function (jqXHR) {
        console.log(jqXHR.responseText)
    })
    .always(function () {
    });

$.get / $.post / $.delete

$.post(url, {}, function (data) {
    console.log(data)
})
    .done(function (data) {
        console.log(data)
    })
    .fail(function (jqXHR) {
        console.log(jqXHR.responseText)
    })
    .always(function () {
    });

jqXHR 对象:

{
  "readyState": 4,
  "responseText": "友好的错误提示语",
  "status": 400,
  "statusText": "error"
}


xoyozo 2 年前
1,465

第一步,创建用户和 AccessKey

登录阿里云控制台后,从头像处进入 AccessKey 管理

image.png

“开始使用子用户 AccessKey”

image.png

点击“创建用户”,填写用户名(本文以 oss-bbs 为例),并勾选“Open API 调用访问”

image.png

点击确定创建成功,可以看到 AccessKey ID 和 AccessKey Secret


第二步,创建权限策略

在阿里云控制台左侧菜单“RAM 访问控制”中展开“权限管理”,选择“权限策略”

image.png

点击“创建权限策略”,切换到“脚本编辑”

image.png

输入以下策略文档(JSON 格式)

{
    "Version": "1",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                    "oss:ListBuckets",
                    "oss:GetBucketStat",
                    "oss:GetBucketInfo",
                    "oss:GetBucketAcl" 
                      ],    
            "Resource": "acs:oss:*:*:*"
        },
        {
            "Effect": "Allow",
            "Action": [
                    "oss:Get*",
                    "oss:Put*",
                    "oss:List*",
                    "oss:DeleteObject"
                    ],
            "Resource": "acs:oss:*:*:bbs"
        },
        {
            "Effect": "Allow",
            "Action": [
                    "oss:Get*",
                    "oss:Put*",
                    "oss:List*",
                    "oss:DeleteObject"
            ],
            "Resource": "acs:oss:*:*:bbs/*"
        }
    ]
}

脚本中第一段表示允许用户登录,第二段表示允许操作 Bucket,第三段表示允许操作 Bucket 内的文件对象。

示例中的 bbs 是 Bucket Name,请修改为自己的 Bucket 名称。

点击下一步,填写名称,确定即可。


第三步,添加用户权限

转到“用户”页面,在刚才创建的用户行“添加权限”

image.png

切换到“自定义策略”,将上一步创建的权限策略添加到右侧“已选择”区

image.png

确定。


相关信息:

You have no right to access this object because of bucket acl.

xoyozo 2 年前
1,997
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"
	}]
}]


xoyozo 2 年前
1,460

POST

axios.post('AjaxCases', {
    following: true,
    success: null,
    page: 1,
}).then(function (response) {
    console.log(response.data)
}).catch(function (error) {
    console.log(error.response.data);
});
[HttpPost]
public IActionResult AjaxCases([FromBody] AjaxCasesRequestModel req)
{
    bool? following = req.following;
    bool? success = req.success;
    int page = req.page;
}

GET

axios.get('AjaxCase', {
    int: 123,
}).then(function (response) {
    console.log(response.data)
}).catch(function (error) {
    console.log(error.response.data);
});
[HttpGet]
public IActionResult AjaxCases([FromQuery] int id)
{
}


* 若服务端获取参数值为 null,可能的情况如下:

  • 请检查相关枚举类型是否已设置 [JsonConverter] 属性,参:C# 枚举(enum)用法笔记,且传入的值不能为空字符串,可以传入 null 值,并将服务端 enum 类型改为可空。可以以 string 方式接收参数后再进行转换。

  • 模型中属性名称不能重复(大小写不同也不行)。

  • 布尔型属性值必须传递布尔型值,即在 JS 中,字符串 "true" 应 JSON.parse("true") 为 true 后再回传给服务端。

xoyozo 2 年前
1,922

Nuget 包:System.Runtime.Caching

依赖注入:

public class HomeController : Controller
{
    private IMemoryCache _cache;

    public HomeController(IMemoryCache memoryCache)
    {
        _cache = memoryCache;
    }

定义键:

public static class CacheKeys
{
    public static string Entry { get { return "_Entry"; } }
}

赋值与取值:

public IActionResult CacheTryGetValueSet()
{
    DateTime cacheEntry;

    // 尝试从缓存获取,若获取失败则重新赋值
    if (!_cache.TryGetValue(CacheKeys.Entry, out cacheEntry))
    {
        // 新的内容
        cacheEntry = DateTime.Now;

        // 缓存选项
        var cacheEntryOptions = new MemoryCacheEntryOptions()
            // Keep in cache for this time, reset time if accessed.
            .SetAbsoluteExpiration(TimeSpan.FromSeconds(3));

        // 保存到缓存中
        _cache.Set(CacheKeys.Entry, cacheEntry, cacheEntryOptions);
    }

    return View("Cache", cacheEntry);
}

注意:

.SetAbsoluteExpiration() 用于设置绝对过期时间,它表示只要时间一到就过期

.SetSlidingExpiration() 用于设置可调过期时间,它表示当离最后访问超过某个时间段后就过期

xoyozo 3 年前
1,859

通过 nuget 安装 UEditorNetCore

从 UEditor 官网 下载最新的包 ueditorx_x_x_x-utf8-net.zip

解压包,并复制到项目的 wwwroot/lib 目录下,删除 net 目录。

根据 UEditorNetCore 官方的使用说明进行操作

步骤中,控制器 UEditorController 接替原 controller.ashx 的功能,用于统一处理上传逻辑

原 config.json 复制到项目根目录,用于配置上传相关设置(若更改文件名和路径在 services.AddUEditorService() 中处理)。

个人喜欢将 xxxPathFormat 值改为:upload/ueditor/{yyyy}{mm}{dd}/{time}{rand:6},方便日后迁移附件后进行批量替换。

记得配置 catcherLocalDomain 项。


上传相关的身份验证和授权控制请在 UEditorController 中处理,如:

public void Do()
{
    if (用户未登录) { throw new System.Exception("请登录后再试"); }
    _ue.DoAction(HttpContext);
}


如果图片仅仅上传到网站目录下,那么这样配置就结束了,如果要上传到阿里云 OSS 等第三方图床,那么继续往下看。


因 UEditorNetCore 虽然实现了上传到 FTP 和 OSS 的功能,但未提供配置相关的账号信息的途径,所以只能(1)重写 action,或(2)下载 github 的源代码,将核心项目加入到您的项目中进行修改。

重写 action 不方便后期维护,这里记录修改源码的方式。

将源码改造为可配置账号的上传到 OSS 的功能,具体步骤如下:

  1. 将 Consts.cs 从项目中排除。

  2. 打开 Handlers/UploadHandler.cs,找到 UploadConfig 类,将

    FtpUpload 改为 OssUpload,

    FtpAccount 改为 OssAccessKeyId,

    FtpPwd 改为 OssAccessKeySecret,

    FtpIp 改为 OssAccessEndpoint,

    再添加一个属性 OssBucketName。

  3. 打开 Handlers/UploadHandler.cs,找到 Process() 方法, 

    将 UploadConfig.FtpUpload 改为 UploadConfig.OssUpload,

    将 Consts.AliyunOssServer.* 改为 UploadConfig.Oss*。

  4. 打开 Handlers/CrawlerHandler.cs,找到 Fetch() 方法,

    将 Config.GetValue<bool>("catcherFtpUpload") 改为 Config.GetValue<bool>("OssUpload"),

    将 Consts.AliyunOssServer.* 改为 Config.GetString("Oss*")。

  5. 打开 UEditorActionCollection.cs,找到 UploadImageAction,将

    FtpUpload = Config.GetValue<bool>("imageFtpUpload"),
    FtpAccount = Consts.ImgFtpServer.account,
    FtpPwd = Consts.ImgFtpServer.pwd,
    FtpIp = Consts.ImgFtpServer.ip,

    替换为

    OssUpload = Config.GetValue<bool>("OssUpload"),
    OssAccessKeyId = Config.GetString("OssAccessKeyId"),
    OssAccessKeySecret = Config.GetString("OssAccessKeySecret"),
    OssAccessEndpoint = Config.GetString("OssAccessEndpoint"),
    OssBucketName = Config.GetString("OssBucketName"),

    其余 3 个 Action(UploadScrawlAction、UploadVideoAction、UploadFileAction)按同样的方式修改。

    在所有创建 UploadHandler 对象时补充添加 SaveAbsolutePath 属性。

  6. 打开 config.json,添加相关配置项(注:配置文件中的 *FtpUpload 全部废弃,统一由 OssUpload 决定)

    "OssUpload": true,
    "OssAccessEndpoint": "",
    "OssAccessKeyId": "",
    "OssAccessKeySecret": "",
    "OssBucketName": "",

    将 xxxUrlPrefix 的值改为 OSS 对应的 CDN 网址(以 / 结尾,如://cdn.xoyozo.net/)。


其它注意点:

  • 若使用 UEditorNetCore github 提供的源代码类库代替 nuget,且使用本地存储,那么需要将 Handlers 目录中与定义 savePath 相关的代码(查找  var savePath)替换成被注释的行

xoyozo 3 年前
3,545

本文适用于 Window,CentOS(Linux) 系统请移步:http://xoyozo.net/Blog/Details/inotify-tools


FileSystemWatcher

关于监视文件系统事件的介绍

https://docs.microsoft.com/zh-cn/previous-versions/visualstudio/visual-studio-2008/ch2s8yd7(v=vs.90)


以控制台应用程序为例:

using System;
using System.IO;
using System.Security.Permissions;

public class Watcher
{
    public static void Main()
    {
        Run();
    }

    [PermissionSet(SecurityAction.Demand, Name = "FullTrust")]
    private static void Run()
    {
        string[] args = Environment.GetCommandLineArgs();

        // If a directory is not specified, exit program.
        if (args.Length != 2)
        {
            // Display the proper way to call the program.
            Console.WriteLine("Usage: Watcher.exe (directory)");
            return;
        }

        // Create a new FileSystemWatcher and set its properties.
        using (FileSystemWatcher watcher = new FileSystemWatcher())
        {
            watcher.Path = args[1];

            // Watch for changes in LastAccess and LastWrite times, and
            // the renaming of files or directories.
            watcher.NotifyFilter = NotifyFilters.LastAccess
                                 | NotifyFilters.LastWrite
                                 | NotifyFilters.FileName
                                 | NotifyFilters.DirectoryName;

            // Only watch text files.
            watcher.Filter = "*.txt";

            // Add event handlers.
            watcher.Changed += OnChanged;
            watcher.Created += OnChanged;
            watcher.Deleted += OnChanged;
            watcher.Renamed += OnRenamed;

            // Begin watching.
            watcher.EnableRaisingEvents = true;

            // Wait for the user to quit the program.
            Console.WriteLine("Press 'q' to quit the sample.");
            while (Console.Read() != 'q') ;
        }
    }

    // Define the event handlers.
    private static void OnChanged(object source, FileSystemEventArgs e) =>
        // Specify what is done when a file is changed, created, or deleted.
        Console.WriteLine($"File: {e.FullPath} {e.ChangeType}");

    private static void OnRenamed(object source, RenamedEventArgs e) =>
        // Specify what is done when a file is renamed.
        Console.WriteLine($"File: {e.OldFullPath} renamed to {e.FullPath}");
}


xoyozo 4 年前
2,591

前端引用 jquery、jquery-ui(或 jquery.ui.widget),以及 jquery.fileupload.js

$('#xxx').fileupload({
    url: 'FileUpload',
    add: function (e, data) {
        console.log('add', data);
        data.submit();
    },
    success: function (response, status) {
        console.log('success', response);
        toastr.success('上传成功!文件名:' + response);
    },
    error: function (error) {
        console.log('error', error);
        toastr.error("上传失败:" + error.responseText);
    }
});

服务端接收文件:

[HttpPost]
public ActionResult FileUpload([FromForm(Name = "files[]")] IFormFile file)
{
    try
    {
        return Ok(file.FileName);
    }
    catch(Exception ex)
    {
        return BadRequest(ex.Message);
    }
}

此处 Name = "files[]" 值是由 <input type="file" name="这里定的" />

如果 input 没有指定 name,那么一般为 file[],或者在 Chrome 的开发者工具中,切换到 Network 标签页,选中 post 的目标请求,在 Headers 的 Form Data 中可以看到 name="xxx"。

xoyozo 4 年前
3,392

ASP.NET MVC 使用 Authorize 过滤器验证用户登录。Authorize 过滤器首先运行在任何其它过滤器或动作方法之前,主要用来做登录验证或者权限验证。

示例:使用 Authorize 过滤器实现简单的用户登录验证。

1、创建登录控制器 LoginController

/// <summary>
/// 登录控制器
/// </summary>
[AllowAnonymous]
public class LoginController : Controller
{
    /// <summary>
    /// 登录页面
    /// </summary>
    public ActionResult Index()
    {
        return View();
    }
 
    /// <summary>
    /// 登录
    /// </summary>
    [HttpPost]
    public ActionResult Login(string loginName, string loginPwd)
    {
        if (loginName == "admin" && loginPwd == "123456")
        {
            // 登录成功
            Session["LoginName"] = loginName;
            return RedirectToAction("Index", "Home");
        }
        else
        {
            // 登录失败
            return RedirectToAction("Index", "Login");
        }
    }
 
    /// <summary>
    /// 注销
    /// </summary>
    public ActionResult Logout()
    {
        Session.Abandon();
        return RedirectToAction("Index", "Login");
    }
}

注意:在登录控制器 LoginController 上添加 AllowAnonymous 特性,该特性用于标记在授权期间要跳过 AuthorizeAttribute 的控制器和操作。

2、创建登录页面

@{
    ViewBag.Title = "登录页面";
    Layout = null;
}
 
<h2>登录页面</h2>
 
<form action='@Url.Action("Login","Login")' id="form1" method="post">
    用户:<input type="text" name="loginName" /><br />
    密码:<input type="password" name="loginPwd" /><br />
    <input type="submit" value="登录">
</form>

效果图:

3、创建主页控制器 LoginController

public class HomeController : Controller
{
    public ActionResult Index()
    {
        // 获取当前登录用户
        string loginName = Session["LoginName"].ToString();
        ViewBag.Message = "当前登录用户:" + loginName;
        return View();
    }
}

4、创建主页页面

@{
    ViewBag.Title = "Index";
    Layout = null;
}
 
<h2>Index</h2>
<h3>@ViewBag.Message</h3>
<a href="@Url.Action("Logout","Login")">注销</a>

效果图:

5、创建授权过滤器 LoginAuthorizeAttribute 类

创建 Filter 目录,在该目录下创建授权过滤器 LoginAuthorizeAttribute 类,继承 AuthorizeAttribute。

using System.Web.Mvc;
 
namespace MvcApp.Filter
{
    /// <summary>
    /// 授权过滤器
    /// </summary>
    public class LoginAuthorizeAttribute : AuthorizeAttribute
    {
        public override void OnAuthorization(AuthorizationContext filterContext)
        {
            // 判断是否跳过授权过滤器
            if (filterContext.ActionDescriptor.IsDefined(typeof(AllowAnonymousAttribute), true)
               || filterContext.ActionDescriptor.ControllerDescriptor.IsDefined(typeof(AllowAnonymousAttribute), true))
            {
                return;
            }
 
            // 判断登录情况
            if (filterContext.HttpContext.Session["LoginName"] == null || filterContext.HttpContext.Session["LoginName"].ToString()=="")
            {
                //HttpContext.Current.Response.Write("认证不通过");
                //HttpContext.Current.Response.End();
 
                filterContext.Result = new RedirectResult("/Login/Index");
            }
        }
    }
}

通常 Authorize 过滤器也是在全局过滤器上面的,在 App_Start 目录下的 FilterConfig 类的 RegisterGlobalFilters 方法中添加:

using System.Web;
using System.Web.Mvc;
using MvcApp.Filter;
 
namespace MvcApp
{
    public class FilterConfig
    {
        public static void RegisterGlobalFilters(GlobalFilterCollection filters)
        {
            filters.Add(new HandleErrorAttribute());
 
            // 添加全局授权过滤器
            filters.Add(new LoginAuthorizeAttribute());
        }
    }
}

Global.asax 下的代码:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Optimization;
using System.Web.Routing;
 
namespace MvcApp
{
    public class MvcApplication : System.Web.HttpApplication
    {
        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();
            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
            RouteConfig.RegisterRoutes(RouteTable.Routes);
            BundleConfig.RegisterBundles(BundleTable.Bundles);
        }
    }
}


xoyozo:暂时没有按区域(Area)来统一配置的方法,建议加在 Controller 上。https://stackoverflow.com/questions/2319157/how-can-we-set-authorization-for-a-whole-area-in-asp-net-mvc


p
转自 pan_junbiao 4 年前
2,838