绿联云Docker默认的Bridge网络开启IPv6

最近NAS系统由Unraid更换回绿联云,经过数个版本的迭代,绿联云的系统可用度还是蛮高的,在绿联云部署qBittorrent过程中发现下载很慢,仅能连接上IPv4的用户,qBittorrent想要下载速度快,那开启IPv6是必须的。Docker开启IPv6最简单的方法当然是使用Host网络,但因为Host网络端口不可控,所以个人习惯还是喜欢用默认的Bridge网络。

开启SSH

绿联云的新系统开启SSH很简单,点击控制面板-终端机-把SSH勾上保存即可。打开任意bash​终端,使用ssh 管理员用户名@NAS IP​来登录,首次登录需要按提示键入yes

到这一步还没完,为了方便后续操作,需要使用sudo -i来切换到root账户,这一步需要输入你管理员用户的密码。

修改daemon.json

vi /etc/docker/daemon.json​,将下面的配置加入配置文件中,按:wq​保存后,使用systemctl restart docker重启Docker引擎即可。

1
2
3
4
""ipv6"": true,
""fixed-cidr-v6"": ""fd00::/80"",
""ip6tables"": true,
""experimental"": true

贴一下最终完整版的daemon.json文件。

1
2
3
4
5
6
7
8
9
10
{
""data-root"": ""/volume1/@docker"",
""experimental"": true,
""fixed-cidr-v6"": ""fd00::/80"",
""ip6tables"": true,
""ipv6"": true,
""registry-mirrors"": [
""https://carefu.link/""
]
}

完成后进入Docker容器内,此时已经可以成功Ping通IPv6地址了,因为这种方式是基于IPv6 NAT,并不会为容器分配独立的IPv6,容器是使用宿主机的IPv6对外通信,但对于qBittorrent这种程序来讲是足够了的,Tracker中可以看到已经可以连上其他用户的IPv6。

给博客添加一个自动更新基金持仓盈亏的页面

今年开始理财,但因为购买渠道众多导致持仓比较分散需要打开各个APP查看盈亏情况,所以给博客加了一个自动更新持仓净值的页面(参考:示例页面)。主要使用的是天天基金的API接口。

代码分3个文件,分别是用以自动更新基金净值的update_funds.php​,用以存储基金信息的funds.json,以及用以展示基金信息的模板文件。

使用方法

首先需要把update_funds.php文件上传到支持php的web服务器中,然后在/data目录创建名为funds.json​的数据文件,其中name、code、cost_price、shares​四个字段需要手动填写,分别对应基金名称、基金代码、持仓成本、持仓份额,每次update_funds.php​文件时,脚本会把最新的净值写入到latest_net_value​字段中并将更新时间写入到last_updated中,可以考虑设置计划任务来定期访问该脚本。

另外可以向json中加入update_enabled字段,当值为false时,会跳过更新基金净值,可以在基金清仓后添加,用以跳过基金净值更新。

前端展现的代码仅供参考,原理无非就是调取funds.json文件中的内容,并计算出收益((最新净值-持仓成本)*持仓份额)、收益率在前端页面展现。

update_funds.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
<?php
/*
* 基金净值自动更新脚本
* 数据源:https://fund.eastmoney.com/
* 配置文件:funds.json
*/

// 配置参数 ================================================
define('USER_AGENTS', [
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15',
'Mozilla/5.0 (iPhone; CPU iPhone OS 16_5 like Mac OS X) AppleWebKit/605.1.15'
]);

define('REQUEST_DELAY', 1); // 请求间隔(秒)
define('MAX_RETRY', 2); // 单基金重试次数

// 主程序 ==================================================
try {
// 加载基金数据
$funds = loadFundsData();

// 遍历更新净值
foreach ($funds as &$fund) {
// 新增逻辑:检查是否允许更新
if (isset($fund['update_enabled']) && !$fund['update_enabled']) {
echo "跳过更新:{$fund['code']} (已禁用更新)\n";
continue;
}
try {
$result = fetchFundValue($fund['code']);

// 只更新净值相关字段
$fund['latest_net_value'] = $result['value'];
$fund['last_updated'] = date('Y-m-d H:i:s');

echo "成功更新:{$fund['code']} => {$result['value']}\n";
} catch (Exception $e) {
echo "更新失败:{$fund['code']} - {$e->getMessage()}\n";
logError($fund['code'], $e->getMessage());
continue;
}

// 遵守请求间隔
sleep(REQUEST_DELAY);
}

// 保存更新后的数据
saveFundsData($funds);
echo "全部更新完成!\n";

} catch (Exception $e) {
die("致命错误:" . $e->getMessage());
}

