fix(billing): 插件 HTTP 路由按操作拆路径 + 正确解析请求体

- 内核插件路由冲突仲裁按**路径**判重(不看 method),同一路径挂 4 个 method 会被
  当成 4 个插件抢同一路径 ⇒ 规则 API 不可用。拆成 /rules(GET)、/rules/replace
  (PUT)、/rules/add(POST)、/rules/remove(DELETE)。
- 插件 HTTP 分发交给 handler 的是**整个请求表**(body 是原始字符串),而直调导出
  拿到的是参数表;此前 PUT 把请求信封当成规则写进了 billing.json,规则全丢、价格表
  清空。新增 reqData() 统一解出 JSON 载荷。
- 生产复验:GET 返回 4 条规则;PUT 后 billing.json 保持规则形状;localzen(free)
  请求不计入 unpriced,codearts 未列价模型计入(预期收紧)。
This commit is contained in:
root
2026-10-20 18:59:16 +08:00
parent 4b9fb7b056
commit 79bc55cfbe

View File

@ -344,6 +344,21 @@ do
if not res then plugin.load_error = tostring(err) end
end
-- 插件 HTTP 分发把**整个请求表**交给 handler(method/path/body/headers…),
-- body 是原始字符串;而直调导出(/call/...)拿到的就是参数表本身。这里统一
-- 解出 JSON 载荷,避免"把请求信封当成规则写进 billing.json"(真实踩过:
-- PUT 之后 billing.json 变成了整个请求的转储,规则全丢、价格表清空)。
local function reqData(req)
if type(req) ~= "table" then return {} end
if type(req.json) == "table" then return req.json end
if type(req.body) == "string" and req.body ~= "" then
local ok, dec = pcall(json.decode, req.body)
if ok and type(dec) == "table" then return dec end
end
if req.method == nil and req.path == nil then return req end
return {}
end
-- ---- 规则编辑器的 HTTP 面(挂在插件自己的 /api/plugins/http/billing/ 下)----
-- 规则属于插件,写盘也在插件内完成;内核没有 billing 端点。
function plugin.__http_rules_get(req)
@ -357,7 +372,7 @@ function plugin.__http_rules_get(req)
end
function plugin.__http_rules_put(req)
local body = (req and (req.json or req))
local body = reqData(req)
if type(body) ~= "table" then
return { status = 400, json = { error = "请求体必须是 JSON 对象" } }
end
@ -373,7 +388,7 @@ function plugin.__http_rules_put(req)
end
function plugin.__http_rules_add(req)
local body = (req and (req.json or req)) or {}
local body = reqData(req) or {}
local copy = json.decode(json.encode(plugin.rules or { active = "", profiles = {} }))
local target = nil
for _, p in ipairs(copy.profiles or {}) do
@ -405,7 +420,7 @@ function plugin.__http_rules_add(req)
end
function plugin.__http_rules_delete(req)
local body = (req and (req.json or req)) or {}
local body = reqData(req) or {}
local copy = json.decode(json.encode(plugin.rules or { active = "", profiles = {} }))
local target = nil
for _, p in ipairs(copy.profiles or {}) do
@ -446,14 +461,16 @@ function plugin.rules_put(args) return unwrap(plugin.__http_rules_put({ json = a
function plugin.rules_add(args) return unwrap(plugin.__http_rules_add({ json = args or {} })) end
function plugin.rules_delete(args) return unwrap(plugin.__http_rules_delete({ json = args or {} })) end
-- 每条路由的 path 必须唯一:内核的路由冲突仲裁按**路径**(不看 method)判重,
-- 同一路径挂 4 个 method 会被当成 4 个插件抢同一个路径。所以按操作拆路径。
plugin.http = {
{ path = "billing/rules", method = "GET", handler = "__http_rules_get",
roles = { "admin" }, description = "计费规则读取" },
{ path = "billing/rules", method = "PUT", handler = "__http_rules_put",
{ path = "billing/rules/replace", method = "PUT", handler = "__http_rules_put",
roles = { "admin" }, description = "替换整份计费规则(插件自己写盘)" },
{ path = "billing/rules", method = "POST", handler = "__http_rules_add",
{ path = "billing/rules/add", method = "POST", handler = "__http_rules_add",
roles = { "admin" }, description = "新增/替换一条计费规则" },
{ path = "billing/rules", method = "DELETE", handler = "__http_rules_delete",
{ path = "billing/rules/remove", method = "DELETE", handler = "__http_rules_delete",
roles = { "admin" }, description = "删除一条计费规则" },
}
@ -1522,8 +1539,9 @@ plugin.ui = {
return { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[c];
});
}
var OP_PATH = { GET: "", PUT: "/replace", POST: "/add", DELETE: "/remove" };
function call(method, body) {
return fetch(API, {
return fetch(API + (OP_PATH[method] || ""), {
method: method,
credentials: "same-origin",
headers: body ? { "Content-Type": "application/json" } : undefined,