Showing Posts From
backend
Python from __future__ import annotations 详解:延迟求值与前向引用
它解决什么问题 Python 在解析函数签名时会立即对类型注解求值。如果类还没定义完就引用了自身,会抛 NameError: class Node: def __init__(self, next: Node = None): # NameError: name 'Node' is not defined self.next = next加上这一行就能解决: from __future__ import annotationsclass Node: def __init__(self, next: Node = None): # 正常 self.next = next原理是让 Python 把所有注解保存为字符串,不再立即解析。 核心效果 from __future__ import annotationsclass User: passdef get_user() -> User: return User()print(get_user.__annotations__) # {'return': 'User'} ← 字符串,不是 <class '__main__.User'>没有这行时,__annotations__ 里存的是实际类对象;有了这行,全部变成字符串,运行时不解析。 解决循环引用 from __future__ import annotationsclass A: def test(self, b: B) -> None: passclass B: def test(self, a: A) -> None: pass不加的话,定义 A 时 B 还不存在,报 NameError。 常见使用场景 FastAPI / Pydantic 模型互相引用: from __future__ import annotations from pydantic import BaseModelclass Article: author: Authorclass Author: articles: list[Article]SQLAlchemy ORM 关系: from __future__ import annotations from sqlalchemy.orm import Mapped, relationshipclass User(Base): posts: Mapped[list[Post]] = relationship()class Post(Base): user: Mapped[User] = relationship()这是 FastAPI、Pydantic、SQLAlchemy 等库的文件开头几乎都有这行的原因。 运行时获取实际类型 如果需要在运行时把字符串注解解析回真实类型,用 typing.get_type_hints(): from __future__ import annotations import typingclass Foo: def bar(self) -> Foo: return selfhints = typing.get_type_hints(Foo.bar) print(hints) # {'return': <class '__main__.Foo'>}get_type_hints() 会在运行时把字符串注解解析成真实类型。 Python 版本说明from __future__ import annotations 来自 PEP 563,Python 3.7 引入 PEP 563 原本计划在 Python 3.10 成为默认行为,后来推迟,至今(Python 3.12+)仍需显式导入 Python 3.10 引入了 X | Y 联合类型语法,可以写 int | None 代替 Optional[int],但前向引用问题 from __future__ import annotations 仍是最简洁的解法一行导入,让注解写法更自由,是现代 Python 项目的常见约定。
量化交易中的 Universe:股票池定义、过滤规则与数据获取
Universe 是什么 量化交易里,Universe(投资宇宙 / 股票池)指策略允许选股和交易的资产集合。简单理解: Universe = 你的策略在哪些股票里选比如:沪深 300 成分股(300 只) 中证 500 成分股(500 只) 全 A 股剔除 ST、停牌、次新股后的剩余标的 某个行业的所有上市公司为什么要定义 Universe A 股有 5000+ 只股票。如果不做筛选,直接在全市场跑因子计算:计算量大,速度慢 流动性差的股票出信号无法成交 次新股、ST 股信号质量差,噪声多先定义好 Universe,再在 Universe 里做因子计算、排名、选股,是量化策略的标准流程: 全市场(5000+只) ↓ Universe 过滤 符合条件的股票池(如 3000 只) ↓ 因子计算 每只股票打分 ↓ 排序选股 前 N 名 → 下单交易静态 Universe 直接用指数成分股,每次调仓时更新成分股列表: import akshare as ak# 沪深 300 成分股 df = ak.index_stock_cons_csindex(symbol="000300") universe = df["成分券代码"].tolist()# 中证 500 df = ak.index_stock_cons_csindex(symbol="000905") universe = df["成分券代码"].tolist()动态 Universe 每天收盘后重新计算,根据流动性、市值等条件动态生成: import pandas as pddef build_universe(daily_data: pd.DataFrame) -> list: """ daily_data: 包含 code, close, volume, market_cap, is_st, list_days 等列 """ universe = daily_data[ (~daily_data["is_st"]) & # 剔除 ST (daily_data["list_days"] >= 60) & # 上市满 60 天 (daily_data["volume"] > 0) & # 非停牌 (daily_data["market_cap"] > 5e9) & # 市值 > 50 亿 (daily_data["turnover_20d"] > 5e7) # 20 日均成交额 > 5000 万 ] return universe["code"].tolist()常用 Universe 类型Universe 说明 适合策略沪深 300 成分股 蓝筹龙头,流动性好 低换手率、稳健因子中证 500 成分股 中盘股,成长性较好 中小盘因子中证 1000 成分股 小盘股,弹性大 小盘动量、题材全 A 剔除 ST/停牌 覆盖面广 全市场扫描行业 Universe 限定某一行业 行业轮动、配对交易在量化框架中使用 JoinQuant(聚宽): from jqdata import get_index_stocksuniverse = get_index_stocks("000300.XSHG") # 沪深 300Qlib: # qlib 配置文件 market: csi300vn.py / 自研框架: universe = build_universe(daily_df)for code in universe: factor_score = compute_factor(code) ...Universe 与 AI Agent 量化 在 AI Agent 驱动的量化系统里,Universe 还有另一层含义:限定 Agent 的分析范围。 universe: - A股 - 港股 - ETF配置好 Universe 后,Agent 的新闻抓取、因子计算、持仓分析都限制在这个范围内,避免无限制扩散导致计算量和 Token 消耗失控。
JS 动态执行字符串代码:new Function、eval 与 Node.js vm 沙箱
new Function(推荐) // 无参数 const code = "return 1 + 2"; const fn = new Function(code); console.log(fn()); // 3// 带参数 const fn2 = new Function("a", "b", "return a + b"); console.log(fn2(1, 2)); // 3// 多行代码 const fn3 = new Function(` const x = 10; return x * 2; `); console.log(fn3()); // 20和 eval 的主要区别:有独立函数作用域,不会污染外部变量 可以明确传入参数,接口更清晰 不能访问外部局部变量(只能访问全局)eval(不推荐) const result = eval("1 + 2"); console.log(result); // 3问题:和外部作用域共享变量,容易出现意外副作用 引擎无法优化,性能差 各种代码规范和 CSP 都禁止使用异步代码执行 const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;const code = ` const res = await Promise.resolve(123); return res; `;const fn = new AsyncFunction(code); fn().then(console.log); // 123Node.js vm 模块(沙箱执行) 适合在 Node 服务端执行用户提交的代码,比 eval 更安全: const vm = require("vm");const context = { a: 1, b: 2 }; vm.createContext(context);const result = vm.runInContext("a + b", context); console.log(result); // 3设置超时防止死循环: try { vm.runInContext("while(true){}", context, { timeout: 1000 }); } catch (e) { console.error("超时:", e.message); // Script execution timed out }执行来自数据库的函数字符串: const str = ` function check(order) { return order.amount > 100; } `;const check = new Function(`${str}; return check;`)(); console.log(check({ amount: 200 })); // true实战:规则引擎表达式 带上下文变量执行表达式: function runExpression(expr, data) { return new Function( "data", `with(data){ return ${expr} }` )(data); }runExpression("price * count", { price: 10, count: 2 }); // 20微信小程序:eval 和 Function 均被禁用 小程序运行环境为了安全禁止动态代码生成: eval("1+1"); // eval is not a function new Function("return 1"); // Function constructor is disabled替代方案一:JSON 规则引擎 { "op": "and", "rules": [ { "field": "amount", "operator": ">", "value": 100 }, { "field": "vip", "operator": "=", "value": true } ] }function runRule(rule, data) { return rule.rules.every(item => { switch (item.operator) { case ">": return data[item.field] > item.value; case "=": return data[item.field] === item.value; } }); }替代方案二:globalThis 注册函数表 // 初始化时注册所有可调用函数 globalThis.actions = { checkVip(data) { return data.vip; }, checkAmount(data) { return data.amount > 100; } };// 运行时按名称调用 function dispatch(funcName, data) { return globalThis.actions[funcName]?.(data); }dispatch("checkAmount", { amount: 200 }); // trueglobalThis 在小程序中可用(等价于浏览器的 window),但 window、document 在小程序里不存在。这种函数表模式是小程序低代码/规则引擎的常见做法。
数据库版本范围查询:SemVer 比较与漏洞影响版本存储设计
版本号不能用字符串比较 -- 错误:字符串排序,结果是 2.10.0 < 2.9.0 SELECT '2.10.0' < '2.9.0' -- 返回 1(true)-- 正确:语义化版本(SemVer)比较 -- 2.10.0 > 2.9.0(major.minor.patch 各位独立比较)版本范围字符串如 >= 2.0-beta9 < 2.15.0 也无法直接用 SQL BETWEEN 或 LIKE 处理。 方案一:数据库存原始版本,程序用 SemVer 库比较 表结构简单: -- 资产表 asset(id, name, product_version)-- 漏洞规则表 vuln_rule(id, cve, affected_version_raw)Java 使用 semver4j 比较: <dependency> <groupId>org.semver4j</groupId> <artifactId>semver4j</artifactId> <version>5.3.0</version> </dependency>List<Asset> assets = assetMapper.selectAll(); List<VulnRule> rules = vulnMapper.selectAll();for (Asset asset : assets) { for (VulnRule rule : rules) { if (VersionMatcher.match(asset.getVersion(), rule.getAffectedVersion())) { // 命中漏洞 } } }优点:存储简单,逻辑清晰。 缺点:全表扫描,百万资产时性能差。 方案二:拆成上下界存数据库(企业推荐) 把 >= 2.0-beta9 < 2.15.0 拆成字段: CREATE TABLE vulnerability_affected_version ( id BIGINT PRIMARY KEY, vuln_id BIGINT, min_version VARCHAR(50), max_version VARCHAR(50), min_include TINYINT, -- 1=>=, 0=> max_include TINYINT -- 1=<=, 0=< );Spring4Shell 的两个受影响区间存成两条记录: INSERT INTO vulnerability_affected_version VALUES (1, 'CVE-2022-22965', '5.2.0', '5.2.20', 1, 0), -- 5.2.0 <= ver < 5.2.20 (2, 'CVE-2022-22965', '5.3.0', '5.3.18', 1, 0); -- 5.3.0 <= ver < 5.3.18查询时取出所有规则,在程序里用 SemVer 库比较,兼容 beta、rc、snapshot 等预发布版本。 方案三:版本号数字化后 SQL 直接查 把 2.15.0 转成整数: // 2 * 1_000_000 + 15 * 1_000 + 0 = 2_015_000 int versionNum = major * 1_000_000 + minor * 1_000 + patch;数据库存两列: ALTER TABLE asset ADD COLUMN version_num BIGINT; ALTER TABLE vuln_rule ADD COLUMN min_num BIGINT, ADD COLUMN max_num BIGINT;-- 建索引 CREATE INDEX idx_version_num ON asset(version_num);直接 SQL 查询: SELECT a.*, v.cve FROM asset a JOIN vuln_rule v ON a.version_num >= v.min_num AND a.version_num < v.max_num;优点:可建索引,百万级数据也快。 缺点:不支持 beta、rc 等预发布后缀。 方案对比方案 速度 支持预发布 复杂度程序 SemVer 比较 全表扫 是 低拆字段 + SemVer 库 取出再比 是 中版本数字化 + SQL 索引查询 否 中推荐设计(漏洞管理平台) 参考 NVD、OSV、Snyk 等平台的数据结构: -- 漏洞主表 vulnerability(cve, severity, description, published_date)-- 受影响版本(一个漏洞可能有多个区间) vulnerability_affected_version( vuln_id, product, -- 组件名 min_version, max_version, min_inclusive, max_inclusive )接入外部数据源(NVD API、OSV API)时,它们的 JSON 结构也是这样分字段的,迁入成本最低。
停车场场景图像标注 JSON 格式解析与训练模型选择
标注 JSON 结构 自动泊车停车场场景的图像标注通常以折线形式描述结构要素,典型 JSON 结构如下: { "code": 0, "data": { "markData": { "width": 1920, "height": 1536, "marks": [ { "type": "line", "pselect": "墙", "class": { "pname": "wall" }, "point": [ {"x": 117.9, "y": 890.8}, {"x": 164.4, "y": 824.3}, {"x": 210.9, "y": 777.4} ], "attrs": {}, "markId": "y4u-C3GB3x" }, { "type": "line", "pselect": "立柱", "class": { "pname": "pillar" }, "attrs": { "P": ["square"] }, "point": [ {"x": 546.7, "y": 626.9}, {"x": 590.8, "y": 621.1} ], "markId": "y4u-CBB2i3" } ], "totalNums": { "line": 10, "polygon": 0, "rect": 0, "point": 0 } } } }关键字段说明:字段 说明type: "line" 折线标注(polyline)pselect 中文类别名(墙、路沿、立柱、其他类)class.pname 英文类别名(wall / kerb / pillar / other)point 折线坐标数组,(x,y) 为像素坐标,原点左上角attrs.P 立柱形状(square=方形柱)objectId 实例 ID,0 表示无实例关联常见标注类别类别 说明 用途wall(墙) 停车场侧墙和隔断 边界检测kerb(路沿) 车位和道路边缘 导航限制pillar(立柱) 承重柱,通常为方形 障碍物避让other(其他类) 无法分类的边缘或遮挡 训练时通常忽略训练模型选择 这类折线标注本质是 Polyline Detection / Vector Map Learning,不是目标检测。 数据量 < 1 万张:SegFormer(推荐) 把折线标注膨胀成 Mask(每类一个像素值),用 SegFormer 做语义分割: JSON 折线标注 ↓ 生成 Mask(墙=1, 路沿=2, 立柱=3, 背景=0) ↓ SegFormer 训练 ↓ Wall/Pillar/Kerb 分割图SegFormer 训练门槛低、开源实现成熟,几天内可出可用结果。 从 Mask 提取折线坐标(后处理): import cv2# 骨架提取 skeleton = cv2.ximgproc.thinning(mask)# 轮廓提取 contours, _ = cv2.findContours(skeleton, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)# 折线拟合(减少点数) for cnt in contours: poly = cv2.approxPolyDP(cnt, epsilon=3, closed=False) points = [(int(p[0][0]), int(p[0][1])) for p in poly]数据量 > 10 万张:MapTR(生产级) MapTR 直接学习向量化地图,输出的就是折线坐标序列,不需要 Mask 中间步骤: 输入图像 ↓ MapTR {"wall": [[x1,y1],[x2,y2],...], "pillar": [...]}代价是训练难度高、数据格式转换复杂、对数据量要求更高。 方案对比方案 输出坐标 难度 适合数据量SegFormer + 后处理 间接 低 < 1 万LaneATT / LSTR 直接 中 数万MapTR 直接 高 > 10 万OpenCV 半自动辅助标注 标注工具里用 OpenCV 实现"沿已标线方向自动延伸",可减少 50%~80% 的点击次数: import cv2 import numpy as npdef predict_next_point(image, points): if len(points) < 2: return None last = np.array(points[-1]) prev = np.array(points[-2]) direction = last - prev direction = direction / (np.linalg.norm(direction) + 1e-6) roi_size = 80 cx, cy = int(last[0]), int(last[1]) roi = image[max(0,cy-roi_size):cy+roi_size, max(0,cx-roi_size):cx+roi_size] edges = cv2.Canny(roi, 50, 150) lines = cv2.HoughLinesP(edges, 1, np.pi/180, threshold=15, minLineLength=10, maxLineGap=5) if lines is None: candidate = last + direction * 30 return tuple(candidate.astype(int)) best_line = None best_score = -1 for line in lines: x1, y1, x2, y2 = line[0] lv = np.array([x2-x1, y2-y1], dtype=float) lv = lv / (np.linalg.norm(lv) + 1e-6) score = abs(np.dot(direction, lv)) if score > best_score: best_score = score best_line = line[0] if best_line is not None and best_score > 0.7: x1, y1, x2, y2 = best_line mid = np.array([cx - roi_size + (x1+x2)//2, cy - roi_size + (y1+y2)//2]) return tuple(mid.astype(int)) return tuple((last + direction * 30).astype(int))用户标完前两个点后,鼠标移动时实时预显示下一个预测点,按 Enter 确认。
Windows 编译 DLL:MSVC cl.exe 与 MinGW g++ 对比
最小示例 dllmain.cpp: #include <windows.h> #include <stdio.h>extern "C" __declspec(dllexport) void HelloFromDll() { MessageBoxA(NULL, "Hello from DLL", "DLL", MB_OK); }BOOL APIENTRY DllMain(HMODULE hModule, DWORD reason, LPVOID lpReserved) { return TRUE; }MSVC cl.exe 编译 cl /LD /EHsc /Y- dllmain.cpp user32.lib参数说明:参数 含义/LD 编译为 DLL(输出 .dll + .lib)/LDd Debug 版 DLL/EHsc 启用 C++ 标准异常处理/Y- 禁用预编译头(避免找不到 pch.h)user32.lib 链接 user32(MessageBox 所在库)必须在正确的命令提示符里运行 普通 cmd 里没有 cl 命令,必须用: 开始菜单 → Visual Studio 20xx → x64 Native Tools Command Prompt for VS 20xx 或在普通 cmd 里手动初始化: call "C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvars64.bat" cl /LD /EHsc /Y- dllmain.cpp user32.libvcvars64.bat 路径随 VS 版本和安装目录变化,注意替换。 输出文件 成功后生成:dllmain.dll — 动态链接库 dllmain.lib — 导入库(供其他项目链接用) dllmain.exp — 导出表MinGW g++ 编译(不需要 VS) 安装 MinGW 后: g++ -shared -o test.dll dllmain.cpp -luser32或指定目标架构: x86_64-w64-mingw32-g++ -shared -o test.dll dllmain.cpp -luser32MinGW 编译的 DLL 默认导出所有 extern "C" 函数,不需要 .def 文件。 预编译头问题(/Y-) VS 新建项目默认启用预编译头,会生成 pch.h 和 pch.cpp。手写 .cpp 文件如果没有包含 pch.h,编译时报: fatal error C1010: unexpected end of file while looking for precompiled header解决方法:加 /Y- 禁用预编译头(适合独立小文件) 在每个 .cpp 第一行加 #include "pch.h" 在文件属性里设置「不使用预编译头」导出函数声明 C++ 有 name mangling,跨语言调用需要 extern "C" 防止符号被修饰: // 头文件 #ifdef MYDLL_EXPORTS #define MYDLL_API __declspec(dllexport) #else #define MYDLL_API __declspec(dllimport) #endifextern "C" MYDLL_API int Add(int a, int b);用 .def 文件显式指定导出名(不受 extern "C" 影响): LIBRARY mydll EXPORTS Add @1 Hello @2编译时加 /DEF:mydll.def。 验证导出 dumpbin /exports dllmain.dll或用 Dependency Walker、x64dbg 查看导出表。
JS 生成 UUID:crypto.randomUUID 与 uuid npm 包
crypto.randomUUID(现代标准方案) 浏览器和 Node.js 都内置支持: const id = crypto.randomUUID(); console.log(id); // "3f6c2a6f-8d7d-4f9c-b7d5-3fcb52c8a1aa"这是 UUID v4(随机),格式固定为 xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx。Chrome / Edge / Firefox 均支持 Node.js ≥ 19 直接用全局 crypto;Node.js 15–18 从 crypto 模块导入// Node.js 15-18 const { randomUUID } = require('crypto'); console.log(randomUUID());uuid npm 包 适合需要 v1 / v5 或更好兼容性的场景: npm install uuidimport { v4 as uuidv4 } from 'uuid';console.log(uuidv4()); // "110e8400-e29b-41d4-a716-446655440000"各版本:版本 特点 场景v1 时间 + MAC 地址 需要可排序的 IDv4 全随机 最常用,通用唯一 IDv5 命名空间 + hash 相同输入生成相同 ID老版本兼容写法 不能用 crypto.randomUUID 时的回退: function uuid() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx' .replace(/[xy]/g, c => { const r = Math.random() * 16 | 0; const v = c === 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); }Math.random() 不是密码学安全随机数,高并发下理论上存在碰撞风险,生产环境优先用 crypto.randomUUID()。 短 ID(不是 UUID) 只需要一个不太长的唯一标识,不要求标准格式时: const id = Math.random().toString(36).slice(2); // "k9x2m8q"toString(36) 用 0–9 + a–z 36进制,slice(2) 去掉开头的 0.。这个不是 UUID,不保证唯一性,只适合临时标识(如 DOM id、session key)。 更长更安全的短 ID: const id = crypto.getRandomValues(new Uint8Array(8)) .reduce((acc, b) => acc + b.toString(16).padStart(2, '0'), ''); // "a3f2c1d9e4b70821"使用场景数据库主键:用 UUID v4,全随机防枚举 请求追踪:X-Request-ID header 用 crypto.randomUUID() 前端元素 key:用短 ID 即可 内容寻址:用 UUID v5,同 URL 生成相同 ID
Electron app.asar 解包只有部分资源的原因与逆向方法
为什么解包只有部分资源 1. 文件在 app.asar.unpacked 而不在 asar 里 Electron 会把不适合打包进 asar 的文件放到同级目录: resources/ ├─ app.asar ← 前端代码、JS bundle └─ app.asar.unpacked/ ← 原生模块、大文件通常放在 unpacked 里的有:.node 原生扩展 ffmpeg.dll / ffmpeg.so AI 模型、WASM 文件 浏览器内核组件解包时要把 app.asar 和 app.asar.unpacked 都看一遍。 2. webpack 把所有代码打成了 bundle 现代 Electron 应用几乎都用 webpack 打包,解包后看到的不是: src/pages/index.js src/components/Button.js而是: main.js renderer.js chunk.a1b2c3.js所有模块都合并进了 bundle,没有"缺文件",只是代码被合并了。 3. 资源在运行时从网络下载 部分 AI 软件、IDE 工具首次启动时会在线下载:语言包 插件资源 模型文件 JS chunk下载后存到用户目录,不在安装包里: Windows: %AppData%\应用名\ ← 配置、sqlite、token %LocalAppData%\应用名\ ← 缓存、大文件、下载资源4. JS 被编译为 V8 字节码(.jsc) 商业 Electron 应用常用 bytenode 把 JS 编译成 .jsc: main.js → main.jsc普通 asar 解包后可以看到文件,但无法读取源码。 5. 应用对资源做了加密 运行时解密: // 应用内部流程(无法直接看到) const encrypted = fs.readFileSync('xxx.bin'); const code = decrypt(encrypted, key); eval(code);解包后只能看到加密的 .bin 文件。 解包工具 # 查看 asar 文件列表 npx asar list app.asar# 解包到指定目录 npx asar extract app.asar ./out解包后搜索这些关键字找 webpack 入口: webpackChunk __webpack_require__Electron 应用资源分布位置 通常存放resources/app.asar 前端页面、JS bundle、html、cssresources/app.asar.unpacked/ .node 模块、dll、ffmpeg、WASM%AppData%/应用名/ 用户配置、sqlite、历史记录%LocalAppData%/应用名/ 缓存、下载的大文件、模型%AppData%/.../Code Cache/ V8 字节码缓存%AppData%/.../IndexedDB/ 结构化数据逆向流程 1. 解包 asar npx asar extract app.asar ./out2. 检查 app.asar.unpacked ls resources/app.asar.unpacked/3. 找 preload.js preload 是 Electron 主进程和渲染进程之间的桥梁,通常包含 IPC 通信和权限暴露逻辑: out/main/preload.js out/dist-electron/preload.js4. 找用户数据目录 # Windows dir %AppData%\应用名 dir %LocalAppData%\应用名5. Hook fs 抓解密内容 对于运行时解密的资源,hook fs.readFileSync 把解密后的结果 dump 出来: const fs = require('fs'); const orig = fs.readFileSync;fs.readFileSync = function (path, options) { const result = orig.apply(this, arguments); if (path.includes('.bin') || path.includes('.jsc')) { console.log('[readFileSync]', path, result.toString('hex').slice(0, 64)); } return result; };6. 检查是否是 V8 字节码 .jsc 文件开头有 V8 magic bytes,可以用 bytenode 的 wrap 机制在运行时拿到原始代码,但需要和目标 Node.js/Electron 版本完全匹配。
Windows 配置 Maven PATH 并用 mvn 运行 Spring Boot
PowerShell 添加 Maven 到 PATH 当前用户(永久) $addPath = "D:\soft\apache-maven-3.9.15\bin" $currentPath = [Environment]::GetEnvironmentVariable("Path", "User")if ($currentPath -notlike "*$addPath*") { [Environment]::SetEnvironmentVariable("Path", "$currentPath;$addPath", "User") Write-Host "已追加: $addPath" } else { Write-Host "PATH 已存在" }当前会话立即生效 $env:Path += ";D:\soft\apache-maven-3.9.15\bin"重新打开终端才能读到写入注册表的值,不想重开就用这行临时追加。 系统级 PATH(需管理员) $addPath = "D:\soft\apache-maven-3.9.15\bin" $currentPath = [Environment]::GetEnvironmentVariable("Path", "Machine")if ($currentPath -notlike "*$addPath*") { [Environment]::SetEnvironmentVariable("Path", "$currentPath;$addPath", "Machine") }cmd 添加 Maven 到 PATH :: 追加到当前用户 PATH(永久,写注册表) setx PATH "%PATH%;D:\soft\apache-maven-3.9.15\bin":: 当前窗口立即可用 set PATH=%PATH%;D:\soft\apache-maven-3.9.15\bin系统 PATH(需管理员): setx /M PATH "%PATH%;D:\soft\apache-maven-3.9.15\bin"setx 会写注册表,当前窗口不生效,重新打开 cmd 后验证: mvn -v用 mvn 运行 Spring Boot 在项目根目录(有 pom.xml 的那层)执行: mvn spring-boot:run常用参数: # 跳过测试(开发时推荐,速度快) mvn spring-boot:run -DskipTests# 指定 profile mvn spring-boot:run -Dspring-boot.run.profiles=dev打包后运行 mvn clean package -DskipTests java -jar target/xxx.jar生产部署通常用这种方式,不依赖 Maven 运行时。 常见错误 mvn 不是内部或外部命令 PATH 没生效。执行 mvn -v 测试,不行就重新打开终端,或确认 Maven bin 目录路径是否正确。 No plugin found for prefix 'spring-boot' pom.xml 缺少 Spring Boot Maven 插件,在 <build> 里加: <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>端口 8080 被占用 netstat -ano | findstr 8080 taskkill /PID 进程ID /F或在 application.yml 改端口: server: port: 8081多模块项目依赖解析失败 ${revision} 未替换说明父 pom 没有先 install,在项目根执行: mvn clean install -N -DskipTests-N 表示只安装父 pom,不递归子模块,之后再正常 mvn spring-boot:run。
JS 获取今天零点并前后偏移 N 小时:Date setHours 与时区格式化
获取今天零点 const now = new Date(); const midnight = new Date(now); midnight.setHours(0, 0, 0, 0);console.log(midnight); // Mon Apr 14 2026 00:00:00 GMT+0800setHours(0, 0, 0, 0) 四个参数分别是小时、分钟、秒、毫秒,全设为 0 就是当天零点(本地时间)。 往后推 N 小时 const now = new Date(); const start = new Date(now); start.setHours(0, 0, 0, 0);const N = 10000; const end = new Date(start.getTime() + N * 60 * 60 * 1000);console.log("开始:", start); console.log("结束:", end); // 10000 小时 ≈ 416 天 + 16 小时后往前推 N 小时 const before = new Date(start.getTime() - N * 60 * 60 * 1000);console.log("今天零点:", start); console.log("往前 10000 小时:", before); // 约 416 天前时区问题:toISOString() 是 UTC console.log(start.toISOString()); // "2026-04-13T16:00:00.000Z" ← UTC 时间,比北京时间少 8 小时toISOString() 始终输出 UTC 时间。中国时区(UTC+8)的零点在 UTC 是前一天 16:00,如果直接用这个输出给后端或用于显示会有偏差。 本地时间格式化: function formatLocal(date) { return [ date.getFullYear(), String(date.getMonth() + 1).padStart(2, "0"), String(date.getDate()).padStart(2, "0") ].join("-") + " " + [ String(date.getHours()).padStart(2, "0"), String(date.getMinutes()).padStart(2, "0"), String(date.getSeconds()).padStart(2, "0") ].join(":"); }console.log(formatLocal(start)); // "2026-04-14 00:00:00" console.log(formatLocal(end)); // "2027-06-30 16:00:00"生成时间序列 K 线拉取、回测等场景需要每小时一个时间点的数组: function generateHourlyRange(startDate, hours) { const step = 60 * 60 * 1000; const arr = []; for (let i = 0; i <= hours; i++) { arr.push(new Date(startDate.getTime() + i * step)); } return arr; }const start = new Date(); start.setHours(0, 0, 0, 0);const range = generateHourlyRange(start, 24); console.log(range.slice(0, 3).map(formatLocal)); // ["2026-04-14 00:00:00", "2026-04-14 01:00:00", "2026-04-14 02:00:00"]往前推的时间序列只需把 + i * step 改成 - i * step。 与后端接口对接 如果后端接受的是 Unix 时间戳(秒): const startTs = Math.floor(start.getTime() / 1000); const endTs = Math.floor(end.getTime() / 1000);fetch(`/api/kline?start=${startTs}&end=${endTs}`)如果后端接受 ISO 字符串(UTC): const params = new URLSearchParams({ start: start.toISOString(), end: end.toISOString() });两者都是基于 getTime() 的毫秒时间戳,不存在时区歧义,只是显示层面需要处理本地时区。
