How to FreeRadius and MySQL

本文详细介绍了如何在MySQL数据库中填充测试数据以支持FreeRadius认证系统。包括不同用户类型的配置示例,如动态分配IP的用户、静态IP用户及拨号路由器等。
英文原文:
Populating MySQL
You should now created some dummy data in the database to test against. It goes something like this:
In usergroup, put entries matching a user account name to a group name.
In radcheck, put an entry for each user account name with a 'Password' attribute with a value of their password.
In radreply, create entries for each user-specific radius reply attribute against their username
In radgroupreply, create attributes to be returned to all group members
Here's a dump of tables from the 'radius' database from mysql on my test box (edited slightly for clarity). This example includes three users, one with a dynamically assigned IP by the NAS (fredf), one assigned a static IP (barney), and one representing a dial-up routed connection (dialrouter):
mysql> select * from usergroup;
+----+---------------+-----------+
| id | UserName | GroupName |
+----+---------------+-----------+
| 1 | fredf | dynamic |
| 2 | barney | static |
| 2 | dialrouter | netdial |
+----+---------------+-----------+
3 rows in set (0.00 sec)

mysql> select * from radcheck;
+----+----------------+----------------+------------------+------+
| id | UserName | Attribute | Value | Op |
+----+----------------+----------------+------------------+------+
| 1 | fredf | Password | wilma | == |
| 2 | barney | Password | betty | == |
| 2 | dialrouter | Password | dialup | == |
+----+----------------+----------------+------------------+------+
3 rows in set (0.02 sec)

mysql> select * from radgroupcheck;

+----+------------+-------------------+---------------------+------+
| id | GroupName | Attribute | Value | Op |
+----+------------+-------------------+---------------------+------+
| 1 | dynamic | Auth-Type | Local | := |
| 2 | static | Auth-Type | Local | := |
| 3 | netdial | Auth-Type | Local | := |
+----+------------+-------------------+---------------------+------+
3 rows in set (0.01 sec)

mysql> select * from radreply;

+----+------------+-------------------+---------------------------------+------+
| id | UserName | Attribute | Value | Op |
+----+------------+-------------------+---------------------------------+------+
| 1 | barney | Framed-IP-Address | 1.2.3.4 | := |
| 2 | dialrouter | Framed-IP-Address | 2.3.4.1 | := |
| 3 | dialrouter | Framed-IP-Netmask | 255.255.255.255 | := |
| 4 | dialrouter | Framed-Routing | Broadcast-Listen | := |
| 5 | dialrouter | Framed-Route | 2.3.4.0 255.255.255.248 | := |
| 6 | dialrouter | Idle-Timeout | 900 | := |
+----+------------+-------------------+---------------------------------+------+
6 rows in set (0.01 sec)

mysql> select * from radgroupreply;
+----+-----------+--------------------+---------------------+------+
| id | GroupName | Attribute | Value | Op |
+----+-----------+--------------------+---------------------+------+
| 34 | dynamic | Framed-Compression | Van-Jacobsen-TCP-IP | := |
| 33 | dynamic | Framed-Protocol | PPP | := |
| 32 | dynamic | Service-Type | Framed-User | := |
| 35 | dynamic | Framed-MTU | 1500 | := |
| 37 | static | Framed-Protocol | PPP | := |
| 38 | static | Service-Type | Framed-User | := |
| 39 | static | Framed-Compression | Van-Jacobsen-TCP-IP | := |
| 41 | netdial | Service-Type | Framed-User | := |
| 42 | netdial | Framed-Protocol | PPP | := |
+----+-----------+--------------------+---------------------+------+
12 rows in set (0.01 sec)

mysql>