// 核心函数 ================================================

/**
* 加载基金数据文件
*/
function loadFundsData() {
$filename = '/data/funds.json';

if (!file_exists($filename)) {
throw new Exception("基金数据文件不存在");
}

$data = json_decode(file_get_contents($filename), true);

if (json_last_error() !== JSON_ERROR_NONE) {
throw new Exception("JSON解析错误:" . json_last_error_msg());
}

return $data;
}

/**
* 保存基金数据文件
*/
function saveFundsData($data) {
$json = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);

if (file_put_contents('/data/funds.json', $json) === false) {
throw new Exception("文件保存失败");
}
}

/**
* 获取基金净值(带重试机制)
*/
function fetchFundValue($code) {
for ($i = 0; $i <= MAX_RETRY; $i++) {
try {
return [
'value' => getLatestNetValue($code),
'timestamp' => time()
];
} catch (Exception $e) {
if ($i == MAX_RETRY) {
throw $e;
}
usleep(500000 * ($i + 1)); // 递增延时
}
}
}

/**
* 核心抓取逻辑
*/
function getLatestNetValue($code) {
$ch = curl_init();

curl_setopt_array($ch, [
CURLOPT_URL => "https://fund.eastmoney.com/pingzhongdata/{$code}.js",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 8,
CURLOPT_HTTPHEADER => [
'Referer: https://fund.eastmoney.com/',
'User-Agent: USER_AGENTS[array_rand(USER_AGENTS)]'
]
]);

$content = curl_exec($ch);

// 错误处理
if (curl_errno($ch)) {
throw new Exception("网络请求失败:" . curl_error($ch));
}

$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($httpCode != 200) {
throw new Exception("HTTP错误代码:{$httpCode}");
}

curl_close($ch);

// 解析数据
if (preg_match('/Data_netWorthTrend\s*=\s*(\[.*?\])/s', $content, $matches)) {
$data = json_decode($matches[1], true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new Exception("JSON解析失败");
}

$latest = end($data);
return $latest['y'];
}

throw new Exception("未找到净值数据");
}

funds.json示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
[
{
"name": "南方红利",
"code": "008163",
"cost_price": 1.1424,
"shares": 43766.09,
"latest_net_value": 1.1679,
"last_updated": "2025-07-16 03:12:56"
},
{
"name": "南方中债",
"code": "006961",
"cost_price": 1.3677,
"shares": 36535.8,
"latest_net_value": 1.3675,
"last_updated": "2025-07-16 03:12:58"
},
{
"name": "鹏华中债",
"code": "008040",
"cost_price": 1.0818,
"shares": 46196.16,
"latest_net_value": 1.0817,
"last_updated": "2025-07-16 03:12:59"
},
{
"name": "华泰红利",
"code": "007467",
"cost_price": 1.6458,
"shares": 30380.4,
"latest_net_value": 1.7166,
"last_updated": "2025-07-15 14:01:51",
"update_enabled": false
},
{
"name": "摩根标普",
"code": "019305",
"cost_price": 1.4634,
"shares": 823.61,
"latest_net_value": 1.4636,
"last_updated": "2025-07-16 03:13:00"
},
{
"name": "纳斯达克",
"code": "006479",
"cost_price": 6.6801,
"shares": 180.87,
"latest_net_value": 6.6769,
"last_updated": "2025-07-16 03:13:01"
}
]

前端模板文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
<style>
table.fund h1 {
color: #2d2d2d;
font-weight: 600;
margin-bottom: 1.5rem;
padding-bottom: 0.5rem;
border-bottom: 2px solid #c62541;
}

table.fund {
border-collapse: collapse;
width: 100%;
background: white;
text-align: center;
border-radius: 8px;
margin-bottom: 5rem;
overflow: hidden;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
transition: box-shadow 0.3s ease;
}