In this example, 'barney' (who is a single user dialup) only needs an attribute for IP address in radreply so he gets his static IP - he does not need any other attributes here as all the others get picked up from the 'static' group entries in radgroupreply.
'fred' needs no entries in radreply as he is dynamically assigned an IP via the NAS - so he'll just get the 'dynamic' group entries from radgroupreply ONLY.
'dialrouter' is a dial-up router, so as well as needing a static IP it needs route and mask attributes (etc) to be returned. Hence the additional entries.
'dialrouter' also has an idle-timeout attribute so the router gets kicked if it's not doing anything - you could add this for other users too if you wanted to. Of course, if you feel like or need to add any other attributes, that's kind of up to you!
Note the operator ('op') values used in the various tables. The password check attribute should use ==. Most return attributes should have a := operator, although if you're returning multiple attributes of the same type (e.g. multiple Cisco- AVpair's) you should use the += operator instead otherwise only the first one will be returned. Read the docs for more details on operators.
If you're stripping all domain name elements from usernames via realms, remember NOT to include the domain name elements in the usernames you put in the MySQL tables - they should get stripped BEFORE the database is checked, so name@domain will NEVER match if you're realm stripping (assuming you follow point 2 above) – you should just have 'name' as a user in the database. Once it's working without, and if you want more complex realm handling, go back to work out not stripping (and keeping name@domain in the db) if you really want to.
Using FreeRadius and MySQL
Fire up radiusd again in debug mode. The debug output should show it connecting to the MySQL database. Use radtest (or NTradPing) to test again - the user should authenticate and the debug output should show FreeRadius talking to MySQL.
You're done!

译文:
填充Mysql
你现在应该已经在数据库里创建了一些用于测试的数据了,它现在看起来应该是这样:
在uesrgroup中,有一个与用户帐号匹配的名字是group的名字的条目。
在radcheck中,有一个所有具有"password"属性的用户帐号名的密码的条目。
在radreply中,为每个具有密码属性的用户名以他们的密码创建一个条目。
在radgroupreply中,创建需要被所有组成员返回的属性。
这里有一个从我的测试工具箱(为了编辑的简便和清晰)里的"radius"数据库的存储表。这个例子包含了三个用户,一个是由NAS(fredf)动态分配IP,一个是被静态指定的IP(barney),还有一个是通过拨号路由连接的(dialrouter)。
mysql> select * from usergroup;
+----+---------------+-----------+
| id | UserName | GroupName |
+----+---------------+-----------+
| 1 | fredf | dynamic |
| 2 | barney | static |
| 2 | dialrouter | netdial |
+----+---------------+-----------+
3 rows in set (0.00 sec)

mysql> select * from radcheck;
+----+----------------+----------------+------------------+------+
| id | UserName | Attribute | Value | Op |
+----+----------------+----------------+------------------+------+
| 1 | fredf | Password | wilma | == |
| 2 | barney | Password | betty | == |
| 2 | dialrouter | Password | dialup | == |
+----+----------------+----------------+------------------+------+
3 rows in set (0.02 sec)

mysql> select * from radgroupcheck;

+----+------------+-------------------+---------------------+------+
| id | GroupName | Attribute | Value | Op |
+----+------------+-------------------+---------------------+------+
| 1 | dynamic | Auth-Type | Local | := |
| 2 | static | Auth-Type | Local | := |
| 3 | netdial | Auth-Type | Local | := |
+----+------------+-------------------+---------------------+------+
3 rows in set (0.01 sec)

mysql> select * from radreply;

+----+------------+-------------------+---------------------------------+------+
| id | UserName | Attribute | Value | Op |
+----+------------+-------------------+---------------------------------+------+
| 1 | barney | Framed-IP-Address | 1.2.3.4 | := |
| 2 | dialrouter | Framed-IP-Address | 2.3.4.1 | := |
| 3 | dialrouter | Framed-IP-Netmask | 255.255.255.255 | := |
| 4 | dialrouter | Framed-Routing | Broadcast-Listen | := |
| 5 | dialrouter | Framed-Route | 2.3.4.0 255.255.255.248 | := |
| 6 | dialrouter | Idle-Timeout | 900 | := |
+----+------------+-------------------+---------------------------------+------+
6 rows in set (0.01 sec)

mysql> select * from radgroupreply;
+----+-----------+--------------------+---------------------+------+
| id | GroupName | Attribute | Value | Op |
+----+-----------+--------------------+---------------------+------+
| 34 | dynamic | Framed-Compression | Van-Jacobsen-TCP-IP | := |
| 33 | dynamic | Framed-Protocol | PPP | := |
| 32 | dynamic | Service-Type | Framed-User | := |
| 35 | dynamic | Framed-MTU | 1500 | := |
| 37 | static | Framed-Protocol | PPP | := |
| 38 | static | Service-Type | Framed-User | := |
| 39 | static | Framed-Compression | Van-Jacobsen-TCP-IP | := |
| 41 | netdial | Service-Type | Framed-User | := |
| 42 | netdial | Framed-Protocol | PPP | := |
+----+-----------+--------------------+---------------------+------+
12 rows in set (0.01 sec)