table.fund:hover {
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
}

table.fund th {
background: #c62541;
color: white;
padding: 14px 16px;
font-weight: 600;
text-transform: uppercase;
font-size: 0.9em;
}

table.fund td {
padding: 12px 16px;
color: #444;
border: 1px solid #f0f0f0;
}

table.fund tr:last-child td {
border-bottom: none;
}

table.fund tr:hover td {
background: #fff5f7;
}

table.fund .positive {
color: #c62541;
font-weight: 500;
}

table.fund .negative {
color: #27ae60;
font-weight: 500;
}

/* 响应式处理 */
@media (max-width: 768px) {
table.fund td, th {
padding: 10px 12px;
font-size: 0.9em;
}

table.fund h1 {
font-size: 1.4rem;
}
}

/* 时间显示样式 */
table.fund #current-time {
color: #c62541;
font-weight: 500;
}
</style>
<table>
<thead>
<tr>
<th>基金代码</th>
<th>持有份额</th>
<th>成本价</th>
<th>当前净值</th>
<th>持仓收益</th>
<th>更新时间</th>
</tr>
</thead>
<tbody id="funds-body"></tbody>
<tfoot class="highlight" id="summary-footer"></tfoot>
</table>
<p style="margin-top: 1rem; color: #666;">
数据更新频率:每日凌晨3点自动更新
<br>当前时间:<span id="current-time"></span>
</p>
<script>
// 配置参数
const JSON_URL = 'https://static.goldrun.click/json/fund.json';
// 初始化
document.addEventListener('DOMContentLoaded', async () => {
try {
const funds = await loadFundsData();
renderTable(funds);
startClock();
} catch (error) {
showError(error.message);
}
});
// 加载远程数据
async function loadFundsData() {
try {
const response = await fetch(JSON_URL);
if (!response.ok) throw new Error('网络响应异常');
return await response.json();
} catch (error) {
throw new Error('数据加载失败,请稍后刷新');
}
}
// 渲染表格
function renderTable(funds) {
let total = { cost: 0, current: 0, profit: 0 };
const tbody = document.getElementById('funds-body');
// 生成数据行
tbody.innerHTML = funds.map(fund => {
const fundCost = fund.cost_price * fund.shares;
const fundCurrent = fund.latest_net_value * fund.shares;
const profit = fundCurrent - fundCost;
// 累计总数
total.cost += fundCost;
total.current += fundCurrent;
total.profit += profit;
return `
<tr>
<td>${fund.code}</td>
<td>${formatNumber(fund.shares, 2)}</td>
<td>${formatNumber(fund.cost_price, 4)}</td>
<td>${fund.latest_net_value ? formatNumber(fund.latest_net_value, 4) : '--'}</td>
<td class="${profit >= 0 ? 'positive' : 'negative'}">
${formatNumber(profit, 2)}
</td>
<td>${fund.last_updated || '--'}</td>
</tr>`;
}).join('');
// 生成汇总行
const footer = document.getElementById('summary-footer');
footer.innerHTML = `
<tr>
<td colspan="4">总收益</td>
<td class="${total.profit >= 0 ? 'positive' : 'negative'}">
${formatNumber(total.profit, 2)}
</td>
<td></td>
</tr>
<tr>
<td colspan="4">总收益率</td>
<td class="${total.profit >= 0 ? 'positive' : 'negative'}">
${total.cost ? formatNumber((total.profit / total.cost) * 100, 2) + '%' : '--'}
</td>
<td></td>
</tr>`;
}
// 数字格式化
function formatNumber(num, digits) {
return num.toLocaleString('zh-CN', {
minimumFractionDigits: digits,
maximumFractionDigits: digits
});
}
// 实时时钟
function startClock() {
function updateTime() {
document.getElementById('current-time').textContent =
new Date().toLocaleString('zh-CN');
}
updateTime();
setInterval(updateTime, 1000);
}
// 错误显示
function showError(message) {
const tbody = document.getElementById('funds-body');
tbody.innerHTML = `<tr><td colspan="6" style="color:red">${message}</td></tr>`;
}
</script>
You need to set client_id and slot_id to show this AD unit. Please set it in _config.yml.