mysql>
在这个例子中,"barney"(单个拨号用户)在redreply中仅仅需要被分配一个IP地址,因此他获得了一个静态IP-在这儿他不需要获得其他任何的分配了,因为其他所有的都通过radgroupreply中的'static'组条目来获得。
'fred'不需要任何的radgroup条目因为他是有NAS动态分配IP的-因此他只需要通过radgroupreply的'dyanmic'组入口获得就行了。
'dialrouter'是一个拨号路由器,因此他也需要一个静态IP,他需要返回路由和掩码属性,因此他需要其他的条目。
'dialrouter'也有一个超时属性,因此如果该路由器无响应的话它会被剔除掉,如果你愿意的话你也可以为其他用户也加上这个属性。当然,如果你喜欢或需要添加其他的属性,这完全取决于你。
需要注意的是操作符('op')这个值也被用在了变量表里面。密码检查属性应该使用=.大多数的返回变量都应该具有:=这个操作符,如果你要返回同种类型的多个属性(比如说多个cisco-AVpair),你应该使用+=操作符而不是其他仅仅用于返回第一个属性的任何操作符。你可以阅读文档来获取更多关于操作符的细节知识。
如果你通过域去掉了所有的用户名的域名元素,记得当你在Mysql表中输入用户名的时候不要包含域名—他们应该在检查数据库之前被去掉,因此如果你去掉了域名的话(假设你按照上面两条做了),name@domain将永远不会被匹配。你应该用'name'作为数据库里的用户。如果你不想这样做,或者你想使用复杂的域,那么返回并且不要去掉域名(并且保持name@domain在数据库里),前提是真的想这么做。
使用FreeRadius和MySQL
再一次让radiusd运行在debug模式。debug的输出可以显示它连接到MySQL。使用radtest(或者NTradPing)来再测一次-这个用户应该通过认证并且debug会显示出FreeRadius和MySQL的会话。
你完成了!
<!DOCTYPE html> <html> <head> <meta http-equiv="Content-type" content="text/html; charset=utf-8"> <title>高级甘特图 - 资源管理与日程分析</title> <script src="https://cdn.dhtmlx.com/gantt/edge/dhtmlxgantt.js"></script> <link href="https://cdn.dhtmlx.com/gantt/edge/dhtmlxgantt.css" rel="stylesheet"> <!-- 引入Chart.js用于数据显示 --> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <style type="text/css"> html, body { height: 100%; padding: 0px; margin: 0px; overflow: hidden; font-family: Arial, sans-serif; } .layout { display: flex; height: 100vh; } .sidebar { width: 300px; background: #f5f5f5; border-right: 1px solid #ddd; padding: 10px; overflow-y: auto; } .main-content { flex: 1; display: flex; flex-direction: column; } .toolbar { padding: 10px; background: #f0f0f0; border-bottom: 1px solid #ddd; display: flex; gap: 10px; } .view-tabs { display: flex; background: #e0e0e0; } .tab { padding: 10px 20px; cursor: pointer; border-right: 1px solid #ccc; } .tab.active { background: #fff; font-weight: bold; } .gantt-container { flex: 1; } .analysis-panel { padding: 15px; background: #fff; border-top: 1px solid #ddd; max-height: 250px; overflow-y: auto; } .chart-container { height: 200px; margin-top: 10px; } .resource-item { padding: 8px; margin: 5px 0; background: #fff; border: 1px solid #ddd; border-radius: 4px; cursor: pointer; } .resource-item.selected { background: #e3f2fd; border-color: #2196f3; } .resource-load { height: 20px; background: #e0e0e0; border-radius: 10px; margin-top: 5px; overflow: hidden; } .resource-load-bar { height: 100%; background: #4caf50; border-radius: 10px; } .overloaded { background: #f44336; } </style> </head> <body> <div class="layout"> <div class="sidebar"> <h3>资源管理</h3> <div id="resources-list"></div> <button onclick="showAddResourceForm()">添加资源</button> <h3>资源负载</h3> <div id="resource-load-chart" class="chart-container"></div> <h3>项目进度</h3> <div id="progress-analysis"> <div>总任务: <span id="total-tasks">0</span></div> <div>已完成: <span id="completed-tasks">0</span></div> <div>进行中: <span id="inprogress-tasks">0</span></div> <div>未开始: <span id="notstarted-tasks">0</span></div> <div>总体进度: <span id="overall-progress">0%</span></div> </div> </div> <div class="main-content"> <div class="toolbar"> <button onclick="switchView('gantt')">甘特图视图</button> <button onclick="switchView('resources')">资源视图</button> <button onclick="loadAnalysis()">刷新分析</button> <button onclick="showResourceLoad()">资源负载分析</button> </div> <div class="view-tabs"> <div class="tab active" data-view="gantt">任务视图</div> <div class="tab" data-view="resources">资源分配</div> </div> <div class="gantt-container"> <div id="gantt_here" style='width:100%; height:100%;'></div> </div> <div class="analysis-panel"> <h4>资源负载分析</h4> <div id="load-analysis-chart" class="chart-container"></div> </div> </div> </div> <!-- 添加资源的模态框 --> <div id="resource-modal" style="display:none; position:fixed; top:50%; left:50%; transform:translate(-50%, -50%); background:white; padding:20px; border:1px solid #ccc; z-index:1000;"> <h3>添加资源</h3> <div> <label>名称: <input type="text" id="resource-name"></label> </div> <div> <label>邮箱: <input type="email" id="resource-email"></label> </div> <div> <label>角色: <input type="text" id="resource-role"></label> </div> <div> <label>容量(%): <input type="number" id="resource-capacity" min="1" max="100" value="100"></label> </div> <div> <button onclick="addResource()">保存</button> <button onclick="document.getElementById('resource-modal').style.display='none'">取消</button> </div> </div> <script type="text/javascript"> // 初始化配置 gantt.config.date_format = "%Y-%m-%d %H:%i:%s"; gantt.config.order_branch = true; gantt.config.order_branch_free = true; gantt.config.work_time = true; // 启用资源管理插件 gantt.plugins({ resource: true }); // 配置资源视图 gantt.config.resource_property = "resources"; gantt.config.resource_store = "resources"; gantt.config.show_resources = true; // 初始化甘特图 gantt.init("gantt_here"); // 配置灯光框(编辑表单)以支持资源分配 gantt.config.lightbox.sections = [ {name: "description", height: 70, map_to: "text", type: "textarea", focus: true}, {name: "time", height: 72, map_to: "auto", type: "duration"}, {name: "resources", height: 50, map_to: "resources", type: "resources", options: gantt.serverList("resources")} ]; // 加载数据 gantt.load("/data"); // 初始化数据处理器 const dp = new gantt.dataProcessor("/data"); dp.init(gantt); dp.setTransactionMode("REST"); // 视图切换 function switchView(viewType) { if (viewType === "resources") { gantt.config.show_resources = true; gantt.render(); } else { gantt.config.show_resources = false; gantt.render(); } } // 加载资源列表 function loadResources() { fetch("/data/resources") .then(response => response.json()) .then(resources => { const list = document.getElementById("resources-list"); list.innerHTML = ""; resources.forEach(resource => { const item = document.createElement("div"); item.className = "resource-item"; item.innerHTML = ` <strong>${resource.name}</strong> (${resource.role}) <div>容量: ${resource.capacity}%</div> <div class="resource-load"> <div class="resource-load-bar" style="width: ${Math.min(resource.capacity, 100)}%"></div> </div> `; item.onclick = () => showResourceDetails(resource.id); list.appendChild(item); }); }); } // 显示添加资源表单 function showAddResourceForm() { document.getElementById("resource-modal").style.display = "block"; } // 添加资源 function addResource() { const name = document.getElementById("resource-name").value; const email = document.getElementById("resource-email").value; const role = document.getElementById("resource-role").value; const capacity = document.getElementById("resource-capacity").value; fetch("/data/resource", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name, email, role, capacity }) }) .then(response => response.json()) .then(data => { if (data.action === "inserted") { document.getElementById("resource-modal").style.display = "none"; loadResources(); // 刷新资源列表 gantt.updateCollections(); } }); } // 加载分析数据 function loadAnalysis() { // 加载项目进度分析 fetch("/data/progress-analysis") .then(response => response.json()) .then(analysis => { document.getElementById("total-tasks").textContent = analysis.total_tasks; document.getElementById("completed-tasks").textContent = analysis.completed_tasks; document.getElementById("inprogress-tasks").textContent = analysis.in_progress_tasks; document.getElementById("notstarted-tasks").textContent = analysis.not_started_tasks; document.getElementById("overall-progress").textContent = analysis.overall_progress.toFixed(2) + "%"; }); // 加载资源负载分析 showResourceLoad(); } // 显示资源负载分析 function showResourceLoad() { const today = new Date(); const startDate = new Date(today.getFullYear(), today.getMonth(), 1); const endDate = new Date(today.getFullYear(), today.getMonth() + 1, 0); const formatDate = date => date.toISOString().split('T')[0]; fetch(`/data/resource-load?start_date=${formatDate(startDate)}&end_date=${formatDate(endDate)}`) .then(response => response.json()) .then(data => { renderResourceLoadChart(data); }); } // 渲染资源负载图表 function renderResourceLoadChart(data) { // 这里简化处理,实际应该使用Chart.js创建详细图表 const ctx = document.getElementById('load-analysis-chart').getContext('2d'); // 按资源分组数据 const resources = {}; data.forEach(item => { if (!resources[item.name]) { resources[item.name] = []; } resources[item.name].push({ date: item.date, load: item.assigned_workload }); }); // 创建图表 new Chart(ctx, { type: 'bar', data: { labels: Object.keys(resources), datasets: [{ label: '资源负载', data: Object.values(resources).map(resource => resource.reduce((sum, day) => sum + day.load, 0) / resource.length ), backgroundColor: Object.values(resources).map(resource => { const avgLoad = resource.reduce((sum, day) => sum + day.load, 0) / resource.length; return avgLoad > 100 ? '#f44336' : '#4caf50'; }) }] }, options: { scales: { y: { beginAtZero: true, title: { display: true, text: '负载 (%)' } } } } }); } // 初始化页面 document.addEventListener("DOMContentLoaded", function() { // 加载资源列表 loadResources(); // 加载分析数据 loadAnalysis(); // 设置标签切换 document.querySelectorAll(".tab").forEach(tab => { tab.addEventListener("click", function() { document.querySelectorAll(".tab").forEach(t => t.classList.remove("active")); this.classList.add("active"); switchView(this.getAttribute("data-view")); }); }); // 设置定期刷新 setInterval(loadAnalysis, 60000); // 每分钟刷新一次分析数据 }); </script> </body> </html>const express = require('express'); const bodyParser = require('body-parser'); const path = require('path'); const Promise = require('bluebird'); require("date-format-lite"); const port = 1337; const app = express(); app.use(express.static(path.join(__dirname, "public"))); app.use(bodyParser.urlencoded({ extended: true })); app.use(bodyParser.json()); // 添加JSON解析中间件 // 允许跨域 app.use((req, res, next) => { res.header('Access-Control-Allow-Origin', '*'); res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept'); next(); }); // 数据库配置 const mysql = require('promise-mysql'); async function serverСonfig() { const db = await mysql.createPool({ host: 'localhost', user: 'root', password: 'root123', database: 'gantt_howto_node' }); // 获取任务数据(包含资源信息) app.get("/data", async (req, res) => { try { const [tasks, links, resources, assignments] = await Promise.all([ db.query("SELECT * FROM gantt_tasks ORDER BY sortorder ASC"), db.query("SELECT * FROM gantt_links"), db.query("SELECT * FROM gantt_resources"), db.query("SELECT * FROM gantt_assignments") ]); // 处理任务数据 for (let i = 0; i < tasks.length; i++) { tasks[i].start_date = tasks[i].start_date.format("YYYY-MM-DD hh:mm:ss"); tasks[i].open = true; // 添加资源分配信息 const taskAssignments = assignments.filter(a => a.task_id == tasks[i].id); tasks[i].resources = taskAssignments.map(a => a.resource_id); } res.send({ data: tasks, collections: { links: links, resources: resources } }); } catch (error) { sendResponse(res, "error", null, error); } }); // 获取资源负载数据 app.get("/data/resource-load", async (req, res) => { try { const { start_date, end_date } = req.query; const resourceLoad = await db.query(` SELECT r.id, r.name, r.capacity, DATE(d.date) as date, COALESCE(SUM(a.units * t.duration / 8), 0) as assigned_workload FROM gantt_resources r CROSS JOIN ( SELECT DATE_ADD(?, INTERVAL seq.seq DAY) as date FROM ( SELECT (a.N + b.N * 10) seq FROM (SELECT 0 AS N UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) a CROSS JOIN (SELECT 0 AS N UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) b ) seq WHERE DATE_ADD(?, INTERVAL seq.seq DAY) <= ? ) d LEFT JOIN gantt_assignments a ON a.resource_id = r.id LEFT JOIN gantt_tasks t ON t.id = a.task_id AND d.date BETWEEN t.start_date AND DATE_ADD(t.start_date, INTERVAL t.duration DAY) WHERE d.date BETWEEN ? AND ? GROUP BY r.id, d.date ORDER BY r.id, d.date `, [start_date, start_date, end_date, start_date, end_date]); res.json(resourceLoad); } catch (error) { sendResponse(res, "error", null, error); } }); // 获取项目进度分析 app.get("/data/progress-analysis", async (req, res) => { try { const analysis = await db.query(` SELECT COUNT(*) as total_tasks, SUM(CASE WHEN progress = 1 THEN 1 ELSE 0 END) as completed_tasks, SUM(CASE WHEN progress > 0 AND progress < 1 THEN 1 ELSE 0 END) as in_progress_tasks, SUM(CASE WHEN progress = 0 THEN 1 ELSE 0 END) as not_started_tasks, AVG(progress) * 100 as overall_progress FROM gantt_tasks WHERE parent = 0 `); res.json(analysis[0]); } catch (error) { sendResponse(res, "error", null, error); } }); // 资源管理API app.get("/data/resources", async (req, res) => { try { const resources = await db.query("SELECT * FROM gantt_resources"); res.json(resources); } catch (error) { sendResponse(res, "error", null, error); } }); app.post("/data/resource", async (req, res) => { try { const { name, email, role, capacity } = req.body; const result = await db.query( "INSERT INTO gantt_resources (name, email, role, capacity) VALUES (?, ?, ?, ?)", [name, email, role, capacity] ); sendResponse(res, "inserted", result.insertId); } catch (error) { sendResponse(res, "error", null, error); } }); app.put("/data/resource/:id", async (req, res) => { try { const { name, email, role, capacity } = req.body; await db.query( "UPDATE gantt_resources SET name = ?, email = ?, role = ?, capacity = ? WHERE id = ?", [name, email, role, capacity, req.params.id] ); sendResponse(res, "updated"); } catch (error) { sendResponse(res, "error", null, error); } }); app.delete("/data/resource/:id", async (req, res) => { try { await db.query("DELETE FROM gantt_resources WHERE id = ?", [req.params.id]); sendResponse(res, "deleted"); } catch (error) { sendResponse(res, "error", null, error); } }); // 资源分配API app.post("/data/assignment", async (req, res) => { try { const { task_id, resource_id, units } = req.body; const result = await db.query( "INSERT INTO gantt_assignments (task_id, resource_id, units) VALUES (?, ?, ?)", [task_id, resource_id, units] ); sendResponse(res, "inserted", result.insertId); } catch (error) { sendResponse(res, "error", null, error); } }); app.delete("/data/assignment/:id", async (req, res) => { try { await db.query("DELETE FROM gantt_assignments WHERE id = ?", [req.params.id]); sendResponse(res, "deleted"); } catch (error) { sendResponse(res, "error", null, error); } }); // 保留你原有的任务和链接API... // [你原有的任务和链接API代码在这里] function sendResponse(res, action, tid, error) { if (action == "error") { console.log(error); res.status(500).send({ action: "error", message: error.message }); } else { let result = { action: action }; if (tid !== undefined && tid !== null) result.tid = tid; res.send(result); } } function getTask(data) { return { text: data.text, start_date: data.start_date.date("YYYY-MM-DD"), duration: data.duration, progress: data.progress || 0, parent: data.parent }; } function getLink(data) { return { source: data.source, target: data.target, type: data.type }; } } serverСonfig(); app.listen(port, () => { console.log("Server is running on port " + port + "..."); });出现了你帮我生成的代码,运行报错,还是无法执行(index):306 GET http://127.0.0.1:1337/data/resource-load?start_date=2025-07-31&end_date=2025-08-30 404 (Not Found) showResourceLoad @ (index):306 loadAnalysis @ (index):295 setInterval (anonymous) @ (index):378 VM1868:1 Uncaught (in promise) SyntaxError: Unexpected token '<', "<!DOCTYPE "... is not valid JSON VM1869:1 Uncaught (in promise) SyntaxError: Unexpected token '<', "<!DOCTYPE "... is not valid JSON
08-26
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值