diff --git a/.gitignore b/.gitignore index 841b23b..9bf3bb8 100644 --- a/.gitignore +++ b/.gitignore @@ -54,3 +54,10 @@ uploads/ tmp/ .codebuddy/ + +# 代理/部署临时产物(不入库) +.tmp-*/ +.cargo-*/ +deploy_output.txt +deploy_remote.sh +/tmp/ diff --git a/.tmp-energy-verify/baseline_old_binary.txt b/.tmp-energy-verify/baseline_old_binary.txt deleted file mode 100644 index efb3264..0000000 --- a/.tmp-energy-verify/baseline_old_binary.txt +++ /dev/null @@ -1,28 +0,0 @@ -### BASE=http://127.0.0.1:3000/api ---- GET /api/energy-stats/summary -{"code":0,"data":{"cabinet_count":3,"estimated_cost":0.0,"month_energy":0,"online_count":3,"today_energy":0},"message":"ok"} -[HTTP 200] - ---- GET /api/energy-stats/cabinets -{"code":"DATABASE_ERROR","message":"服务器内部错误"} -[HTTP 500] - ---- GET /api/energy-stats/projects -{"code":0,"data":[{"cabinet_count":1,"month_energy":0,"name":"联调测试项目","project_id":1,"today_energy":0},{"cabinet_count":2,"month_energy":0,"name":"能耗验证临时项目","project_id":2,"today_energy":0}],"message":"ok"} -[HTTP 200] - ---- GET /api/energy-stats/trend?granularity=day&range=30 -{"code":0,"data":{"granularity":"day","list":[]},"message":"ok"} -[HTTP 200] - ---- GET /api/energy-stats/trend?granularity=week&range=30 -{"code":0,"data":{"granularity":"week","list":[]},"message":"ok"} -[HTTP 200] - ---- GET /api/energy-stats/trend?granularity=month&range=30 -{"code":0,"data":{"granularity":"month","list":[]},"message":"ok"} -[HTTP 200] - -=== 导出接口(异步任务) === -{"code":0,"data":{"task_id":1},"message":"导出任务已创建"} -[HTTP 200] diff --git a/.tmp-energy-verify/check_indexes.py b/.tmp-energy-verify/check_indexes.py deleted file mode 100644 index 4f92bcc..0000000 --- a/.tmp-energy-verify/check_indexes.py +++ /dev/null @@ -1,31 +0,0 @@ -"""确认 pms_dev 上 charge_records 的索引(含能耗报表新增的两条)。""" -import pymysql - -conn = pymysql.connect(host="10.8.0.252", user="root", - password="Hbhyg731024@", database="pms_dev") -cur = conn.cursor() -cur.execute( - "SELECT index_name, GROUP_CONCAT(column_name ORDER BY seq_in_index) AS cols, " - "non_unique FROM information_schema.statistics " - "WHERE table_schema = 'pms_dev' AND table_name = 'charge_records' " - "GROUP BY index_name, non_unique ORDER BY index_name" -) -print("charge_records 索引:") -for name, cols, non_unique in cur.fetchall(): - kind = "UNIQUE" if non_unique == 0 else "INDEX" - print(f" {kind:6} {name:40} ({cols})") - -cur.execute( - "SELECT COUNT(*) FROM information_schema.statistics " - "WHERE table_schema='pms_dev' AND table_name='charge_records' " - "AND index_name='idx_charge_records_cabinet_start_time'" -) -print("idx_charge_records_cabinet_start_time 存在:", cur.fetchone()[0] == 2, "(2 列)") -cur.execute( - "SELECT COUNT(*) FROM information_schema.statistics " - "WHERE table_schema='pms_dev' AND table_name='charge_records' " - "AND index_name='idx_charge_records_start_time'" -) -print("idx_charge_records_start_time 存在:", cur.fetchone()[0] == 1, "(1 列)") -cur.close() -conn.close() diff --git a/.tmp-energy-verify/check_timezone.py b/.tmp-energy-verify/check_timezone.py deleted file mode 100644 index b2b5574..0000000 --- a/.tmp-energy-verify/check_timezone.py +++ /dev/null @@ -1,39 +0,0 @@ -"""确认能耗报表"今日/本月"的时区口径。 - -背景:服务返回 today_energy=6000(= 2026-09-19 那天的数据),而本机 pymysql -连接下 CURDATE() 是 2026-09-20。差异来自 **sqlx 会把 MySQL 会话时区设成 +00:00** -(保证时间转换确定性),而普通客户端沿用服务器的默认会话时区。 -""" -import pymysql - -conn = pymysql.connect(host="10.8.0.252", user="root", - password="Hbhyg731024@", database="pms_dev") -cur = conn.cursor() - -cur.execute("SELECT @@global.time_zone, @@session.time_zone, NOW(), CURDATE()") -print("默认会话时区 :", cur.fetchone()) - -cur.execute("SET time_zone = '+00:00'") -cur.execute("SELECT @@session.time_zone, NOW(), CURDATE()") -print("模拟 sqlx(+00:00) :", cur.fetchone()) - -TODAY = "cr.start_time >= CURDATE() AND cr.start_time < CURDATE() + INTERVAL 1 DAY" -THIS_MONTH = ( - "cr.start_time >= DATE_FORMAT(CURDATE(), '%Y-%m-01') " - "AND cr.start_time < DATE_FORMAT(CURDATE() + INTERVAL 1 MONTH, '%Y-%m-01')" -) -cur.execute(f"SELECT CAST(COALESCE(SUM(cr.energy),0) AS SIGNED) FROM charge_records cr WHERE {TODAY}") -print("UTC 会话下 today :", cur.fetchone()[0], "(服务实测返回 6000 ✓)" if cur.rowcount else "") -cur.execute(f"SELECT CAST(COALESCE(SUM(cr.energy),0) AS SIGNED) FROM charge_records cr WHERE {THIS_MONTH}") -print("UTC 会话下 month :", cur.fetchone()[0], "(服务实测返回 6990 ✓)") - -# start_time 在两种会话时区下的呈现(TIMESTAMP 列按会话时区转换) -cur.execute("SET time_zone = '+08:00'") -cur.execute("SELECT id, cabinet_id, start_time FROM charge_records ORDER BY id LIMIT 3") -print("+08:00 会话读 start_time :", cur.fetchall()) -cur.execute("SET time_zone = '+00:00'") -cur.execute("SELECT id, cabinet_id, start_time FROM charge_records ORDER BY id LIMIT 3") -print("+00:00 会话读 start_time :", cur.fetchall()) - -cur.close() -conn.close() diff --git a/.tmp-energy-verify/cleanup_output.txt b/.tmp-energy-verify/cleanup_output.txt deleted file mode 100644 index 8baaa4a..0000000 --- a/.tmp-energy-verify/cleanup_output.txt +++ /dev/null @@ -1,22 +0,0 @@ -DELETE charge_records 7 行 (ids=[10, 11, 12, 13, 14, 15, 16]) -DELETE compartments 3 行 (ids=[5, 6, 7]) -DELETE cabin_boards 3 行 (ids=[3, 4, 5]) -DELETE cabinets 3 行 (ids=[2, 3, 4]) -DELETE projects 2 行 (ids=[2, 3]) -DELETE organizations 1 行 (ids=[4]) -DELETE download_tasks 4 行 (ids=[1, 2, 3, 4]) -DELETE users 1 行 (ids=[7]) - -=== 清理后校验 === - charge_records = 0 - energy_stats = 0 - cabinets = 1 - projects = 1 - organizations = 1 - users = 0 - compartments = 4 - cabin_boards = 2 - download_tasks = 0 - charge_records 索引(应保留): (('compartment_id', 'compartment_id'), ('idx_charge_records_cabinet', 'cabinet_id'), ('idx_charge_records_cabinet_start_time', 'cabinet_id,start_time'), ('idx_charge_records_start_time', 'start_time'), ('PRIMARY', 'id')) - -临时数据清理完成 diff --git a/.tmp-energy-verify/cleanup_temp_data.py b/.tmp-energy-verify/cleanup_temp_data.py deleted file mode 100644 index fcf6218..0000000 --- a/.tmp-energy-verify/cleanup_temp_data.py +++ /dev/null @@ -1,72 +0,0 @@ -"""清理临时联调数据:只删除本任务自己插入的行(按记录到的 ID), -并删除测试期间产生的 download_tasks 行;**保留**新增的两条索引(迁移产物)。 - -删除顺序按外键依赖:charge_records → compartments → cabin_boards → cabinets → projects → organizations -""" -import json -import os -import pymysql - -HERE = os.path.dirname(os.path.abspath(__file__)) -with open(os.path.join(HERE, "temp_ids.json"), encoding="utf-8") as fh: - main_ids = json.load(fh) -with open(os.path.join(HERE, "temp_ids_orgB.json"), encoding="utf-8") as fh: - orgb_ids = json.load(fh) - -record_ids = main_ids["record_ids"] + [orgb_ids["record_id"]] -compartment_ids = main_ids["compartment_ids"] + [orgb_ids["compartment_id"]] -board_ids = main_ids["board_ids"] + [orgb_ids["board_id"]] -cabinet_ids = main_ids["cabinet_ids"] + [orgb_ids["cabinet_id"]] -project_ids = [main_ids["project_id"], orgb_ids["project_id"]] -org_ids = [orgb_ids["org_id"]] -user_ids = [main_ids["user_id"]] -task_ids = [1, 2, 3, 4] # 本次测试创建的下载任务 - -conn = pymysql.connect(host="10.8.0.252", user="root", - password="Hbhyg731024@", database="pms_dev") -cur = conn.cursor() - -steps = [ - ("charge_records", "id", record_ids), - ("compartments", "id", compartment_ids), - ("cabin_boards", "id", board_ids), - ("cabinets", "id", cabinet_ids), - ("projects", "id", project_ids), - ("organizations", "id", org_ids), - # download_tasks.user_id 外键指向 users.id ⇒ 必须**先**删 download_tasks 再删 users - ("download_tasks", "id", task_ids), - ("users", "id", user_ids), -] -for table, col, ids in steps: - if not ids: - continue - ph = ",".join(["%s"] * len(ids)) - cur.execute(f"DELETE FROM {table} WHERE {col} IN ({ph})", ids) - print(f"DELETE {table:16} {cur.rowcount:2d} 行 (ids={ids})") - -conn.commit() - -print("\n=== 清理后校验 ===") -for table, sql in [ - ("charge_records", "SELECT COUNT(*) FROM charge_records"), - ("energy_stats", "SELECT COUNT(*) FROM energy_stats"), - ("cabinets", "SELECT COUNT(*) FROM cabinets"), - ("projects", "SELECT COUNT(*) FROM projects"), - ("organizations", "SELECT COUNT(*) FROM organizations"), - ("users", "SELECT COUNT(*) FROM users"), - ("compartments", "SELECT COUNT(*) FROM compartments"), - ("cabin_boards", "SELECT COUNT(*) FROM cabin_boards"), - ("download_tasks", "SELECT COUNT(*) FROM download_tasks"), -]: - cur.execute(sql) - print(f" {table:16} = {cur.fetchone()[0]}") - -cur.execute( - "SELECT index_name, GROUP_CONCAT(column_name ORDER BY seq_in_index) FROM information_schema.statistics " - "WHERE table_schema='pms_dev' AND table_name='charge_records' GROUP BY index_name ORDER BY index_name" -) -print(" charge_records 索引(应保留):", cur.fetchall()) - -cur.close() -conn.close() -print("\n临时数据清理完成") diff --git a/.tmp-energy-verify/export_xlsx_content.txt b/.tmp-energy-verify/export_xlsx_content.txt deleted file mode 100644 index 016f81f..0000000 --- a/.tmp-energy-verify/export_xlsx_content.txt +++ /dev/null @@ -1,8 +0,0 @@ -sheet: 能耗统计 dims: A1:F7 - ('日期', '柜子ID', '柜子名称', '所属项目', '能耗(Wh)', '充电次数') - ('2026-09-20', 1, '联调测试柜', '联调测试项目', 220, 1) - ('2026-09-20', 2, '能耗验证临时柜A', '能耗验证临时项目', 330, 1) - ('2026-09-20', 3, '能耗验证临时柜B', '能耗验证临时项目', 440, 1) - ('2026-09-19', 1, '联调测试柜', '联调测试项目', 1000, 1) - ('2026-09-19', 2, '能耗验证临时柜A', '能耗验证临时项目', 2000, 1) - ('2026-09-19', 3, '能耗验证临时柜B', '能耗验证临时项目', 3000, 1) diff --git a/.tmp-energy-verify/exports/能耗统计_20260919-20260920.xlsx b/.tmp-energy-verify/exports/能耗统计_20260919-20260920.xlsx deleted file mode 100644 index b59a866..0000000 Binary files a/.tmp-energy-verify/exports/能耗统计_20260919-20260920.xlsx and /dev/null differ diff --git a/.tmp-energy-verify/facts.txt b/.tmp-energy-verify/facts.txt deleted file mode 100644 index e6ad001..0000000 --- a/.tmp-energy-verify/facts.txt +++ /dev/null @@ -1,2 +0,0 @@ -SUPER=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjcsInBob25lIjoiMTk5MDAwMDAwMDEiLCJleHAiOjE3ODk5MjYzMTUsImlhdCI6MTc4OTgzOTkxNX0.36ocLLksl-sZ_PK8M1mI8M9i9NOq0OcYv_xOdz2iCQA -ORG1=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjcsInBob25lIjoiMTk5MDAwMDAwMDEiLCJleHAiOjE3ODk5MjYzMTYsImlhdCI6MTc4OTgzOTkxNn0.OpOgTeYh9UdjReUVLh698TN9OOjmuPMxbao8LBZHQDM diff --git a/.tmp-energy-verify/index_check.txt b/.tmp-energy-verify/index_check.txt deleted file mode 100644 index 1150a7a..0000000 --- a/.tmp-energy-verify/index_check.txt +++ /dev/null @@ -1,8 +0,0 @@ -charge_records 索引: - INDEX compartment_id (compartment_id) - INDEX idx_charge_records_cabinet (cabinet_id) - INDEX idx_charge_records_cabinet_start_time (cabinet_id,start_time) - INDEX idx_charge_records_start_time (start_time) - UNIQUE PRIMARY (id) -idx_charge_records_cabinet_start_time 存在: True (2 列) -idx_charge_records_start_time 存在: True (1 列) diff --git a/.tmp-energy-verify/isolation_org1.txt b/.tmp-energy-verify/isolation_org1.txt deleted file mode 100644 index d88ed4a..0000000 --- a/.tmp-energy-verify/isolation_org1.txt +++ /dev/null @@ -1,28 +0,0 @@ -### BASE=http://127.0.0.1:3000/api ---- GET /api/energy-stats/summary -{"code":0,"data":{"cabinet_count":3,"estimated_cost":5.59,"month_energy":6990,"online_count":3,"today_energy":6000},"message":"ok"} -[HTTP 200] - ---- GET /api/energy-stats/cabinets -{"code":0,"data":[{"abstract_id":"99-00000002","cabinet_id":3,"month_energy":3440,"name":"能耗验证临时柜B","today_energy":3000},{"abstract_id":"99-00000001","cabinet_id":2,"month_energy":2330,"name":"能耗验证临时柜A","today_energy":2000},{"abstract_id":"","cabinet_id":1,"month_energy":1220,"name":"联调测试柜","today_energy":1000}],"message":"ok"} -[HTTP 200] - ---- GET /api/energy-stats/projects -{"code":0,"data":[{"cabinet_count":2,"month_energy":5770,"name":"能耗验证临时项目","project_id":2,"today_energy":5000},{"cabinet_count":1,"month_energy":1220,"name":"联调测试项目","project_id":1,"today_energy":1000}],"message":"ok"} -[HTTP 200] - ---- GET /api/energy-stats/trend?granularity=day&range=30 -{"code":0,"data":{"granularity":"day","list":[{"charge_count":3,"energy":6000,"period":"2026-09-19"},{"charge_count":3,"energy":990,"period":"2026-09-20"}]},"message":"ok"} -[HTTP 200] - ---- GET /api/energy-stats/trend?granularity=week&range=30 -{"code":0,"data":{"granularity":"week","list":[{"charge_count":6,"energy":6990,"period":"2026-38"}]},"message":"ok"} -[HTTP 200] - ---- GET /api/energy-stats/trend?granularity=month&range=30 -{"code":0,"data":{"granularity":"month","list":[{"charge_count":6,"energy":6990,"period":"2026-09"}]},"message":"ok"} -[HTTP 200] - -=== 导出接口(异步任务) === -{"code":0,"data":{"task_id":4},"message":"导出任务已创建"} -[HTTP 200] diff --git a/.tmp-energy-verify/isolation_super.txt b/.tmp-energy-verify/isolation_super.txt deleted file mode 100644 index 38c0481..0000000 --- a/.tmp-energy-verify/isolation_super.txt +++ /dev/null @@ -1,28 +0,0 @@ -### BASE=http://127.0.0.1:3000/api ---- GET /api/energy-stats/summary -{"code":0,"data":{"cabinet_count":4,"estimated_cost":11.81,"month_energy":14767,"online_count":4,"today_energy":13777},"message":"ok"} -[HTTP 200] - ---- GET /api/energy-stats/cabinets -{"code":0,"data":[{"abstract_id":"99-00000009","cabinet_id":4,"month_energy":7777,"name":"能耗验证临时柜-组织B","today_energy":7777},{"abstract_id":"99-00000002","cabinet_id":3,"month_energy":3440,"name":"能耗验证临时柜B","today_energy":3000},{"abstract_id":"99-00000001","cabinet_id":2,"month_energy":2330,"name":"能耗验证临时柜A","today_energy":2000},{"abstract_id":"","cabinet_id":1,"month_energy":1220,"name":"联调测试柜","today_energy":1000}],"message":"ok"} -[HTTP 200] - ---- GET /api/energy-stats/projects -{"code":0,"data":[{"cabinet_count":1,"month_energy":7777,"name":"能耗验证临时项目B","project_id":3,"today_energy":7777},{"cabinet_count":2,"month_energy":5770,"name":"能耗验证临时项目","project_id":2,"today_energy":5000},{"cabinet_count":1,"month_energy":1220,"name":"联调测试项目","project_id":1,"today_energy":1000}],"message":"ok"} -[HTTP 200] - ---- GET /api/energy-stats/trend?granularity=day&range=30 -{"code":0,"data":{"granularity":"day","list":[{"charge_count":4,"energy":13777,"period":"2026-09-19"},{"charge_count":3,"energy":990,"period":"2026-09-20"}]},"message":"ok"} -[HTTP 200] - ---- GET /api/energy-stats/trend?granularity=week&range=30 -{"code":0,"data":{"granularity":"week","list":[{"charge_count":7,"energy":14767,"period":"2026-38"}]},"message":"ok"} -[HTTP 200] - ---- GET /api/energy-stats/trend?granularity=month&range=30 -{"code":0,"data":{"granularity":"month","list":[{"charge_count":7,"energy":14767,"period":"2026-09"}]},"message":"ok"} -[HTTP 200] - -=== 导出接口(异步任务) === -{"code":0,"data":{"task_id":3},"message":"导出任务已创建"} -[HTTP 200] diff --git a/.tmp-energy-verify/make_role_tokens.py b/.tmp-energy-verify/make_role_tokens.py deleted file mode 100644 index 810e57c..0000000 --- a/.tmp-energy-verify/make_role_tokens.py +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env python3 -"""组织隔离实测:切换临时用户的角色,用两个 JWT 打同一组接口。 - -- token A: users.id=7 role=2 organization_id=NULL → 总管理员,应看到全部 -- token B: users.id=7 role=1 organization_id=1 → 组织1管理员,应看不到组织B的 7777 -""" -import time -import pymysql -import jwt - -SECRET = "32e8708743c42fe764aff1d40737a63803a46815a9501f4bcb50c41fe3d27108" -FACTS = "facts.txt" - - -def token_for(role, org_id): - conn = pymysql.connect(host="10.8.0.252", user="root", - password="Hbhyg731024@", database="pms_dev") - cur = conn.cursor() - cur.execute("UPDATE users SET role=%s, organization_id=%s WHERE id=7", (role, org_id)) - conn.commit() - cur.close() - conn.close() - now = int(time.time()) - return jwt.encode({"sub": 7, "phone": "19900000001", - "exp": now + 86400, "iat": now}, SECRET, algorithm="HS256") - - -if __name__ == "__main__": - with open(FACTS, "w", encoding="utf-8") as fh: - fh.write("SUPER=" + token_for(2, None) + "\n") - fh.write("ORG1=" + token_for(1, 1) + "\n") - print(open(FACTS, encoding="utf-8").read()) diff --git a/.tmp-energy-verify/setup_orgB.py b/.tmp-energy-verify/setup_orgB.py deleted file mode 100644 index 7a6d0fc..0000000 --- a/.tmp-energy-verify/setup_orgB.py +++ /dev/null @@ -1,58 +0,0 @@ -"""组织隔离的实测准备:再插一个「另一个组织」的数据(energy=7777), -用于验证 role_level=1 的组织管理员**看不到**它的数据。 - -落点故意与 2026-09-19 那批数据同一时刻(= 服务 UTC 会话下的"今天"), -这样超管"今日"= 6000+7777 = 13777,而组织 1 管理员必须仍是 6000。 -""" -import json -import os -import pymysql - -HERE = os.path.dirname(os.path.abspath(__file__)) -conn = pymysql.connect(host="10.8.0.252", user="root", - password="Hbhyg731024@", database="pms_dev") -cur = conn.cursor() - -created = {"org_id": None, "project_id": None, "cabinet_id": None, - "board_id": None, "compartment_id": None, "record_id": None} - -cur.execute("INSERT INTO organizations (name) VALUES ('能耗验证临时组织B')") -created["org_id"] = cur.lastrowid -org_b = created["org_id"] - -cur.execute("INSERT INTO projects (organization_id, name) VALUES (%s, %s)", - (org_b, "能耗验证临时项目B")) -created["project_id"] = cur.lastrowid - -cur.execute( - "INSERT INTO cabinets (project_id, abstract_id, imei, name, status) " - "VALUES (%s, %s, %s, %s, %s)", - (created["project_id"], "99-00000009", "999000000000009", "能耗验证临时柜-组织B", 1), -) -created["cabinet_id"] = cur.lastrowid - -cur.execute("INSERT INTO cabin_boards (cabinet_id, board_id, status) VALUES (%s, 1, 1)", - (created["cabinet_id"],)) -created["board_id"] = cur.lastrowid - -cur.execute("INSERT INTO compartments (cabin_board_id, channel_id, status) VALUES (%s, 1, 0)", - (created["board_id"],)) -created["compartment_id"] = cur.lastrowid - -# 与 2026-09-19 那批(服务"今天")同一时刻 -cur.execute( - "INSERT INTO charge_records " - "(compartment_id, cabinet_id, start_time, end_time, energy, max_temperature, status) " - "VALUES (%s, %s, DATE_SUB(CURDATE(), INTERVAL 1 DAY) + INTERVAL 10 HOUR, " - "DATE_SUB(CURDATE(), INTERVAL 1 DAY) + INTERVAL 12 HOUR, 7777, 29815, 0)", - (created["compartment_id"], created["cabinet_id"]), -) -created["record_id"] = cur.lastrowid - -conn.commit() -path = os.path.join(HERE, "temp_ids_orgB.json") -with open(path, "w", encoding="utf-8") as fh: - json.dump(created, fh, ensure_ascii=False, indent=2) -print(json.dumps(created, ensure_ascii=False)) -cur.close() -conn.close() diff --git a/.tmp-energy-verify/setup_temp_data.py b/.tmp-energy-verify/setup_temp_data.py deleted file mode 100644 index a9d36c1..0000000 --- a/.tmp-energy-verify/setup_temp_data.py +++ /dev/null @@ -1,99 +0,0 @@ -"""临时联调数据:为能耗报表验证准备 pms_dev 数据,并打印清理所需的 ID。 - -产出: -- 1 个临时用户(role=2,总管理员)→ 用于自签 JWT -- 1 个临时项目(org=1)+ 2 个临时柜子(含板/仓体)→ 用于验证项目/柜分组 -- 6 条 charge_records(3 柜 × 2 天)→ 用于验证分组与趋势 - -所有 ID 写入 temp_ids.json,供 cleanup_temp_data.py 精确删除。 -""" -import json -import os -import pymysql - -HERE = os.path.dirname(os.path.abspath(__file__)) -DB = dict(host="10.8.0.252", user="root", password="Hbhyg731024@", database="pms_dev") - -conn = pymysql.connect(**DB) -cur = conn.cursor() - -created = { - "user_id": None, - "project_id": None, - "cabinet_ids": [], - "board_ids": [], - "compartment_ids": [], - "record_ids": [], -} - -# ---------------------------------------------------------------- 临时总管理员 -cur.execute( - "INSERT INTO users (phone, name, password, role, organization_id, status) " - "VALUES (%s, %s, %s, %s, %s, %s)", - ("19900000001", "能耗验证临时管理员", "x", 2, None, 1), -) -created["user_id"] = cur.lastrowid - -# ------------------------------------------------- 临时项目(org=1,验证项目分组) -cur.execute( - "INSERT INTO projects (organization_id, name) VALUES (%s, %s)", - (1, "能耗验证临时项目"), -) -temp_project_id = cur.lastrowid -created["project_id"] = temp_project_id - -# ------------------------------------- 临时柜子 ×2(挂临时项目;imei 15 位、abstract_id 唯一) -for idx, name in enumerate(["能耗验证临时柜A", "能耗验证临时柜B"], start=1): - cur.execute( - "INSERT INTO cabinets (project_id, abstract_id, imei, name, status) " - "VALUES (%s, %s, %s, %s, %s)", - (temp_project_id, f"99-0000000{idx}", f"99900000000000{idx}", name, 1), - ) - cabinet_id = cur.lastrowid - created["cabinet_ids"].append(cabinet_id) - cur.execute( - "INSERT INTO cabin_boards (cabinet_id, board_id, status) VALUES (%s, %s, %s)", - (cabinet_id, 1, 1), - ) - board_id = cur.lastrowid - created["board_ids"].append(board_id) - cur.execute( - "INSERT INTO compartments (cabin_board_id, channel_id, status) VALUES (%s, %s, %s)", - (board_id, 1, 0), - ) - created["compartment_ids"].append(cur.lastrowid) - -comp_a, comp_b = created["compartment_ids"] -cab_a, cab_b = created["cabinet_ids"] - -# ----------------------------------------------------------- 6 条充电记录 -# 3 个柜子(已有柜 1 + 临时柜 A/B)× 2 天(今天 / 昨天): -# 柜1 今天 220 Wh 昨天 1000 Wh (项目 1「联调测试项目」) -# 柜A 今天 330 Wh 昨天 2000 Wh (临时项目) -# 柜B 今天 440 Wh 昨天 3000 Wh (临时项目) -# 期望:今日合计 990 Wh;本月合计 990 + 6000 = 6990 Wh -plan = [ - (1, 1, 220, "CURDATE()"), - (1, 1, 1000, "DATE_SUB(CURDATE(), INTERVAL 1 DAY)"), - (cab_a, comp_a, 330, "CURDATE()"), - (cab_a, comp_a, 2000, "DATE_SUB(CURDATE(), INTERVAL 1 DAY)"), - (cab_b, comp_b, 440, "CURDATE()"), - (cab_b, comp_b, 3000, "DATE_SUB(CURDATE(), INTERVAL 1 DAY)"), -] -for cabinet_id, compartment_id, energy, day_expr in plan: - cur.execute( - "INSERT INTO charge_records " - "(compartment_id, cabinet_id, start_time, end_time, energy, max_temperature, status) " - f"VALUES (%s, %s, {day_expr} + INTERVAL 10 HOUR, {day_expr} + INTERVAL 12 HOUR, %s, %s, %s)", - (compartment_id, cabinet_id, energy, 29815, 0), - ) - created["record_ids"].append(cur.lastrowid) - -conn.commit() - -with open(os.path.join(HERE, "temp_ids.json"), "w", encoding="utf-8") as fh: - json.dump(created, fh, ensure_ascii=False, indent=2) - -print(json.dumps(created, ensure_ascii=False, indent=2)) -cur.close() -conn.close() diff --git a/.tmp-energy-verify/sql_verify_output.txt b/.tmp-energy-verify/sql_verify_output.txt deleted file mode 100644 index 192c6c9..0000000 --- a/.tmp-energy-verify/sql_verify_output.txt +++ /dev/null @@ -1,158 +0,0 @@ -============================================================================== -### 0. charge_records 现有行(cabinet_id / start_time / energy) -SQL> SELECT cr.id, cr.cabinet_id, c.name, cr.start_time, cr.end_time, cr.energy FROM charge_records cr LEFT JOIN cabinets c ON cr.cabinet_id = c.id ORDER BY cr.start_time DESC, cr.cabinet_id - id | cabinet_id | name | start_time | end_time | energy - 10 | 1 | 联调测试柜 | 2026-09-20 10:00:00 | 2026-09-20 12:00:00 | 220.00 - 12 | 2 | 能耗验证临时柜A | 2026-09-20 10:00:00 | 2026-09-20 12:00:00 | 330.00 - 14 | 3 | 能耗验证临时柜B | 2026-09-20 10:00:00 | 2026-09-20 12:00:00 | 440.00 - 11 | 1 | 联调测试柜 | 2026-09-19 10:00:00 | 2026-09-19 12:00:00 | 1000.00 - 13 | 2 | 能耗验证临时柜A | 2026-09-19 10:00:00 | 2026-09-19 12:00:00 | 2000.00 - 15 | 3 | 能耗验证临时柜B | 2026-09-19 10:00:00 | 2026-09-19 12:00:00 | 3000.00 - => 6 行 -============================================================================== -### 1a. summary today(总管理员,无组织条件) -SQL> SELECT CAST(COALESCE(SUM(cr.energy), 0) AS SIGNED) FROM charge_records cr WHERE cr.start_time >= CURDATE() AND cr.start_time < CURDATE() + INTERVAL 1 DAY - CAST(COALESCE(SUM(cr.energy), 0) AS SIGNED) - 990 - => 1 行 -============================================================================== -### 1b. summary month(总管理员) -SQL> SELECT CAST(COALESCE(SUM(cr.energy), 0) AS SIGNED) FROM charge_records cr WHERE cr.start_time >= DATE_FORMAT(CURDATE(), '%Y-%m-01') AND cr.start_time < DATE_FORMAT(CURDATE() + INTERVAL 1 MONTH, '%Y-%m-01') - CAST(COALESCE(SUM(cr.energy), 0) AS SIGNED) - 6990 - => 1 行 -============================================================================== -### 1c. summary today(组织 1 隔离) -SQL> SELECT CAST(COALESCE(SUM(cr.energy), 0) AS SIGNED) FROM charge_records cr WHERE cr.start_time >= CURDATE() AND cr.start_time < CURDATE() + INTERVAL 1 DAY AND cr.cabinet_id IN (SELECT c.id FROM cabinets c JOIN projects p ON c.project_id = p.id WHERE p.organization_id = 1) - CAST(COALESCE(SUM(cr.energy), 0) AS SIGNED) - 990 - => 1 行 -============================================================================== -### 1d. summary today(不存在组织 999 ⇒ 必须为空集 0) -SQL> SELECT CAST(COALESCE(SUM(cr.energy), 0) AS SIGNED) FROM charge_records cr WHERE cr.start_time >= CURDATE() AND cr.start_time < CURDATE() + INTERVAL 1 DAY AND cr.cabinet_id IN (SELECT c.id FROM cabinets c JOIN projects p ON c.project_id = p.id WHERE p.organization_id = 999) - CAST(COALESCE(SUM(cr.energy), 0) AS SIGNED) - 0 - => 1 行 -============================================================================== -### 1e. summary today(客户侧无组织 ⇒ AND 1=0) -SQL> SELECT CAST(COALESCE(SUM(cr.energy), 0) AS SIGNED) FROM charge_records cr WHERE cr.start_time >= CURDATE() AND cr.start_time < CURDATE() + INTERVAL 1 DAY AND 1=0 - CAST(COALESCE(SUM(cr.energy), 0) AS SIGNED) - 0 - => 1 行 -============================================================================== -### 2a. cabinets 排行(总管理员) -SQL> SELECT c.id, COALESCE(c.abstract_id, '') AS abstract_id, c.name, CAST(COALESCE(cr_today.today_total, 0) AS SIGNED) AS today_energy, CAST(COALESCE(cr_month.month_total, 0) AS SIGNED) AS month_energy FROM cabinets c LEFT JOIN ( SELECT cr.cabinet_id, SUM(cr.energy) AS today_total FROM charge_records cr WHERE cr.start_time >= CURDATE() AND cr.start_time < CURDATE() + INTERVAL 1 DAY GROUP BY cr.cabinet_id ) cr_today ON c.id = cr_today.cabinet_id LEFT JOIN ( SELECT cr.cabinet_id, SUM(cr.energy) AS month_total FROM charge_records cr WHERE cr.start_time >= DATE_FORMAT(CURDATE(), '%Y-%m-01') AND cr.start_time < DATE_FORMAT(CURDATE() + INTERVAL 1 MONTH, '%Y-%m-01') GROUP BY cr.cabinet_id ) cr_month ON c.id = cr_month.cabinet_id ORDER BY today_energy DESC, c.id ASC - id | abstract_id | name | today_energy | month_energy - 3 | 99-00000002 | 能耗验证临时柜B | 440 | 3440 - 2 | 99-00000001 | 能耗验证临时柜A | 330 | 2330 - 1 | | 联调测试柜 | 220 | 1220 - => 3 行 -============================================================================== -### 2b. cabinets 排行(组织 1 隔离) -SQL> SELECT c.id, COALESCE(c.abstract_id, '') AS abstract_id, c.name, CAST(COALESCE(cr_today.today_total, 0) AS SIGNED) AS today_energy, CAST(COALESCE(cr_month.month_total, 0) AS SIGNED) AS month_energy FROM cabinets c LEFT JOIN ( SELECT cr.cabinet_id, SUM(cr.energy) AS today_total FROM charge_records cr WHERE cr.start_time >= CURDATE() AND cr.start_time < CURDATE() + INTERVAL 1 DAY GROUP BY cr.cabinet_id ) cr_today ON c.id = cr_today.cabinet_id LEFT JOIN ( SELECT cr.cabinet_id, SUM(cr.energy) AS month_total FROM charge_records cr WHERE cr.start_time >= DATE_FORMAT(CURDATE(), '%Y-%m-01') AND cr.start_time < DATE_FORMAT(CURDATE() + INTERVAL 1 MONTH, '%Y-%m-01') GROUP BY cr.cabinet_id ) cr_month ON c.id = cr_month.cabinet_id JOIN projects p ON c.project_id = p.id AND p.organization_id = 1 ORDER BY today_energy DESC, c.id ASC - id | abstract_id | name | today_energy | month_energy - 3 | 99-00000002 | 能耗验证临时柜B | 440 | 3440 - 2 | 99-00000001 | 能耗验证临时柜A | 330 | 2330 - 1 | | 联调测试柜 | 220 | 1220 - => 3 行 -============================================================================== -### 2c. cabinets 排行(客户侧无组织 ⇒ JOIN projects p ON 1=0 ⇒ 空) -SQL> SELECT c.id, COALESCE(c.abstract_id, '') AS abstract_id, c.name, CAST(COALESCE(cr_today.today_total, 0) AS SIGNED) AS today_energy, CAST(COALESCE(cr_month.month_total, 0) AS SIGNED) AS month_energy FROM cabinets c LEFT JOIN ( SELECT cr.cabinet_id, SUM(cr.energy) AS today_total FROM charge_records cr WHERE cr.start_time >= CURDATE() AND cr.start_time < CURDATE() + INTERVAL 1 DAY GROUP BY cr.cabinet_id ) cr_today ON c.id = cr_today.cabinet_id LEFT JOIN ( SELECT cr.cabinet_id, SUM(cr.energy) AS month_total FROM charge_records cr WHERE cr.start_time >= DATE_FORMAT(CURDATE(), '%Y-%m-01') AND cr.start_time < DATE_FORMAT(CURDATE() + INTERVAL 1 MONTH, '%Y-%m-01') GROUP BY cr.cabinet_id ) cr_month ON c.id = cr_month.cabinet_id JOIN projects p ON 1=0 ORDER BY today_energy DESC, c.id ASC - id | abstract_id | name | today_energy | month_energy - => 0 行 -============================================================================== -### 3a. projects 统计(总管理员) -SQL> SELECT p.id, p.name, COUNT(DISTINCT c.id) AS cabinet_count, CAST(COALESCE(SUM(cr_today.today_total), 0) AS SIGNED) AS today_energy, CAST(COALESCE(SUM(cr_month.month_total), 0) AS SIGNED) AS month_energy FROM projects p LEFT JOIN cabinets c ON p.id = c.project_id LEFT JOIN ( SELECT cr.cabinet_id, SUM(cr.energy) AS today_total FROM charge_records cr WHERE cr.start_time >= CURDATE() AND cr.start_time < CURDATE() + INTERVAL 1 DAY GROUP BY cr.cabinet_id ) cr_today ON c.id = cr_today.cabinet_id LEFT JOIN ( SELECT cr.cabinet_id, SUM(cr.energy) AS month_total FROM charge_records cr WHERE cr.start_time >= DATE_FORMAT(CURDATE(), '%Y-%m-01') AND cr.start_time < DATE_FORMAT(CURDATE() + INTERVAL 1 MONTH, '%Y-%m-01') GROUP BY cr.cabinet_id ) cr_month ON c.id = cr_month.cabinet_id GROUP BY p.id, p.name ORDER BY today_energy DESC, p.id ASC - id | name | cabinet_count | today_energy | month_energy - 2 | 能耗验证临时项目 | 2 | 770 | 5770 - 1 | 联调测试项目 | 1 | 220 | 1220 - => 2 行 -============================================================================== -### 3b. projects 统计(组织 1 隔离) -SQL> SELECT p.id, p.name, COUNT(DISTINCT c.id) AS cabinet_count, CAST(COALESCE(SUM(cr_today.today_total), 0) AS SIGNED) AS today_energy, CAST(COALESCE(SUM(cr_month.month_total), 0) AS SIGNED) AS month_energy FROM projects p LEFT JOIN cabinets c ON p.id = c.project_id LEFT JOIN ( SELECT cr.cabinet_id, SUM(cr.energy) AS today_total FROM charge_records cr WHERE cr.start_time >= CURDATE() AND cr.start_time < CURDATE() + INTERVAL 1 DAY GROUP BY cr.cabinet_id ) cr_today ON c.id = cr_today.cabinet_id LEFT JOIN ( SELECT cr.cabinet_id, SUM(cr.energy) AS month_total FROM charge_records cr WHERE cr.start_time >= DATE_FORMAT(CURDATE(), '%Y-%m-01') AND cr.start_time < DATE_FORMAT(CURDATE() + INTERVAL 1 MONTH, '%Y-%m-01') GROUP BY cr.cabinet_id ) cr_month ON c.id = cr_month.cabinet_id WHERE p.organization_id = 1 GROUP BY p.id, p.name ORDER BY today_energy DESC, p.id ASC - id | name | cabinet_count | today_energy | month_energy - 2 | 能耗验证临时项目 | 2 | 770 | 5770 - 1 | 联调测试项目 | 1 | 220 | 1220 - => 2 行 -============================================================================== -### 3c. projects 统计(客户侧无组织 ⇒ WHERE 1=0) -SQL> SELECT p.id, p.name, COUNT(DISTINCT c.id) AS cabinet_count, CAST(COALESCE(SUM(cr_today.today_total), 0) AS SIGNED) AS today_energy, CAST(COALESCE(SUM(cr_month.month_total), 0) AS SIGNED) AS month_energy FROM projects p LEFT JOIN cabinets c ON p.id = c.project_id LEFT JOIN ( SELECT cr.cabinet_id, SUM(cr.energy) AS today_total FROM charge_records cr WHERE cr.start_time >= CURDATE() AND cr.start_time < CURDATE() + INTERVAL 1 DAY GROUP BY cr.cabinet_id ) cr_today ON c.id = cr_today.cabinet_id LEFT JOIN ( SELECT cr.cabinet_id, SUM(cr.energy) AS month_total FROM charge_records cr WHERE cr.start_time >= DATE_FORMAT(CURDATE(), '%Y-%m-01') AND cr.start_time < DATE_FORMAT(CURDATE() + INTERVAL 1 MONTH, '%Y-%m-01') GROUP BY cr.cabinet_id ) cr_month ON c.id = cr_month.cabinet_id WHERE 1=0 GROUP BY p.id, p.name ORDER BY today_energy DESC, p.id ASC - id | name | cabinet_count | today_energy | month_energy - => 0 行 -============================================================================== -### 4a. trend granularity=day(总管理员, range=30) -SQL> SELECT DATE_FORMAT(cr.start_time, '%Y-%m-%d') AS period, CAST(COALESCE(SUM(cr.energy), 0) AS SIGNED) AS energy, CAST(COUNT(*) AS SIGNED) AS charge_count FROM charge_records cr WHERE cr.start_time >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) GROUP BY period ORDER BY period ASC - period | energy | charge_count - 2026-09-19 | 6000 | 3 - 2026-09-20 | 990 | 3 - => 2 行 -============================================================================== -### 4b. trend granularity=week(range=30) -SQL> SELECT DATE_FORMAT(cr.start_time, '%Y-%u') AS period, CAST(COALESCE(SUM(cr.energy), 0) AS SIGNED) AS energy, CAST(COUNT(*) AS SIGNED) AS charge_count FROM charge_records cr WHERE cr.start_time >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) GROUP BY period ORDER BY period ASC - period | energy | charge_count - 2026-38 | 6990 | 6 - => 1 行 -============================================================================== -### 4c. trend granularity=month(range=30) -SQL> SELECT DATE_FORMAT(cr.start_time, '%Y-%m') AS period, CAST(COALESCE(SUM(cr.energy), 0) AS SIGNED) AS energy, CAST(COUNT(*) AS SIGNED) AS charge_count FROM charge_records cr WHERE cr.start_time >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) GROUP BY period ORDER BY period ASC - period | energy | charge_count - 2026-09 | 6990 | 6 - => 1 行 -============================================================================== -### 4d. trend granularity=day(组织 1 隔离) -SQL> SELECT DATE_FORMAT(cr.start_time, '%Y-%m-%d') AS period, CAST(COALESCE(SUM(cr.energy), 0) AS SIGNED) AS energy, CAST(COUNT(*) AS SIGNED) AS charge_count FROM charge_records cr WHERE cr.start_time >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) AND cr.cabinet_id IN (SELECT c.id FROM cabinets c JOIN projects p ON c.project_id = p.id WHERE p.organization_id = 1) GROUP BY period ORDER BY period ASC - period | energy | charge_count - 2026-09-19 | 6000 | 3 - 2026-09-20 | 990 | 3 - => 2 行 -============================================================================== -### 4e. trend range=-5 **未经夹紧**(DATE_SUB 变成未来 5 天 ⇒ 必然空) -SQL> SELECT DATE_FORMAT(cr.start_time, '%Y-%m-%d') AS period, CAST(COALESCE(SUM(cr.energy), 0) AS SIGNED) AS energy, CAST(COUNT(*) AS SIGNED) AS charge_count FROM charge_records cr WHERE cr.start_time >= DATE_SUB(CURDATE(), INTERVAL -5 DAY) GROUP BY period ORDER BY period ASC - period | energy | charge_count - => 0 行 -============================================================================== -### 4f. trend range=-5 经 clamp_range ⇒ 1(应含昨天+今天) -SQL> SELECT DATE_FORMAT(cr.start_time, '%Y-%m-%d') AS period, CAST(COALESCE(SUM(cr.energy), 0) AS SIGNED) AS energy, CAST(COUNT(*) AS SIGNED) AS charge_count FROM charge_records cr WHERE cr.start_time >= DATE_SUB(CURDATE(), INTERVAL 1 DAY) GROUP BY period ORDER BY period ASC - period | energy | charge_count - 2026-09-19 | 6000 | 3 - 2026-09-20 | 990 | 3 - => 2 行 -============================================================================== -### 5a. export 明细(无筛选) -SQL> SELECT DATE(cr.start_time) AS stat_date, cr.cabinet_id, c.name AS cabinet_name, p.name AS project_name, CAST(COALESCE(SUM(cr.energy), 0) AS SIGNED) AS total_energy, CAST(COUNT(*) AS SIGNED) AS charge_count FROM charge_records cr LEFT JOIN cabinets c ON cr.cabinet_id = c.id LEFT JOIN projects p ON c.project_id = p.id WHERE cr.start_time IS NOT NULL GROUP BY DATE(cr.start_time), cr.cabinet_id, c.name, p.name ORDER BY stat_date DESC, cr.cabinet_id ASC LIMIT 100000 - stat_date | cabinet_id | cabinet_name | project_name | total_energy | charge_count - 2026-09-20 | 1 | 联调测试柜 | 联调测试项目 | 220 | 1 - 2026-09-20 | 2 | 能耗验证临时柜A | 能耗验证临时项目 | 330 | 1 - 2026-09-20 | 3 | 能耗验证临时柜B | 能耗验证临时项目 | 440 | 1 - 2026-09-19 | 1 | 联调测试柜 | 联调测试项目 | 1000 | 1 - 2026-09-19 | 2 | 能耗验证临时柜A | 能耗验证临时项目 | 2000 | 1 - 2026-09-19 | 3 | 能耗验证临时柜B | 能耗验证临时项目 | 3000 | 1 - => 6 行 -============================================================================== -### 5b. export 明细(end_date=今天必须含当天) -SQL> SELECT DATE(cr.start_time) AS stat_date, cr.cabinet_id, c.name AS cabinet_name, p.name AS project_name, CAST(COALESCE(SUM(cr.energy), 0) AS SIGNED) AS total_energy, CAST(COUNT(*) AS SIGNED) AS charge_count FROM charge_records cr LEFT JOIN cabinets c ON cr.cabinet_id = c.id LEFT JOIN projects p ON c.project_id = p.id WHERE cr.start_time IS NOT NULL AND cr.start_time >= DATE_SUB(CURDATE(), INTERVAL 1 DAY) AND cr.start_time < DATE_ADD(CURDATE(), INTERVAL 1 DAY) GROUP BY DATE(cr.start_time), cr.cabinet_id, c.name, p.name ORDER BY stat_date DESC, cr.cabinet_id ASC LIMIT 100000 - stat_date | cabinet_id | cabinet_name | project_name | total_energy | charge_count - 2026-09-20 | 1 | 联调测试柜 | 联调测试项目 | 220 | 1 - 2026-09-20 | 2 | 能耗验证临时柜A | 能耗验证临时项目 | 330 | 1 - 2026-09-20 | 3 | 能耗验证临时柜B | 能耗验证临时项目 | 440 | 1 - 2026-09-19 | 1 | 联调测试柜 | 联调测试项目 | 1000 | 1 - 2026-09-19 | 2 | 能耗验证临时柜A | 能耗验证临时项目 | 2000 | 1 - 2026-09-19 | 3 | 能耗验证临时柜B | 能耗验证临时项目 | 3000 | 1 - => 6 行 -============================================================================== -### 5c. export 明细(组织 1 隔离) -SQL> SELECT DATE(cr.start_time) AS stat_date, cr.cabinet_id, c.name AS cabinet_name, p.name AS project_name, CAST(COALESCE(SUM(cr.energy), 0) AS SIGNED) AS total_energy, CAST(COUNT(*) AS SIGNED) AS charge_count FROM charge_records cr LEFT JOIN cabinets c ON cr.cabinet_id = c.id LEFT JOIN projects p ON c.project_id = p.id WHERE cr.start_time IS NOT NULL AND cr.cabinet_id IN (SELECT c.id FROM cabinets c JOIN projects p ON c.project_id = p.id WHERE p.organization_id = 1) GROUP BY DATE(cr.start_time), cr.cabinet_id, c.name, p.name ORDER BY stat_date DESC, cr.cabinet_id ASC LIMIT 100000 - stat_date | cabinet_id | cabinet_name | project_name | total_energy | charge_count - 2026-09-20 | 1 | 联调测试柜 | 联调测试项目 | 220 | 1 - 2026-09-20 | 2 | 能耗验证临时柜A | 能耗验证临时项目 | 330 | 1 - 2026-09-20 | 3 | 能耗验证临时柜B | 能耗验证临时项目 | 440 | 1 - 2026-09-19 | 1 | 联调测试柜 | 联调测试项目 | 1000 | 1 - 2026-09-19 | 2 | 能耗验证临时柜A | 能耗验证临时项目 | 2000 | 1 - 2026-09-19 | 3 | 能耗验证临时柜B | 能耗验证临时项目 | 3000 | 1 - => 6 行 -============================================================================== -### 9. 对照:energy_stats 表行数(已无人读写) -SQL> SELECT COUNT(*) AS energy_stats_rows FROM energy_stats - energy_stats_rows - 0 - => 1 行 diff --git a/.tmp-energy-verify/temp_ids.json b/.tmp-energy-verify/temp_ids.json deleted file mode 100644 index c26018a..0000000 --- a/.tmp-energy-verify/temp_ids.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "user_id": 7, - "project_id": 2, - "cabinet_ids": [ - 2, - 3 - ], - "board_ids": [ - 3, - 4 - ], - "compartment_ids": [ - 5, - 6 - ], - "record_ids": [ - 10, - 11, - 12, - 13, - 14, - 15 - ] -} \ No newline at end of file diff --git a/.tmp-energy-verify/temp_ids_orgB.json b/.tmp-energy-verify/temp_ids_orgB.json deleted file mode 100644 index ba466f1..0000000 --- a/.tmp-energy-verify/temp_ids_orgB.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "org_id": 4, - "project_id": 3, - "cabinet_id": 4, - "board_id": 5, - "compartment_id": 7, - "record_id": 16 -} \ No newline at end of file diff --git a/.tmp-energy-verify/timezone_check.txt b/.tmp-energy-verify/timezone_check.txt deleted file mode 100644 index 4d7c7fb..0000000 --- a/.tmp-energy-verify/timezone_check.txt +++ /dev/null @@ -1,6 +0,0 @@ -默认会话时区 : ('+08:00', '+08:00', datetime.datetime(2026, 9, 20, 1, 44, 32), datetime.date(2026, 9, 20)) -模拟 sqlx(+00:00) : ('+00:00', datetime.datetime(2026, 9, 19, 17, 44, 32), datetime.date(2026, 9, 19)) -UTC 会话下 today : 6000 (服务实测返回 6000 ✓) -UTC 会话下 month : 6990 (服务实测返回 6990 ✓) -+08:00 会话读 start_time : ((10, 1, datetime.datetime(2026, 9, 20, 10, 0)), (11, 1, datetime.datetime(2026, 9, 19, 10, 0)), (12, 2, datetime.datetime(2026, 9, 20, 10, 0))) -+00:00 会话读 start_time : ((10, 1, datetime.datetime(2026, 9, 20, 2, 0)), (11, 1, datetime.datetime(2026, 9, 19, 2, 0)), (12, 2, datetime.datetime(2026, 9, 20, 2, 0))) diff --git a/.tmp-energy-verify/token.txt b/.tmp-energy-verify/token.txt deleted file mode 100644 index bfe9433..0000000 --- a/.tmp-energy-verify/token.txt +++ /dev/null @@ -1 +0,0 @@ -eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjcsInBob25lIjoiMTk5MDAwMDAwMDEiLCJleHAiOjE3ODk5MjU5NDYsImlhdCI6MTc4OTgzOTU0Nn0.X-T6uBTENj9UK9seDIyyJJXopJcNJfSfDZ1C60AP-yA \ No newline at end of file diff --git a/.tmp-energy-verify/token_local.txt b/.tmp-energy-verify/token_local.txt deleted file mode 100644 index 8e63930..0000000 --- a/.tmp-energy-verify/token_local.txt +++ /dev/null @@ -1 +0,0 @@ -eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjcsInBob25lIjoiMTk5MDAwMDAwMDEiLCJleHAiOjE3ODk5MjU5ODksImlhdCI6MTc4OTgzOTU4OX0.ZiKHmR2fKvfSLaINjDq_R9npcFldGqP4WOQessAotxg \ No newline at end of file diff --git a/.tmp-energy-verify/token_testenv.txt b/.tmp-energy-verify/token_testenv.txt deleted file mode 100644 index 1f88283..0000000 --- a/.tmp-energy-verify/token_testenv.txt +++ /dev/null @@ -1 +0,0 @@ -eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjcsInBob25lIjoiMTk5MDAwMDAwMDEiLCJleHAiOjE3ODk5MjU5ODksImlhdCI6MTc4OTgzOTU4OX0.Y0Tju_TsC07S-qyIhD4cRpT8I1uqzTuCGWVH6QOorJM \ No newline at end of file diff --git a/.tmp-energy-verify/verify_endpoints.sh b/.tmp-energy-verify/verify_endpoints.sh deleted file mode 100644 index c9c42d9..0000000 --- a/.tmp-energy-verify/verify_endpoints.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash -# 能耗报表接口验证脚本(在测试服务器上执行,直连本机 :3000,绕开 nginx) -# 用法: bash verify_endpoints.sh [BASE] -TOKEN="$1" -BASE="${2:-http://127.0.0.1:3000/api}" -echo "### BASE=$BASE" -for path in "energy-stats/summary" "energy-stats/cabinets" "energy-stats/projects" "energy-stats/trend?granularity=day&range=30" "energy-stats/trend?granularity=week&range=30" "energy-stats/trend?granularity=month&range=30"; do - echo "--- GET /api/$path" - curl -s -m 20 -w "\n[HTTP %{http_code}]\n" -H "Authorization: Bearer $TOKEN" "$BASE/$path" - echo -done -echo "=== 导出接口(异步任务) ===" -curl -s -m 20 -w "\n[HTTP %{http_code}]\n" -H "Authorization: Bearer $TOKEN" "$BASE/energy-stats/export?start_date=2026-09-19&end_date=2026-09-20" diff --git a/.tmp-energy-verify/verify_live_testenv.py b/.tmp-energy-verify/verify_live_testenv.py deleted file mode 100644 index 9bdbaa4..0000000 --- a/.tmp-energy-verify/verify_live_testenv.py +++ /dev/null @@ -1,69 +0,0 @@ -"""对**当前线上测试环境**(test.anzhizhichong.com → :3000,另一 agent 于 01:50 重新部署的 -合并版二进制)做一次端到端验证: - -插入 2 条临时 charge_records → 调 4 个接口(经 nginx + HTTPS)→ 删除临时数据。 - -服务端 sqlx 会话时区为 +00:00,所以"今天(UTC)"= 2026-09-19。 -""" -import json -import time -import pymysql -import jwt -import requests - -DB = dict(host="10.8.0.252", user="root", password="Hbhyg731024@", database="pms_dev") -SECRET = "32e8708743c42fe764aff1d40737a63803a46815a9501f4bcb50c41fe3d27108" -BASE = "https://test.anzhizhichong.com/api/pms" - -conn = pymysql.connect(**DB) -cur = conn.cursor() - -# ---- 临时总管理员 -cur.execute( - "INSERT INTO users (phone, name, password, role, organization_id, status) " - "VALUES ('19900000002','能耗复验临时管理员','x',2,NULL,1)" -) -user_id = cur.lastrowid - -# ---- 2 条临时充电记录(挂在已有柜子 1 / 仓体 1 上):昨天 222Wh + 今天(UTC) 333Wh -records = [] -for energy, day_expr in ((333, "CURDATE()"), (222, "DATE_SUB(CURDATE(), INTERVAL 1 DAY)")): - cur.execute( - "INSERT INTO charge_records " - "(compartment_id, cabinet_id, start_time, end_time, energy, max_temperature, status) " - f"VALUES (1, 1, {day_expr} + INTERVAL 10 HOUR, {day_expr} + INTERVAL 12 HOUR, %s, 29815, 0)", - (energy,), - ) - records.append(cur.lastrowid) -conn.commit() -print(f"插入临时数据: user_id={user_id} charge_records={records} (333Wh + 222Wh)") - -now = int(time.time()) -token = jwt.encode({"sub": user_id, "phone": "19900000002", - "exp": now + 3600, "iat": now}, SECRET, algorithm="HS256") -headers = {"Authorization": f"Bearer {token}"} - -paths = [ - "/energy-stats/summary", - "/energy-stats/cabinets", - "/energy-stats/projects", - "/energy-stats/trend?granularity=day&range=30", -] -try: - for p in paths: - r = requests.get(BASE + p, headers=headers, timeout=20) - print(f"\n--- GET /api/pms{p} [HTTP {r.status_code}]") - try: - print(json.dumps(r.json(), ensure_ascii=False)) - except ValueError: - print(r.text[:400]) -finally: - # ---- 清理 - cur.execute("DELETE FROM charge_records WHERE id IN (%s,%s)", records) - cur.execute("DELETE FROM users WHERE id = %s", (user_id,)) - conn.commit() - print(f"\n已清理: charge_records {records} 与 user_id={user_id}") - cur.execute("SELECT COUNT(*) FROM charge_records") - print("charge_records 剩余行数 =", cur.fetchone()[0]) - cur.close() - conn.close() diff --git a/.tmp-energy-verify/verify_live_testenv_output.txt b/.tmp-energy-verify/verify_live_testenv_output.txt deleted file mode 100644 index e07c138..0000000 --- a/.tmp-energy-verify/verify_live_testenv_output.txt +++ /dev/null @@ -1,16 +0,0 @@ -插入临时数据: user_id=9 charge_records=[17, 18] (333Wh + 222Wh) - ---- GET /api/pms/energy-stats/summary [HTTP 200] -{"code": 0, "data": {"cabinet_count": 1, "estimated_cost": 0.44, "month_energy": 555, "online_count": 1, "today_energy": 222}, "message": "ok"} - ---- GET /api/pms/energy-stats/cabinets [HTTP 200] -{"code": 0, "data": [{"abstract_id": "", "cabinet_id": 1, "month_energy": 555, "name": "TCP-E2E-SMOKE", "today_energy": 222}], "message": "ok"} - ---- GET /api/pms/energy-stats/projects [HTTP 200] -{"code": 0, "data": [{"cabinet_count": 1, "month_energy": 555, "name": "联调测试项目", "project_id": 1, "today_energy": 222}], "message": "ok"} - ---- GET /api/pms/energy-stats/trend?granularity=day&range=30 [HTTP 200] -{"code": 0, "data": {"granularity": "day", "list": [{"charge_count": 1, "energy": 222, "period": "2026-09-19"}, {"charge_count": 1, "energy": 333, "period": "2026-09-20"}]}, "message": "ok"} - -已清理: charge_records [17, 18] 与 user_id=9 -charge_records 剩余行数 = 0 diff --git a/.tmp-energy-verify/verify_new_binary.txt b/.tmp-energy-verify/verify_new_binary.txt deleted file mode 100644 index 1b2dd78..0000000 --- a/.tmp-energy-verify/verify_new_binary.txt +++ /dev/null @@ -1,28 +0,0 @@ -### BASE=http://127.0.0.1:3000/api ---- GET /api/energy-stats/summary -{"code":0,"data":{"cabinet_count":3,"estimated_cost":5.59,"month_energy":6990,"online_count":3,"today_energy":6000},"message":"ok"} -[HTTP 200] - ---- GET /api/energy-stats/cabinets -{"code":0,"data":[{"abstract_id":"99-00000002","cabinet_id":3,"month_energy":3440,"name":"能耗验证临时柜B","today_energy":3000},{"abstract_id":"99-00000001","cabinet_id":2,"month_energy":2330,"name":"能耗验证临时柜A","today_energy":2000},{"abstract_id":"","cabinet_id":1,"month_energy":1220,"name":"联调测试柜","today_energy":1000}],"message":"ok"} -[HTTP 200] - ---- GET /api/energy-stats/projects -{"code":0,"data":[{"cabinet_count":2,"month_energy":5770,"name":"能耗验证临时项目","project_id":2,"today_energy":5000},{"cabinet_count":1,"month_energy":1220,"name":"联调测试项目","project_id":1,"today_energy":1000}],"message":"ok"} -[HTTP 200] - ---- GET /api/energy-stats/trend?granularity=day&range=30 -{"code":0,"data":{"granularity":"day","list":[{"charge_count":3,"energy":6000,"period":"2026-09-19"},{"charge_count":3,"energy":990,"period":"2026-09-20"}]},"message":"ok"} -[HTTP 200] - ---- GET /api/energy-stats/trend?granularity=week&range=30 -{"code":0,"data":{"granularity":"week","list":[{"charge_count":6,"energy":6990,"period":"2026-38"}]},"message":"ok"} -[HTTP 200] - ---- GET /api/energy-stats/trend?granularity=month&range=30 -{"code":0,"data":{"granularity":"month","list":[{"charge_count":6,"energy":6990,"period":"2026-09"}]},"message":"ok"} -[HTTP 200] - -=== 导出接口(异步任务) === -{"code":0,"data":{"task_id":2},"message":"导出任务已创建"} -[HTTP 200] diff --git a/.tmp-energy-verify/verify_sql.py b/.tmp-energy-verify/verify_sql.py deleted file mode 100644 index 70ed880..0000000 --- a/.tmp-energy-verify/verify_sql.py +++ /dev/null @@ -1,160 +0,0 @@ -"""用 pymysql 直接跑改后的 SQL(与 routes/energy_stats.rs 中的语句逐字对应, -唯一差别:Rust 用 `?` 占位符 + bind,这里把测试常量(org_id=1 / range=30) -直接内联,以保持 SQL 文本与 Rust 生成的完全一致,并避开 pymysql 的 `%` 转义)。 - -只做只读 SELECT,不改任何数据。 -""" -import pymysql - -DB = dict(host="10.8.0.252", user="root", password="Hbhyg731024@", database="pms_dev") -conn = pymysql.connect(**DB) -cur = conn.cursor() - -TODAY = "cr.start_time >= CURDATE() AND cr.start_time < CURDATE() + INTERVAL 1 DAY" -THIS_MONTH = ( - "cr.start_time >= DATE_FORMAT(CURDATE(), '%Y-%m-01') " - "AND cr.start_time < DATE_FORMAT(CURDATE() + INTERVAL 1 MONTH, '%Y-%m-01')" -) -SUM_E = "CAST(COALESCE(SUM(cr.energy), 0) AS SIGNED)" -CNT_E = "CAST(COUNT(*) AS SIGNED)" - -# 组织隔离片段(与 auth::org_condition(user, "cr") / 各处的内联版本逐字一致) -ORG_WHERE_1 = (" AND cr.cabinet_id IN (SELECT c.id FROM cabinets c JOIN projects p " - "ON c.project_id = p.id WHERE p.organization_id = 1)") -ORG_WHERE_999 = (" AND cr.cabinet_id IN (SELECT c.id FROM cabinets c JOIN projects p " - "ON c.project_id = p.id WHERE p.organization_id = 999)") -ORG_WHERE_NONE = " AND 1=0" -ORG_JOIN_1 = " JOIN projects p ON c.project_id = p.id AND p.organization_id = 1" -ORG_JOIN_NONE = " JOIN projects p ON 1=0" -PROJ_ORG_1 = " WHERE p.organization_id = 1" -PROJ_ORG_NONE = " WHERE 1=0" - - -def show(title, sql): - print("=" * 78) - print(f"### {title}") - print("SQL> " + " ".join(sql.split())) - cur.execute(sql) - cols = [d[0] for d in cur.description] - print(" " + " | ".join(cols)) - rows = cur.fetchall() - for r in rows: - print(" " + " | ".join(str(v) for v in r)) - print(f" => {len(rows)} 行") - return rows - - -# ---------------------------------------------------------------- 原始数据 -show( - "0. charge_records 现有行(cabinet_id / start_time / energy)", - "SELECT cr.id, cr.cabinet_id, c.name, cr.start_time, cr.end_time, cr.energy " - "FROM charge_records cr LEFT JOIN cabinets c ON cr.cabinet_id = c.id " - "ORDER BY cr.start_time DESC, cr.cabinet_id", -) - -# ---------------------------------------------------------------- ① summary -show("1a. summary today(总管理员,无组织条件)", - f"SELECT {SUM_E} FROM charge_records cr WHERE {TODAY}") -show("1b. summary month(总管理员)", - f"SELECT {SUM_E} FROM charge_records cr WHERE {THIS_MONTH}") -show("1c. summary today(组织 1 隔离)", - f"SELECT {SUM_E} FROM charge_records cr WHERE {TODAY}{ORG_WHERE_1}") -show("1d. summary today(不存在组织 999 ⇒ 必须为空集 0)", - f"SELECT {SUM_E} FROM charge_records cr WHERE {TODAY}{ORG_WHERE_999}") -show("1e. summary today(客户侧无组织 ⇒ AND 1=0)", - f"SELECT {SUM_E} FROM charge_records cr WHERE {TODAY}{ORG_WHERE_NONE}") - -# ---------------------------------------------------------------- ② cabinets -RANK = f"""SELECT c.id, COALESCE(c.abstract_id, '') AS abstract_id, c.name, - CAST(COALESCE(cr_today.today_total, 0) AS SIGNED) AS today_energy, - CAST(COALESCE(cr_month.month_total, 0) AS SIGNED) AS month_energy - FROM cabinets c - LEFT JOIN ( - SELECT cr.cabinet_id, SUM(cr.energy) AS today_total - FROM charge_records cr - WHERE {TODAY} - GROUP BY cr.cabinet_id - ) cr_today ON c.id = cr_today.cabinet_id - LEFT JOIN ( - SELECT cr.cabinet_id, SUM(cr.energy) AS month_total - FROM charge_records cr - WHERE {THIS_MONTH} - GROUP BY cr.cabinet_id - ) cr_month ON c.id = cr_month.cabinet_id - {{org}} - ORDER BY today_energy DESC, c.id ASC""" -show("2a. cabinets 排行(总管理员)", RANK.format(org="")) -show("2b. cabinets 排行(组织 1 隔离)", RANK.format(org=ORG_JOIN_1)) -show("2c. cabinets 排行(客户侧无组织 ⇒ JOIN projects p ON 1=0 ⇒ 空)", - RANK.format(org=ORG_JOIN_NONE)) - -# ---------------------------------------------------------------- ③ projects -PROJ = f"""SELECT p.id, p.name, - COUNT(DISTINCT c.id) AS cabinet_count, - CAST(COALESCE(SUM(cr_today.today_total), 0) AS SIGNED) AS today_energy, - CAST(COALESCE(SUM(cr_month.month_total), 0) AS SIGNED) AS month_energy - FROM projects p - LEFT JOIN cabinets c ON p.id = c.project_id - LEFT JOIN ( - SELECT cr.cabinet_id, SUM(cr.energy) AS today_total - FROM charge_records cr - WHERE {TODAY} - GROUP BY cr.cabinet_id - ) cr_today ON c.id = cr_today.cabinet_id - LEFT JOIN ( - SELECT cr.cabinet_id, SUM(cr.energy) AS month_total - FROM charge_records cr - WHERE {THIS_MONTH} - GROUP BY cr.cabinet_id - ) cr_month ON c.id = cr_month.cabinet_id - {{org}} - GROUP BY p.id, p.name - ORDER BY today_energy DESC, p.id ASC""" -show("3a. projects 统计(总管理员)", PROJ.format(org="")) -show("3b. projects 统计(组织 1 隔离)", PROJ.format(org=PROJ_ORG_1)) -show("3c. projects 统计(客户侧无组织 ⇒ WHERE 1=0)", PROJ.format(org=PROJ_ORG_NONE)) - -# ---------------------------------------------------------------- ④ trend -TREND = f"""SELECT {{period}} AS period, {SUM_E} AS energy, {CNT_E} AS charge_count - FROM charge_records cr - WHERE cr.start_time >= DATE_SUB(CURDATE(), INTERVAL {{rng}} DAY){{org}} - GROUP BY period ORDER BY period ASC""" -show("4a. trend granularity=day(总管理员, range=30)", - TREND.format(period="DATE_FORMAT(cr.start_time, '%Y-%m-%d')", rng=30, org="")) -show("4b. trend granularity=week(range=30)", - TREND.format(period="DATE_FORMAT(cr.start_time, '%Y-%u')", rng=30, org="")) -show("4c. trend granularity=month(range=30)", - TREND.format(period="DATE_FORMAT(cr.start_time, '%Y-%m')", rng=30, org="")) -show("4d. trend granularity=day(组织 1 隔离)", - TREND.format(period="DATE_FORMAT(cr.start_time, '%Y-%m-%d')", rng=30, org=ORG_WHERE_1)) -show("4e. trend range=-5 **未经夹紧**(DATE_SUB 变成未来 5 天 ⇒ 必然空)", - TREND.format(period="DATE_FORMAT(cr.start_time, '%Y-%m-%d')", rng=-5, org="")) -show("4f. trend range=-5 经 clamp_range ⇒ 1(应含昨天+今天)", - TREND.format(period="DATE_FORMAT(cr.start_time, '%Y-%m-%d')", rng=1, org="")) - -# ---------------------------------------------------------------- ⑤ export -EXPORT = f"""SELECT DATE(cr.start_time) AS stat_date, cr.cabinet_id, c.name AS cabinet_name, - p.name AS project_name, - CAST(COALESCE(SUM(cr.energy), 0) AS SIGNED) AS total_energy, - {CNT_E} AS charge_count - FROM charge_records cr - LEFT JOIN cabinets c ON cr.cabinet_id = c.id - LEFT JOIN projects p ON c.project_id = p.id - WHERE cr.start_time IS NOT NULL{{extra}} - GROUP BY DATE(cr.start_time), cr.cabinet_id, c.name, p.name - ORDER BY stat_date DESC, cr.cabinet_id ASC - LIMIT 100000""" -show("5a. export 明细(无筛选)", EXPORT.format(extra="")) -show("5b. export 明细(end_date=今天必须含当天)", - EXPORT.format( - extra=" AND cr.start_time >= DATE_SUB(CURDATE(), INTERVAL 1 DAY)" - " AND cr.start_time < DATE_ADD(CURDATE(), INTERVAL 1 DAY)" - )) -show("5c. export 明细(组织 1 隔离)", EXPORT.format(extra=ORG_WHERE_1)) - -# ---------------------------------------------------------------- 对照 -show("9. 对照:energy_stats 表行数(已无人读写)", - "SELECT COUNT(*) AS energy_stats_rows FROM energy_stats") - -cur.close() -conn.close() diff --git a/.tmp-schema-align/__pycache__/inspect.cpython-314.pyc b/.tmp-schema-align/__pycache__/inspect.cpython-314.pyc deleted file mode 100644 index 44c5b58..0000000 Binary files a/.tmp-schema-align/__pycache__/inspect.cpython-314.pyc and /dev/null differ diff --git a/.tmp-schema-align/cargo-check.txt b/.tmp-schema-align/cargo-check.txt deleted file mode 100644 index 34694d3..0000000 --- a/.tmp-schema-align/cargo-check.txt +++ /dev/null @@ -1,2 +0,0 @@ - Checking pms-api v0.1.0 (D:\charging-cabinet\software\api-server) - Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.23s diff --git a/.tmp-schema-align/cargo-check2.txt b/.tmp-schema-align/cargo-check2.txt deleted file mode 100644 index a600461..0000000 --- a/.tmp-schema-align/cargo-check2.txt +++ /dev/null @@ -1 +0,0 @@ - Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.80s diff --git a/.tmp-schema-align/cargo-test.txt b/.tmp-schema-align/cargo-test.txt deleted file mode 100644 index a45b690..0000000 --- a/.tmp-schema-align/cargo-test.txt +++ /dev/null @@ -1,151 +0,0 @@ - Finished `test` profile [unoptimized + debuginfo] target(s) in 0.46s - Running unittests src\main.rs (.cargo-check-target\debug\deps\pms_api-0e8b7d95b05a137e.exe) - -running 144 tests -test commands::tests::test_is_valid_command_target_accepts_board_and_compartment ... ok -test commands::tests::test_is_valid_command_target_rejects_imei_and_malformed ... ok -test commands::tests::test_take_command_target_rejects_invalid_id ... ok -test db::migrate::tests::test_alignment_targets_cover_known_broken_columns ... ok -test db::migrate::tests::test_every_alignment_target_appears_verbatim_in_schema_sql ... ok -test db::migrate::tests::test_column_spec_from_target_parses_all_dimensions ... ok -test commands::tests::test_take_command_target_absent_returns_none ... ok -test db::migrate::tests::test_referenced_tables_single_foreign_key_returns_table_name ... ok -test db::migrate::tests::test_schema_sql_energy_columns_comment_unit_is_wh ... ok -test db::migrate::tests::test_schema_sql_table_order_has_forward_reference_returns_none ... ok -test commands::tests::test_take_command_target_extracts_and_removes_id ... ok -test middleware::auth::tests::test_append_condition_handles_empty_clause ... ok -test middleware::auth::tests::test_builtin_role_name_covers_all_valid_levels ... ok -test middleware::auth::tests::test_builtin_role_name_invalid_level_returns_none ... ok -test middleware::auth::tests::test_builtin_role_name_operator_maps_to_normal_user ... ok -test db::migrate::tests::test_schema_sql_contains_no_timestamp_column ... ok -test middleware::auth::tests::test_builtin_role_name_org_admin_maps_to_org_admin ... ok -test middleware::auth::tests::test_builtin_role_name_super_admin_maps_to_super_admin ... ok -test middleware::auth::tests::test_check_permission_operator_empty_permissions_forbidden ... ok -test middleware::auth::tests::test_org_condition_no_org ... ok -test middleware::auth::tests::test_org_condition_normal_admin ... ok -test middleware::auth::tests::test_org_condition_super_admin ... ok -test middleware::auth::tests::test_super_admin_role_level_constant_matches_mapping ... ok -test middleware::auth::tests::test_user_scope_allows_project ... ok -test middleware::auth::tests::test_user_scope_default_is_unrestricted ... ok -test middleware::auth::tests::test_user_scope_project_condition_narrows_by_project ... ok -test middleware::auth::tests::test_verify_password_invalid_hash ... ok -test routes::cabinet_tree::tests::test_build_filter_where_scope_applies_to_explicit_project_and_org_params ... ok -test routes::cabinet_tree::tests::test_build_filter_where_scope_combines_with_bound_filter ... ok -test routes::cabinet_tree::tests::test_build_filter_where_super_admin_ignores_scope ... ok -test routes::cabinet_tree::tests::test_build_filter_where_with_scope_appends_project_narrowing ... ok -test routes::cabinet_tree::tests::test_build_filter_where_without_scope_is_unchanged ... ok -test routes::cabinet_tree::tests::test_channel_flags_has_data_and_status_code_match_frontend_mapping ... ok -test routes::cabinet_tree::tests::test_derive_channel_counts_all_idle_channels_yield_zero_full_fault ... ok -test routes::cabinet_tree::tests::test_derive_channel_counts_charging_channels_counted_as_charging ... ok -test routes::cabinet_tree::tests::test_derive_channel_counts_fault_channels_counted_as_fault ... ok -test routes::cabinet_tree::tests::test_derive_channel_counts_fault_wins_over_full_on_conflict ... ok -test routes::cabinet_tree::tests::test_derive_channel_counts_full_channels_counted_as_full ... ok -test routes::cabinet_tree::tests::test_derive_channel_counts_offline_missing_data_yields_zero_full_fault ... ok -test routes::cabinet_tree::tests::test_derive_channel_view_batt_fault_bits_map_to_fault ... ok -test routes::cabinet_tree::tests::test_derive_channel_view_charging_channel_sets_on_true ... ok -test routes::cabinet_tree::tests::test_derive_channel_view_empty_channel_keeps_power_status ... ok -test routes::cabinet_tree::tests::test_derive_channel_view_full_battery_prefers_full_over_charging ... ok -test routes::cabinet_tree::tests::test_derive_channel_view_missing_fields_yields_idle_and_empty_hex ... ok -test routes::cabinet_tree::tests::test_derive_channel_view_normalizes_hex_case_and_padding ... ok -test routes::cabinet_tree::tests::test_derive_channel_view_pow_fault_maps_to_fault ... ok -test routes::cabinet_tree::tests::test_derive_channel_view_single_byte_yields_empty_hex ... ok -test routes::cabinet_tree::tests::test_device_access_sql_appends_scope_only_when_configured ... ok -test routes::cabinet_tree::tests::test_device_list_fields_in_sync ... ok -test routes::cabinet_tree::tests::test_parse_channel_key_accepts_real_channel_keys ... ok -test routes::cabinet_tree::tests::test_parse_channel_key_rejects_internal_keys ... ok -test routes::cabinet_tree::tests::test_parse_channel_key_rejects_out_of_range_and_malformed ... ok -test routes::cabinet_tree::tests::test_parse_hex_byte_rejects_malformed_values ... ok -test routes::cabinet_tree::tests::test_pick_non_empty_hex_keeps_00_but_drops_missing ... ok -test routes::cabinets::tests::test_apply_realtime_keeps_null_when_missing ... ok -test routes::cabinets::tests::test_cabinet_realtime_field_indices_are_in_sync ... ok -test routes::cabinets::tests::test_parse_cabinet_realtime_cabinet_status_bits_raw_hex ... ok -test routes::cabinets::tests::test_parse_cabinet_realtime_cabinet_status_missing_is_null_not_00 ... ok -test routes::cabinets::tests::test_parse_cabinet_realtime_missing_fields_stay_none ... ok -test routes::cabinets::tests::test_parse_cabinet_realtime_reads_voltage_current_and_temp ... ok -test routes::charge_orders::tests::test_order_scope_conditions_applies_scope ... ok -test routes::charge_orders::tests::test_order_scope_conditions_org_isolation ... ok -test routes::charge_orders::tests::test_order_scope_conditions_super_admin_exempt ... ok -test routes::charge_records::tests::test_build_export_where_scope_appends_to_existing_conditions ... ok -test routes::charge_records::tests::test_build_export_where_applies_scope_project_ids ... ok -test routes::energy_stats::tests::test_build_export_where_keeps_org_isolation ... ok -test routes::energy_stats::tests::test_clamp_range_bounds_illegal_values ... ok -test routes::energy_stats::tests::test_build_export_clause_targets_charge_records ... ok -test routes::energy_stats::tests::test_estimated_cost_unit_is_wh_times_80_cent_per_kwh ... ok -test routes::energy_stats::tests::test_build_export_clause_empty_when_no_filters ... ok -test routes::energy_stats::tests::test_time_window_uses_start_time_half_open_ranges ... ok -test routes::h5::tests::test_compartment_realtime_fields_order_matches_indexes ... ok -test routes::h5::tests::test_h5_list_pick_non_empty_hex_keeps_00_but_drops_missing ... ok -test routes::h5::tests::test_parse_compartment_realtime_garbage_is_none ... ok -test routes::h5::tests::test_parse_compartment_realtime_no_data_returns_none_not_zero ... ok -test routes::h5::tests::test_parse_compartment_realtime_reads_all_fields ... ok -test routes::h5::tests::test_parse_compartment_realtime_real_zero_is_kept ... ok -test routes::h5::tests::test_visible_org_scope_operator_sees_own_org_only ... ok -test routes::h5::tests::test_visible_org_scope_org_admin_sees_own_org_only ... ok -test routes::h5::tests::test_visible_org_scope_org_user_without_org_is_empty ... ok -test routes::h5::tests::test_visible_org_scope_super_admin_sees_all ... ok -test routes::h5::tests::test_visible_orgs_own_sql_filters_by_id ... ok -test routes::scope_guard::tests::test_access_sql_with_scope_narrows_by_project ... ok -test routes::scope_guard::tests::test_access_sql_without_scope_has_no_narrowing ... ok -test routes::scope_guard::tests::test_verify_org_ownership_rules ... ok -test routes::tests::test_is_allowed_origin_allows_whitelist ... ok -test routes::tests::test_is_allowed_origin_rejects_others ... ok -test routes::upload_guard::tests::test_sniff_image_ext_recognizes_bitmaps ... ok -test routes::upload_guard::tests::test_validate_image_upload_accepts_real_images ... ok -test routes::upload_guard::tests::test_validate_image_upload_rejects_scriptable_exts ... ok -test routes::upload_guard::tests::test_validate_image_upload_rejects_svg_content_with_image_ext ... ok -test routes::upload_guard::tests::test_validate_image_upload_rejects_unknown_ext ... ok -test workers::energy::tests::test_fixed_interval_is_conservative ... ok -test workers::energy::tests::test_full_hour_charge_magnitude ... ok -test workers::energy::tests::test_mwh_delta_real_world_case ... ok -test workers::energy::tests::test_mwh_delta_rejects_non_positive_inputs ... ok -test workers::energy::tests::test_mwh_delta_small_current_not_lost_by_rounding ... ok -test workers::energy::tests::test_mwh_delta_zero_current_no_accumulation ... ok -test workers::energy::tests::test_mwh_to_wh_rounding ... ok -test workers::energy::tests::test_order_energy_key_format ... ok -test workers::log_retention::tests::test_retention_constants_are_sane ... ok -test workers::report_worker::tests::test_board_channel_id_range_guards ... ok -test workers::report_worker::tests::test_build_spec_complete_topology_prefers_real_topology ... ok -test workers::report_worker::tests::test_build_spec_incomplete_topology_falls_back_to_legacy_cell ... ok -test workers::report_worker::tests::test_build_spec_legacy_cell_clamped_into_protocol_range ... ok -test workers::report_worker::tests::test_build_spec_no_cell_uses_constant_default ... ok -test workers::report_worker::tests::test_channel_battery_key_format_contract ... ok -test workers::report_worker::tests::test_build_spec_never_produces_all_zero_spec ... ok -test workers::report_worker::tests::test_channel_hex_len_constant ... ok -test workers::report_worker::tests::test_channel_status_key_format_contract ... ok -test workers::report_worker::tests::test_compartment_lock_key_is_identity ... ok -test workers::report_worker::tests::test_decide_order_action_boundary_is_strict ... ok -test workers::report_worker::tests::test_decide_order_action_replace_on_different_battery ... ok -test workers::report_worker::tests::test_decide_order_action_skip_same_battery ... ok -test workers::report_worker::tests::test_decide_order_action_create_when_no_active_order ... ok -test workers::report_worker::tests::test_decide_order_action_skip_when_either_battery_id_empty ... ok -test workers::report_worker::tests::test_compartment_lock_allows_different_compartments ... ok -test workers::report_worker::tests::test_normalize_hex_byte_rejects_invalid ... ok -test workers::report_worker::tests::test_is_unique_violation_ignores_non_database_errors ... ok -test workers::report_worker::tests::test_normalize_hex_byte_valid_and_lowercased ... ok -test workers::report_worker::tests::test_order_timeout_bound_goes_backwards ... ok -test workers::report_worker::tests::test_out_of_range_sample_contributes_zero_energy ... ok -test workers::report_worker::tests::test_parse_channel_hex_case_insensitive_and_lowercased ... ok -test workers::report_worker::tests::test_parse_channel_hex_doc_example_bat_in ... ok -test workers::report_worker::tests::test_parse_channel_hex_doc_example_bat_out ... ok -test workers::report_worker::tests::test_parse_channel_hex_doc_example_full ... ok -test workers::report_worker::tests::test_parse_channel_hex_doc_example_off ... ok -test workers::report_worker::tests::test_parse_channel_hex_empty_channel_keeps_power_status ... ok -test workers::report_worker::tests::test_parse_channel_hex_handles_multibyte_without_panic ... ok -test workers::report_worker::tests::test_parse_channel_hex_rejects_non_hex ... ok -test workers::report_worker::tests::test_parse_channel_hex_rejects_wrong_length ... ok -test workers::report_worker::tests::test_parse_compartment_id_accepts_two_segments ... ok -test workers::report_worker::tests::test_parse_compartment_id_rejects_invalid_values ... ok -test workers::report_worker::tests::test_parse_compartment_id_rejects_other_segment_counts ... ok -test workers::report_worker::tests::test_parse_compartment_id_rejects_out_of_range ... ok -test workers::report_worker::tests::test_pending_key_format_matches_device_server ... ok -test workers::report_worker::tests::test_power_fail_mask_semantics ... ok -test workers::report_worker::tests::test_sanitize_sample_value_accepts_in_range ... ok -test workers::report_worker::tests::test_sanitize_sample_value_rejects_out_of_range ... ok -test workers::report_worker::tests::test_should_finalize_bat_out_skips_late_report_of_previous_battery ... ok -test workers::report_worker::tests::test_should_finalize_bat_out_when_same_or_unknown_battery ... ok -test workers::report_worker::tests::test_compartment_lock_excludes_same_compartment ... ok -test middleware::auth::tests::test_verify_password_correct ... ok -test middleware::auth::tests::test_verify_password_wrong ... ok - -test result: ok. 144 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.98s - diff --git a/.tmp-schema-align/cargo-test2.txt b/.tmp-schema-align/cargo-test2.txt deleted file mode 100644 index aff0956..0000000 --- a/.tmp-schema-align/cargo-test2.txt +++ /dev/null @@ -1,152 +0,0 @@ - Finished `test` profile [unoptimized + debuginfo] target(s) in 0.50s - Running unittests src\main.rs (.cargo-check-target\debug\deps\pms_api-0e8b7d95b05a137e.exe) - -running 145 tests -test db::migrate::tests::test_alignment_targets_cover_known_broken_columns ... ok -test db::migrate::tests::test_schema_sql_energy_columns_comment_unit_is_wh ... ok -test commands::tests::test_is_valid_command_target_rejects_imei_and_malformed ... ok -test commands::tests::test_is_valid_command_target_accepts_board_and_compartment ... ok -test db::migrate::tests::test_every_alignment_target_appears_verbatim_in_schema_sql ... ok -test db::migrate::tests::test_schema_sql_table_order_has_forward_reference_returns_none ... ok -test middleware::auth::tests::test_builtin_role_name_covers_all_valid_levels ... ok -test db::migrate::tests::test_referenced_tables_single_foreign_key_returns_table_name ... ok -test middleware::auth::tests::test_builtin_role_name_invalid_level_returns_none ... ok -test db::migrate::tests::test_legacy_column_comments_marker_matches_ddl ... ok -test middleware::auth::tests::test_builtin_role_name_operator_maps_to_normal_user ... ok -test middleware::auth::tests::test_builtin_role_name_super_admin_maps_to_super_admin ... ok -test middleware::auth::tests::test_builtin_role_name_org_admin_maps_to_org_admin ... ok -test middleware::auth::tests::test_check_permission_operator_empty_permissions_forbidden ... ok -test middleware::auth::tests::test_append_condition_handles_empty_clause ... ok -test middleware::auth::tests::test_org_condition_normal_admin ... ok -test db::migrate::tests::test_schema_sql_contains_no_timestamp_column ... ok -test middleware::auth::tests::test_org_condition_super_admin ... ok -test middleware::auth::tests::test_super_admin_role_level_constant_matches_mapping ... ok -test commands::tests::test_take_command_target_extracts_and_removes_id ... ok -test commands::tests::test_take_command_target_absent_returns_none ... ok -test commands::tests::test_take_command_target_rejects_invalid_id ... ok -test middleware::auth::tests::test_user_scope_allows_project ... ok -test db::migrate::tests::test_column_spec_from_target_parses_all_dimensions ... ok -test middleware::auth::tests::test_user_scope_default_is_unrestricted ... ok -test middleware::auth::tests::test_verify_password_invalid_hash ... ok -test middleware::auth::tests::test_org_condition_no_org ... ok -test routes::cabinet_tree::tests::test_build_filter_where_super_admin_ignores_scope ... ok -test routes::cabinet_tree::tests::test_build_filter_where_without_scope_is_unchanged ... ok -test routes::cabinet_tree::tests::test_channel_flags_has_data_and_status_code_match_frontend_mapping ... ok -test routes::cabinet_tree::tests::test_derive_channel_counts_all_idle_channels_yield_zero_full_fault ... ok -test routes::cabinet_tree::tests::test_build_filter_where_scope_combines_with_bound_filter ... ok -test middleware::auth::tests::test_user_scope_project_condition_narrows_by_project ... ok -test routes::cabinet_tree::tests::test_build_filter_where_scope_applies_to_explicit_project_and_org_params ... ok -test routes::cabinet_tree::tests::test_build_filter_where_with_scope_appends_project_narrowing ... ok -test routes::cabinet_tree::tests::test_derive_channel_counts_charging_channels_counted_as_charging ... ok -test routes::cabinet_tree::tests::test_derive_channel_counts_fault_channels_counted_as_fault ... ok -test routes::cabinet_tree::tests::test_derive_channel_counts_fault_wins_over_full_on_conflict ... ok -test routes::cabinet_tree::tests::test_derive_channel_counts_full_channels_counted_as_full ... ok -test routes::cabinet_tree::tests::test_derive_channel_counts_offline_missing_data_yields_zero_full_fault ... ok -test routes::cabinet_tree::tests::test_derive_channel_view_charging_channel_sets_on_true ... ok -test routes::cabinet_tree::tests::test_derive_channel_view_batt_fault_bits_map_to_fault ... ok -test routes::cabinet_tree::tests::test_derive_channel_view_empty_channel_keeps_power_status ... ok -test routes::cabinet_tree::tests::test_derive_channel_view_full_battery_prefers_full_over_charging ... ok -test routes::cabinet_tree::tests::test_derive_channel_view_missing_fields_yields_idle_and_empty_hex ... ok -test routes::cabinet_tree::tests::test_derive_channel_view_normalizes_hex_case_and_padding ... ok -test routes::cabinet_tree::tests::test_derive_channel_view_pow_fault_maps_to_fault ... ok -test routes::cabinet_tree::tests::test_derive_channel_view_single_byte_yields_empty_hex ... ok -test routes::cabinet_tree::tests::test_device_access_sql_appends_scope_only_when_configured ... ok -test routes::cabinet_tree::tests::test_device_list_fields_in_sync ... ok -test routes::cabinet_tree::tests::test_parse_channel_key_accepts_real_channel_keys ... ok -test routes::cabinet_tree::tests::test_parse_channel_key_rejects_internal_keys ... ok -test routes::cabinet_tree::tests::test_parse_channel_key_rejects_out_of_range_and_malformed ... ok -test routes::cabinet_tree::tests::test_parse_hex_byte_rejects_malformed_values ... ok -test routes::cabinet_tree::tests::test_pick_non_empty_hex_keeps_00_but_drops_missing ... ok -test routes::cabinets::tests::test_apply_realtime_keeps_null_when_missing ... ok -test routes::cabinets::tests::test_cabinet_realtime_field_indices_are_in_sync ... ok -test routes::cabinets::tests::test_parse_cabinet_realtime_cabinet_status_bits_raw_hex ... ok -test routes::cabinets::tests::test_parse_cabinet_realtime_cabinet_status_missing_is_null_not_00 ... ok -test routes::cabinets::tests::test_parse_cabinet_realtime_missing_fields_stay_none ... ok -test routes::cabinets::tests::test_parse_cabinet_realtime_reads_voltage_current_and_temp ... ok -test routes::charge_orders::tests::test_order_scope_conditions_super_admin_exempt ... ok -test routes::charge_orders::tests::test_order_scope_conditions_org_isolation ... ok -test routes::charge_records::tests::test_build_export_where_scope_appends_to_existing_conditions ... ok -test routes::charge_orders::tests::test_order_scope_conditions_applies_scope ... ok -test routes::charge_records::tests::test_build_export_where_applies_scope_project_ids ... ok -test routes::energy_stats::tests::test_build_export_clause_empty_when_no_filters ... ok -test routes::energy_stats::tests::test_build_export_clause_targets_charge_records ... ok -test routes::energy_stats::tests::test_build_export_where_keeps_org_isolation ... ok -test routes::energy_stats::tests::test_clamp_range_bounds_illegal_values ... ok -test routes::energy_stats::tests::test_estimated_cost_unit_is_wh_times_80_cent_per_kwh ... ok -test routes::energy_stats::tests::test_time_window_uses_start_time_half_open_ranges ... ok -test routes::h5::tests::test_compartment_realtime_fields_order_matches_indexes ... ok -test routes::h5::tests::test_h5_list_pick_non_empty_hex_keeps_00_but_drops_missing ... ok -test routes::h5::tests::test_parse_compartment_realtime_garbage_is_none ... ok -test routes::h5::tests::test_parse_compartment_realtime_no_data_returns_none_not_zero ... ok -test routes::h5::tests::test_parse_compartment_realtime_reads_all_fields ... ok -test routes::h5::tests::test_parse_compartment_realtime_real_zero_is_kept ... ok -test routes::h5::tests::test_visible_org_scope_operator_sees_own_org_only ... ok -test routes::h5::tests::test_visible_org_scope_org_admin_sees_own_org_only ... ok -test routes::h5::tests::test_visible_org_scope_org_user_without_org_is_empty ... ok -test routes::h5::tests::test_visible_org_scope_super_admin_sees_all ... ok -test routes::h5::tests::test_visible_orgs_own_sql_filters_by_id ... ok -test routes::scope_guard::tests::test_access_sql_with_scope_narrows_by_project ... ok -test routes::scope_guard::tests::test_access_sql_without_scope_has_no_narrowing ... ok -test routes::scope_guard::tests::test_verify_org_ownership_rules ... ok -test routes::tests::test_is_allowed_origin_allows_whitelist ... ok -test routes::tests::test_is_allowed_origin_rejects_others ... ok -test routes::upload_guard::tests::test_sniff_image_ext_recognizes_bitmaps ... ok -test routes::upload_guard::tests::test_validate_image_upload_accepts_real_images ... ok -test routes::upload_guard::tests::test_validate_image_upload_rejects_scriptable_exts ... ok -test routes::upload_guard::tests::test_validate_image_upload_rejects_svg_content_with_image_ext ... ok -test routes::upload_guard::tests::test_validate_image_upload_rejects_unknown_ext ... ok -test workers::energy::tests::test_fixed_interval_is_conservative ... ok -test workers::energy::tests::test_full_hour_charge_magnitude ... ok -test workers::energy::tests::test_mwh_delta_real_world_case ... ok -test workers::energy::tests::test_mwh_delta_rejects_non_positive_inputs ... ok -test workers::energy::tests::test_mwh_delta_small_current_not_lost_by_rounding ... ok -test workers::energy::tests::test_mwh_delta_zero_current_no_accumulation ... ok -test workers::energy::tests::test_mwh_to_wh_rounding ... ok -test workers::energy::tests::test_order_energy_key_format ... ok -test workers::log_retention::tests::test_retention_constants_are_sane ... ok -test workers::report_worker::tests::test_board_channel_id_range_guards ... ok -test workers::report_worker::tests::test_build_spec_complete_topology_prefers_real_topology ... ok -test workers::report_worker::tests::test_build_spec_incomplete_topology_falls_back_to_legacy_cell ... ok -test workers::report_worker::tests::test_build_spec_legacy_cell_clamped_into_protocol_range ... ok -test workers::report_worker::tests::test_build_spec_never_produces_all_zero_spec ... ok -test workers::report_worker::tests::test_build_spec_no_cell_uses_constant_default ... ok -test workers::report_worker::tests::test_channel_battery_key_format_contract ... ok -test workers::report_worker::tests::test_channel_hex_len_constant ... ok -test workers::report_worker::tests::test_channel_status_key_format_contract ... ok -test workers::report_worker::tests::test_compartment_lock_key_is_identity ... ok -test workers::report_worker::tests::test_decide_order_action_boundary_is_strict ... ok -test workers::report_worker::tests::test_decide_order_action_create_when_no_active_order ... ok -test workers::report_worker::tests::test_decide_order_action_replace_on_different_battery ... ok -test workers::report_worker::tests::test_decide_order_action_skip_same_battery ... ok -test workers::report_worker::tests::test_decide_order_action_skip_when_either_battery_id_empty ... ok -test workers::report_worker::tests::test_is_unique_violation_ignores_non_database_errors ... ok -test workers::report_worker::tests::test_normalize_hex_byte_rejects_invalid ... ok -test workers::report_worker::tests::test_normalize_hex_byte_valid_and_lowercased ... ok -test workers::report_worker::tests::test_order_timeout_bound_goes_backwards ... ok -test workers::report_worker::tests::test_out_of_range_sample_contributes_zero_energy ... ok -test workers::report_worker::tests::test_parse_channel_hex_case_insensitive_and_lowercased ... ok -test workers::report_worker::tests::test_parse_channel_hex_doc_example_bat_in ... ok -test workers::report_worker::tests::test_parse_channel_hex_doc_example_bat_out ... ok -test workers::report_worker::tests::test_parse_channel_hex_doc_example_full ... ok -test workers::report_worker::tests::test_parse_channel_hex_doc_example_off ... ok -test workers::report_worker::tests::test_parse_channel_hex_empty_channel_keeps_power_status ... ok -test workers::report_worker::tests::test_parse_channel_hex_handles_multibyte_without_panic ... ok -test workers::report_worker::tests::test_parse_channel_hex_rejects_non_hex ... ok -test workers::report_worker::tests::test_parse_channel_hex_rejects_wrong_length ... ok -test workers::report_worker::tests::test_parse_compartment_id_accepts_two_segments ... ok -test workers::report_worker::tests::test_parse_compartment_id_rejects_invalid_values ... ok -test workers::report_worker::tests::test_parse_compartment_id_rejects_other_segment_counts ... ok -test workers::report_worker::tests::test_parse_compartment_id_rejects_out_of_range ... ok -test workers::report_worker::tests::test_pending_key_format_matches_device_server ... ok -test workers::report_worker::tests::test_power_fail_mask_semantics ... ok -test workers::report_worker::tests::test_sanitize_sample_value_accepts_in_range ... ok -test workers::report_worker::tests::test_sanitize_sample_value_rejects_out_of_range ... ok -test workers::report_worker::tests::test_should_finalize_bat_out_skips_late_report_of_previous_battery ... ok -test workers::report_worker::tests::test_should_finalize_bat_out_when_same_or_unknown_battery ... ok -test workers::report_worker::tests::test_compartment_lock_allows_different_compartments ... ok -test workers::report_worker::tests::test_compartment_lock_excludes_same_compartment ... ok -test middleware::auth::tests::test_verify_password_correct ... ok -test middleware::auth::tests::test_verify_password_wrong ... ok - -test result: ok. 145 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.05s - diff --git a/.tmp-schema-align/compare.py b/.tmp-schema-align/compare.py deleted file mode 100644 index 0bb68f4..0000000 --- a/.tmp-schema-align/compare.py +++ /dev/null @@ -1,76 +0,0 @@ -"""只读比对:pms_dev 与 pms 的目标列定义 + organizations.parent_id 索引/外键 - -用法: python .tmp-schema-align/compare.py -只发 SELECT,不做任何写入。 -""" -import pymysql - -COLS = [ - ("charge_records", "energy"), - ("charge_records", "start_time"), - ("charge_records", "end_time"), - ("energy_stats", "total_energy"), - ("download_tasks", "completed_at"), - ("organizations", "parent_id"), -] - -COL_SQL = """ -SELECT COLUMN_TYPE, IS_NULLABLE, IFNULL(CAST(COLUMN_DEFAULT AS CHAR),''), - IFNULL(CAST(COLUMN_COMMENT AS CHAR),'') -FROM information_schema.COLUMNS -WHERE TABLE_SCHEMA=%s AND TABLE_NAME=%s AND COLUMN_NAME=%s -""" - -IDX_SQL = """ -SELECT INDEX_NAME, SEQ_IN_INDEX, COLUMN_NAME, NON_UNIQUE -FROM information_schema.STATISTICS -WHERE TABLE_SCHEMA=%s AND TABLE_NAME='organizations' -ORDER BY INDEX_NAME, SEQ_IN_INDEX -""" - -FK_SQL = """ -SELECT CONSTRAINT_NAME, COLUMN_NAME, REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME -FROM information_schema.KEY_COLUMN_USAGE -WHERE TABLE_SCHEMA=%s AND TABLE_NAME='organizations' AND REFERENCED_TABLE_NAME IS NOT NULL -""" - - -def snapshot(db): - c = pymysql.connect(host="10.8.0.252", user="root", password="Hbhyg731024@", database=db) - cur = c.cursor() - cols = {} - for t, col in COLS: - cur.execute(COL_SQL, (db, t, col)) - cols[f"{t}.{col}"] = cur.fetchone() or ("",) * 4 - cur.execute(IDX_SQL, (db,)) - idx = cur.fetchall() - cur.execute(FK_SQL, (db,)) - fk = cur.fetchall() - c.close() - return cols, idx, fk - - -dev_cols, dev_idx, dev_fk = snapshot("pms_dev") -prd_cols, prd_idx, prd_fk = snapshot("pms") - -print("=" * 100) -print("%-32s | %-44s | %-44s | %s" % ("列", "pms_dev(测试)", "pms(生产)", "一致")) -print("=" * 100) -all_same = True -for key in [f"{t}.{c}" for t, c in COLS]: - d, p = dev_cols[key], prd_cols[key] - same = d == p - all_same &= same - fmt = lambda r: "%s null=%s def=%s cmt=%s" % r - print("%-32s | %-44s | %-44s | %s" % (key, fmt(d), fmt(p), "OK" if same else "DIFF")) -print("-" * 100) -print("列定义完全一致:", all_same) -print() -print("organizations 索引 pms_dev :", dev_idx) -print("organizations 索引 pms :", prd_idx) -print("parent_id 处于索引最左列 :", any(r[1] == 1 and r[2] == "parent_id" for r in dev_idx), - "/", any(r[1] == 1 and r[2] == "parent_id" for r in prd_idx)) -print() -print("organizations 自引用外键 pms_dev :", dev_fk) -print("organizations 自引用外键 pms :", prd_fk) -print("外键均存在:", bool(dev_fk) and bool(prd_fk)) diff --git a/.tmp-schema-align/compare_full.py b/.tmp-schema-align/compare_full.py deleted file mode 100644 index c0fe96d..0000000 --- a/.tmp-schema-align/compare_full.py +++ /dev/null @@ -1,58 +0,0 @@ -"""只读比对脚本:逐列比对 pms_dev(测试) 与 pms(生产) 的**全部**列定义 - -比对维度:COLUMN_TYPE / IS_NULLABLE / COLUMN_DEFAULT / EXTRA / COLUMN_COMMENT -另外单独统计两库是否还残留 `TIMESTAMP` 列(本项目禁止:sqlx 只能把它解码成 DateTime, -而 Rust 侧时间列全是 NaiveDateTime/Option ⇒ 接口 500)。 - -用法: python .tmp-schema-align/compare_full.py -只发 SELECT,不做任何写入。 -""" -import pymysql - -COLSQL = """ -SELECT TABLE_NAME, COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE, - IFNULL(CAST(COLUMN_DEFAULT AS CHAR),''), - IFNULL(CAST(EXTRA AS CHAR),''), - IFNULL(CAST(COLUMN_COMMENT AS CHAR),'') -FROM information_schema.COLUMNS -WHERE TABLE_SCHEMA=%s AND TABLE_NAME IN ( - SELECT TABLE_NAME FROM information_schema.TABLES - WHERE TABLE_SCHEMA=%s AND TABLE_TYPE='BASE TABLE') -ORDER BY TABLE_NAME, ORDINAL_POSITION -""" - - -def snap(db): - c = pymysql.connect(host="10.8.0.252", user="root", password="Hbhyg731024@", database=db) - cur = c.cursor() - cur.execute(COLSQL, (db, db)) - rows = {(r[0], r[1]): r[2:] for r in cur.fetchall()} - c.close() - return rows - - -dev, prd = snap("pms_dev"), snap("pms") -keys = sorted(set(dev) | set(prd)) - -print("=" * 118) -print("两库列定义比对:pms_dev(测试) vs pms(生产) 共 %d 列" % len(keys)) -print("=" * 118) -diffs = [] -for k in keys: - d, p = dev.get(k), prd.get(k) - if d != p: - diffs.append((k, d, p)) - -if not diffs: - print("✅ 全部 %d 列的 类型/可空性/默认值/EXTRA/注释 完全一致" % len(keys)) -else: - print("❌ 仍有 %d 列不一致:" % len(diffs)) - for k, d, p in diffs: - print(" %-32s dev=%s" % (f"{k[0]}.{k[1]}", d)) - print(" %-32s prd=%s" % ("", p)) - -print() -ts_dev = sorted(k for k, v in dev.items() if v[0].startswith("timestamp")) -ts_prd = sorted(k for k, v in prd.items() if v[0].startswith("timestamp")) -print("pms_dev 残留 TIMESTAMP 列: %d %s" % (len(ts_dev), [f"{t}.{c}" for t, c in ts_dev])) -print("pms 残留 TIMESTAMP 列: %d %s" % (len(ts_prd), [f"{t}.{c}" for t, c in ts_prd])) diff --git a/.tmp-schema-align/compare_time.py b/.tmp-schema-align/compare_time.py deleted file mode 100644 index 3572e2e..0000000 --- a/.tmp-schema-align/compare_time.py +++ /dev/null @@ -1,42 +0,0 @@ -"""列出两库全部时间列的类型,并做差异比对(只读)""" -import pymysql - -SQL = """ -SELECT TABLE_NAME, COLUMN_NAME, DATA_TYPE, IS_NULLABLE, - IFNULL(CAST(COLUMN_DEFAULT AS CHAR),'') -FROM information_schema.COLUMNS -WHERE TABLE_SCHEMA=%s AND DATA_TYPE IN ('timestamp','datetime') -ORDER BY TABLE_NAME, ORDINAL_POSITION -""" - - -def snap(db): - c = pymysql.connect(host="10.8.0.252", user="root", password="Hbhyg731024@", database=db) - cur = c.cursor() - cur.execute(SQL, (db,)) - rows = {(r[0], r[1]): (r[2], r[3], r[4]) for r in cur.fetchall()} - c.close() - return rows - - -dev, prd = snap("pms_dev"), snap("pms") -keys = sorted(set(dev) | set(prd)) -print("%-30s %-20s %-34s %-34s %s" % ("table.column", "", "pms_dev", "pms", "一致")) -diff = 0 -for k in keys: - d, p = dev.get(k, ("",) * 3), prd.get(k, ("",) * 3) - same = d == p - if not same: - diff += 1 - print("%-30s %-20s %-34s %-34s %s" % (f"{k[0]}.{k[1]}", "", str(d), str(p), "OK" if same else "*** DIFF ***")) -print() -print("时间列总数:", len(keys), " 不一致:", diff) -print() -print("pms_dev 中仍是 TIMESTAMP 的列:") -for k in keys: - if dev.get(k, ("",))[0] == "timestamp": - print(" ", f"{k[0]}.{k[1]}", dev[k]) -print("pms(生产) 中仍是 TIMESTAMP 的列:") -for k in keys: - if prd.get(k, ("",))[0] == "timestamp": - print(" ", f"{k[0]}.{k[1]}", prd[k]) diff --git a/.tmp-schema-align/dev_sql.py b/.tmp-schema-align/dev_sql.py deleted file mode 100644 index fc6ab3d..0000000 --- a/.tmp-schema-align/dev_sql.py +++ /dev/null @@ -1,12 +0,0 @@ -import pymysql, sys - -SQL = sys.argv[1] -c = pymysql.connect(host='10.8.0.252', user='root', password='Hbhyg731024@', database='pms_dev', autocommit=True) -cur = c.cursor() -cur.execute(SQL) -try: - for r in cur.fetchall(): - print(r) -except Exception: - pass -print("lastrowid", cur.lastrowid, "rowcount", cur.rowcount) diff --git a/.tmp-schema-align/diff_detail.py b/.tmp-schema-align/diff_detail.py deleted file mode 100644 index 0e72eb3..0000000 --- a/.tmp-schema-align/diff_detail.py +++ /dev/null @@ -1,41 +0,0 @@ -"""列出两库时间列的完整定义(含 EXTRA),用于生成对齐清单(只读)""" -import pymysql - -SQL = """ -SELECT TABLE_NAME, COLUMN_NAME, DATA_TYPE, IS_NULLABLE, - IFNULL(CAST(COLUMN_DEFAULT AS CHAR),''), - EXTRA, IFNULL(CAST(COLUMN_COMMENT AS CHAR),'') -FROM information_schema.COLUMNS -WHERE TABLE_SCHEMA=%s AND DATA_TYPE IN ('timestamp','datetime') -ORDER BY TABLE_NAME, ORDINAL_POSITION -""" - - -def snap(db): - c = pymysql.connect(host="10.8.0.252", user="root", password="Hbhyg731024@", database=db) - cur = c.cursor() - cur.execute(SQL, (db,)) - rows = {(r[0], r[1]): r[2:] for r in cur.fetchall()} - c.close() - return rows - - -dev, prd = snap("pms_dev"), snap("pms") -print("== 生产(pms)时间列定义(作为目标)==") -for k in sorted(prd): - print(" %-28s %-34s %s" % (f"{k[0]}.{k[1]}", str(prd[k][:3]), prd[k][3])) -print() -print("== 仅 dev != prod 的列(扩展对齐候选)==") -n = 0 -for k in sorted(set(dev) | set(prd)): - d, p = dev.get(k), prd.get(k) - if d != p: - n += 1 - print(" %-28s dev=%s" % (f"{k[0]}.{k[1]}", d)) - print(" %-28s prd=%s" % ("", p)) -print("共", n, "列") -print() -print("== 两库相同的 TIMESTAMP 列(不在漂移范围,需单独决策)==") -for k in sorted(set(dev) & set(prd)): - if dev[k][0] == "timestamp": - print(" %-28s %s" % (f"{k[0]}.{k[1]}", dev[k])) diff --git a/.tmp-schema-align/dryrun.out.txt b/.tmp-schema-align/dryrun.out.txt deleted file mode 100644 index 446af21..0000000 --- a/.tmp-schema-align/dryrun.out.txt +++ /dev/null @@ -1,106 +0,0 @@ - -=== 2/8 校验产物与静态检查 === - ✅ pms-api (12.1 MB) - ✅ pms-device-server (2.8 MB) - ✅ web/dist (6 个文件) - -=== 3/8 服务器备份(保留旧二进制/.env/nginx/单元/前端) === - [DryRun] ssh: set -e -TS=$(date +%Y%m%d-%H%M%S) -mkdir -p /data/pms/backup-$TS -cp -a /data/pms/api-server/pms-api /data/pms/backup-$TS/ 2>/dev/null || true -cp -a /data/pms/device-server/pms-device-server /data/pms/backup-$TS/ 2>/dev/null || true -cp -a /data/pms/api-server/.env /data/pms/device-server/.env /data/pms/backup-$TS/ 2>/dev/null || true -mkdir -p /data/pms/backup-$TS/web && cp -a /data/pms/web/. /data/pms/backup-$TS/web/ 2>/dev/null || true -echo $TS > /tmp/pms_last_backup -echo " 备份目录: /data/pms/backup-$TS" - -=== 4/8 上传(二进制传 .new,运行中的二进制不可直接覆盖) === - [DryRun] scp: D:\charging-cabinet\software\api-server\target\x86_64-unknown-linux-gnu\release\pms-api -> /data/pms/api-server/pms-api.new - [DryRun] scp: D:\charging-cabinet\software\device-server\target\x86_64-unknown-linux-gnu\release\pms-device-server -> /data/pms/device-server/pms-device-server.new - [DryRun] scp: C:\Users\12451\AppData\Local\Temp\pms-dist.tgz -> /tmp/pms-dist.tgz - -=== 5/8 原子替换并重启服务 === - [DryRun] ssh: set -e -chmod +x /data/pms/api-server/pms-api.new /data/pms/device-server/pms-device-server.new -systemctl stop pms-api pms-device -mv -f /data/pms/api-server/pms-api.new /data/pms/api-server/pms-api -mv -f /data/pms/device-server/pms-device-server.new /data/pms/device-server/pms-device-server -rm -rf /data/pms/web/* && tar -xzf /tmp/pms-dist.tgz -C /data/pms/web/ && rm -f /tmp/pms-dist.tgz -systemctl daemon-reload -systemctl start pms-device pms-api -sleep 8 -nginx -t && systemctl reload nginx - -=== 6/8 验收(服务/端口/域名/日志) === - [DryRun] ssh: echo '--- 服务 ---'; systemctl is-active pms-api pms-device nginx | paste -sd' ' -echo '--- 端口(应为 9000 / 9001)---'; ss -lntp | grep -E ':9000|:9001' | awk '{print " " $4}' -echo '--- 域名 ---' -curl -sk -m 8 -o /dev/null -w ' /api/pms/cabinets -> %{http_code}\n' https://app.anzhizhichong.com/api/pms/cabinets -curl -sk -m 8 -o /dev/null -w ' /pms/ -> %{http_code}\n' https://app.anzhizhichong.com/pms/ -echo '--- 启动日志尾部 ---'; journalctl -u pms-api --no-pager -n 4 | tail -4 -echo '--- 新进程 ERROR 数(应为 0)---' -MAIN=$(systemctl show -p MainPID --value pms-api) -journalctl -u pms-api --since '-3 min' --no-pager | grep -c " ERROR " | sed 's/^/ ERROR 行数: /' - -=== 7/8 上报消费链路探测(用不存在的 IMEI,不碰真实设备数据) === - -=== 8/8 回滚点保留策略(只保留最近 5 次 backup-YYYYMMDD-HHMMSS) === - [DryRun] ssh: set -e -shopt -s nullglob -KEEP=5 -# glob 字符类只匹配严格的 backup-YYYYMMDD-HHMMSS;nullglob 让无匹配时展开为空数组。 -# glob 结果按字典序排列,对该命名格式而言等价于时间戳升序(最早的在前)。 -rollbacks=() -for p in /data/pms/backup-[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-[0-9][0-9][0-9][0-9][0-9][0-9]; do - if [ -d "$p" ]; then - rollbacks+=( "$p" ) - fi -done -total=${#rollbacks[@]} -echo " 当前回滚点 $total 个(deploy 会为每次部署新建一份):" -for p in "${rollbacks[@]}"; do - echo " $p" -done -if [ "$total" -gt "$KEEP" ]; then - drop=$((total - KEEP)) - echo " 将删除最早的 $drop 个(保留最近 $KEEP 个):" - i=0 - for p in "${rollbacks[@]}"; do - if [ "$i" -ge "$drop" ]; then - break - fi - i=$((i + 1)) - # 复核:名字必须严格符合 backup-YYYYMMDD-HHMMSS,否则拒绝删除(绝不碰其它目录) - case "$(basename "$p")" in - backup-[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-[0-9][0-9][0-9][0-9][0-9][0-9]) - echo " 删除 $p" - rm -rf -- "$p" - ;; - *) - echo " [skip] 名字不符合回滚点模式,拒绝删除: $p" - ;; - esac - done -else - echo " 回滚点数量未超过 $KEEP,无需清理" -fi -kept=() -for p in /data/pms/backup-[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-[0-9][0-9][0-9][0-9][0-9][0-9]; do - if [ -d "$p" ]; then - kept+=( "$p" ) - fi -done -echo " 清理后保留的回滚点 ${#kept[@]} 个:" -for p in "${kept[@]}"; do - echo " $p" -done -echo " /data/pms 占用: $(du -sh /data/pms | cut -f1)" - -=== 部署完成 === - 正式环境:https://app.anzhizhichong.com/pms/ - API :9000 TCP :9001 - - 回滚方法(如需): - plink -batch -pw root@118.31.119.206 "systemctl stop pms-api pms-device && cp /data/pms/backup-/* /data/pms/... && systemctl start pms-api pms-device" - 备份目录见 /data/pms/backup-<时间戳>/(脚本已把 .env / 前端 / 单元一并备份;只保留最近 5 次) diff --git a/.tmp-schema-align/endpoint_verify.py b/.tmp-schema-align/endpoint_verify.py deleted file mode 100644 index 3179f41..0000000 --- a/.tmp-schema-align/endpoint_verify.py +++ /dev/null @@ -1,152 +0,0 @@ -"""端点层验收:造临时数据 → 调接口 → 断言 200 且能返回行 → 清理临时数据 - -用法: - python .tmp-schema-align/endpoint_verify.py dev # 测试环境(pms_dev + test.anzhizhichong.com) - python .tmp-schema-align/endpoint_verify.py prod # 生产环境(pms + app.anzhizhichong.com) - python .tmp-schema-align/endpoint_verify.py dev clean # 只清理 -""" -import base64, hashlib, hmac, json, ssl, sys, time, urllib.request, urllib.error, pymysql - -ENVS = { - "dev": dict( - db="pms_dev", - base="https://test.anzhizhichong.com/api/pms", - secret="32e8708743c42fe764aff1d40737a63803a46815a9501f4bcb50c41fe3d27108", - admin_phone="19900000001", - admin_role=2, - ), - "prod": dict( - db="pms", - base="https://app.anzhizhichong.com/api/pms", - secret="787789e8eb1f3a10e81596f8219a20aa3c34102acacba2a5ed14bbb1d16ec833", - admin_phone="13912391234", - admin_role=2, - # 生产库**不新建账号**:直接复用既有的总管理员账号(id=2)签 token, - # 只造 charge_orders / device_logs 两条临时业务行,验完立刻删除。 - existing_admin_id=2, - ), -} - -TAG = "VERIFY-SCHEMA-TMP" - - -def b64(b): - return base64.urlsafe_b64encode(b).rstrip(b"=").decode() - - -def sign(secret, sub, phone): - now = int(time.time()) - seg = b64(json.dumps({"alg": "HS256", "typ": "JWT"}, separators=(",", ":")).encode()) + "." + \ - b64(json.dumps({"sub": sub, "phone": phone, "exp": now + 1800, "iat": now}, - separators=(",", ":")).encode()) - return seg + "." + b64(hmac.new(secret.encode(), seg.encode(), hashlib.sha256).digest()) - - -def call(base, path, token): - req = urllib.request.Request(base + path, headers={"Authorization": "Bearer " + token}) - ctx = ssl.create_default_context() - ctx.check_hostname = False - ctx.verify_mode = ssl.CERT_NONE - try: - with urllib.request.urlopen(req, timeout=25, context=ctx) as r: - return r.status, r.read().decode("utf-8", "replace") - except urllib.error.HTTPError as e: - return e.code, e.read().decode("utf-8", "replace") - - -def main(): - env = ENVS[sys.argv[1]] - only_clean = len(sys.argv) > 2 and sys.argv[2] == "clean" - c = pymysql.connect(host="10.8.0.252", user="root", password="Hbhyg731024@", - database=env["db"], autocommit=True) - cur = c.cursor() - - # ---- 清理上一轮遗留 ---- - creates_admin = "existing_admin_id" not in env - cur.execute("DELETE FROM device_logs WHERE content LIKE %s", (f"%{TAG}%",)) - cur.execute("DELETE FROM charge_orders WHERE battery_id = %s", (TAG,)) - if creates_admin: - # 只删自己造的临时账号;复用既有账号时**绝不能**碰 users 表 - cur.execute("DELETE FROM users WHERE phone = %s", (env["admin_phone"],)) - if only_clean: - print("已清理临时数据(device_logs/charge_orders/users)") - return - - # ---- 取一个真实拓扑:柜子 + 仓体(满足外键)---- - cur.execute( - "SELECT c.id, cb.cabinet_id FROM compartments c " - "JOIN cabin_boards cb ON c.cabin_board_id = cb.id ORDER BY c.id LIMIT 1" - ) - row = cur.fetchone() - if not row: - sys.exit("库里没有 compartments,无法造订单(外键需要)") - compartment_id, cabinet_id = row - - # ---- 造临时超管(JWT 只带 sub,角色与权限从库里读)---- - if creates_admin: - cur.execute( - "INSERT INTO users (phone, name, password, role, organization_id, status) VALUES (%s, %s, 'x', %s, NULL, 1)", - (env["admin_phone"], TAG, env["admin_role"]), - ) - admin_id = cur.lastrowid - else: - admin_id = env["existing_admin_id"] - - # ---- 造 2 条 charge_orders(status<>0 ⇒ active_flag=NULL,不撞唯一约束)---- - cur.execute( - "INSERT INTO charge_orders (compartment_id, cabinet_id, battery_id, start_time, end_time, " - "start_soc, end_soc, energy, reason, status) " - "VALUES (%s, %s, %s, NOW(), NOW(), 850, 1000, 22, 'normal', 1)", - (compartment_id, cabinet_id, TAG), - ) - order_id = cur.lastrowid - - # ---- 造 2 条 device_logs ---- - cur.execute( - "INSERT INTO device_logs (cabinet_id, log_type, content) VALUES (%s, 1, %s)", - (cabinet_id, json.dumps({"tag": TAG, "n": 1})), - ) - log_id = cur.lastrowid - - print(f"环境={sys.argv[1]} 库={env['db']} 临时数据: users.id={admin_id} " - f"charge_orders.id={order_id} device_logs.id={log_id} (cabinet_id={cabinet_id})") - - # ---- 自签 token 调接口 ---- - token = sign(env["secret"], admin_id, env["admin_phone"]) - results = {} - for path in ("/charge-orders?page=1&page_size=20", "/device-logs?page=1&page_size=20", - "/charge-records?page=1&page_size=5"): - status, body = call(env["base"], path, token) - results[path] = (status, body) - has_tmp = TAG in body - print(f"\n--- GET {path} -> HTTP {status} (临时行出现={has_tmp}) ---") - print(body[:600]) - - # ---- 断言 ---- - ok_orders = results["/charge-orders?page=1&page_size=20"][0] == 200 and TAG in results["/charge-orders?page=1&page_size=20"][1] - ok_logs = results["/device-logs?page=1&page_size=20"][0] == 200 and TAG in results["/device-logs?page=1&page_size=20"][1] - ok_records = results["/charge-records?page=1&page_size=5"][0] == 200 - print("\n==== 断言 ====") - print(" /api/charge-orders 200 且返回临时行:", "PASS" if ok_orders else "FAIL") - print(" /api/device-logs 200 且返回临时行:", "PASS" if ok_logs else "FAIL") - print(" /api/charge-records 200 :", "PASS" if ok_records else "FAIL") - - print("\n==== 清理临时数据 ====") - cur.execute("DELETE FROM device_logs WHERE content LIKE %s", (f"%{TAG}%",)) - print(" device_logs 删除行数:", cur.rowcount) - cur.execute("DELETE FROM charge_orders WHERE battery_id = %s", (TAG,)) - print(" charge_orders 删除行数:", cur.rowcount) - if creates_admin: - cur.execute("DELETE FROM users WHERE phone = %s", (env["admin_phone"],)) - print(" users 删除行数:", cur.rowcount) - else: - print(" users: 未新建账号(复用既有超管 id=%s),无需删除" % admin_id) - cur.execute("SELECT COUNT(*) FROM charge_orders WHERE battery_id=%s", (TAG,)) - print(" 残留 charge_orders:", cur.fetchone()[0]) - cur.execute("SELECT COUNT(*) FROM device_logs WHERE content LIKE %s", (f"%{TAG}%",)) - print(" 残留 device_logs:", cur.fetchone()[0]) - c.close() - sys.exit(0 if (ok_orders and ok_logs and ok_records) else 1) - - -main() diff --git a/.tmp-schema-align/export_probe.py b/.tmp-schema-align/export_probe.py deleted file mode 100644 index cac08c4..0000000 --- a/.tmp-schema-align/export_probe.py +++ /dev/null @@ -1,51 +0,0 @@ -"""自签 HS256 JWT 并调用测试环境导出接口(只读探测,不写业务数据)""" -import base64, hashlib, hmac, json, ssl, sys, time, urllib.request, pymysql - -JWT_SECRET = "32e8708743c42fe764aff1d40737a63803a46815a9501f4bcb50c41fe3d27108" -BASE = "https://test.anzhizhichong.com/api/pms" - - -def b64(b: bytes) -> str: - return base64.urlsafe_b64encode(b).rstrip(b"=").decode() - - -def sign(sub: int, phone: str) -> str: - now = int(time.time()) - header = {"alg": "HS256", "typ": "JWT"} - payload = {"sub": sub, "phone": phone, "exp": now + 3600, "iat": now} - seg = b64(json.dumps(header, separators=(",", ":")).encode()) + "." + \ - b64(json.dumps(payload, separators=(",", ":")).encode()) - sig = hmac.new(JWT_SECRET.encode(), seg.encode(), hashlib.sha256).digest() - return seg + "." + b64(sig) - - -def call(path: str, token: str): - req = urllib.request.Request(BASE + path, headers={"Authorization": "Bearer " + token}) - ctx = ssl.create_default_context() - ctx.check_hostname = False - ctx.verify_mode = ssl.CERT_NONE - try: - with urllib.request.urlopen(req, timeout=20, context=ctx) as r: - return r.status, r.read().decode("utf-8", "replace") - except urllib.error.HTTPError as e: - return e.code, e.read().decode("utf-8", "replace") - - -conn = pymysql.connect(host="10.8.0.252", user="root", password="Hbhyg731024@", database="pms_dev") -cur = conn.cursor() -cur.execute("SELECT id, phone, role FROM users WHERE role >= 2 ORDER BY id LIMIT 5") -admins = cur.fetchall() -print("super admins:", admins) -conn.close() -if not admins: - sys.exit("no super admin") - -uid, phone, _ = admins[0] -tok = sign(uid, phone or "13800000000") -print("token(sub=%s) len=%d" % (uid, len(tok))) - -if len(sys.argv) > 1 and sys.argv[1] == "orders": - print("charge-orders:", call("/charge-orders?page=1&page_size=5", tok)) -else: - print("charge-records list:", call("/charge-records?page=1&page_size=5", tok)) - print("charge-records export:", call("/charge-records/export", tok)) diff --git a/.tmp-schema-align/export_verify.py b/.tmp-schema-align/export_verify.py deleted file mode 100644 index dfa8f63..0000000 --- a/.tmp-schema-align/export_verify.py +++ /dev/null @@ -1,67 +0,0 @@ -"""最终验收:测试环境导出功能(GET /api/charge-records/export)→ 检查 xlsx → 清理 - -用法: python .tmp-schema-align\export_verify.py -""" -import base64, hashlib, hmac, json, ssl, subprocess, sys, time, urllib.request, urllib.error, pymysql - -SECRET = "32e8708743c42fe764aff1d40737a63803a46815a9501f4bcb50c41fe3d27108" -BASE = "https://test.anzhizhichong.com/api/pms" -PHONE = "19900000003" - - -def b64(b): - return base64.urlsafe_b64encode(b).rstrip(b"=").decode() - - -def sign(sub): - now = int(time.time()) - seg = b64(json.dumps({"alg": "HS256", "typ": "JWT"}, separators=(",", ":")).encode()) + "." + \ - b64(json.dumps({"sub": sub, "phone": PHONE, "exp": now + 1800, "iat": now}, - separators=(",", ":")).encode()) - return seg + "." + b64(hmac.new(SECRET.encode(), seg.encode(), hashlib.sha256).digest()) - - -def call(path, token): - req = urllib.request.Request(BASE + path, headers={"Authorization": "Bearer " + token}) - ctx = ssl.create_default_context() - ctx.check_hostname = False - ctx.verify_mode = ssl.CERT_NONE - try: - with urllib.request.urlopen(req, timeout=30, context=ctx) as r: - return r.status, r.read().decode("utf-8", "replace") - except urllib.error.HTTPError as e: - return e.code, e.read().decode("utf-8", "replace") - - -c = pymysql.connect(host="10.8.0.252", user="root", password="Hbhyg731024@", - database="pms_dev", autocommit=True) -cur = c.cursor() -cur.execute("DELETE FROM download_tasks WHERE user_id IN (SELECT id FROM users WHERE phone=%s)", (PHONE,)) -cur.execute("DELETE FROM users WHERE phone=%s", (PHONE,)) -cur.execute("INSERT INTO users (phone,name,password,role,organization_id,status) VALUES (%s,'VERIFY-EXPORT-TMP','x',2,NULL,1)", (PHONE,)) -admin_id = cur.lastrowid -tok = sign(admin_id) -print("临时超管 id=", admin_id) - -status, body = call("/charge-records/export", tok) -print("GET /api/charge-records/export ->", status, body) -task_id = json.loads(body)["data"]["task_id"] - -time.sleep(4) -cur.execute("SELECT id,status,progress,file_path,completed_at,IFNULL(error_message,'') FROM download_tasks WHERE id=%s", (task_id,)) -row = cur.fetchone() -print("download_tasks 行:", row) - -ok = row and row[1] == 2 and row[2] == 100 and row[3] -print("断言:任务 status=2(完成) 且 progress=100 且 file_path 非空 ->", "PASS" if ok else "FAIL") - -print("\n--- 清理 ---") -cur.execute("DELETE FROM download_tasks WHERE user_id=%s", (admin_id,)) -print(" download_tasks 删除:", cur.rowcount) -cur.execute("DELETE FROM users WHERE id=%s", (admin_id,)) -print(" users 删除:", cur.rowcount) -cur.execute("SELECT COUNT(*) FROM download_tasks") -print(" download_tasks 剩余行:", cur.fetchone()[0]) -c.close() -print(" 导出的 xlsx 由服务器侧脚本删除(见 remote 输出)") -sys.exit(0 if ok else 1) diff --git a/.tmp-schema-align/extract_step8.ps1 b/.tmp-schema-align/extract_step8.ps1 deleted file mode 100644 index 5c38edf..0000000 --- a/.tmp-schema-align/extract_step8.ps1 +++ /dev/null @@ -1,8 +0,0 @@ -# 从 -DryRun 输出中抽取第 8 步渲染出来的 bash 原文,落到本地 .sh(验证转义/语法/逻辑) -$text = Get-Content 'D:\charging-cabinet\.tmp-schema-align\dryrun.out.txt' -Raw -$m = [regex]::Match($text, '(?ms)^\s*\[DryRun\] ssh: (set -e\r?\nshopt -s nullglob\r?\n.*?)(?=\r?\n\r?\n)') -if (-not $m.Success) { throw '未找到第 8 步 DryRun 输出' } -$bash = $m.Groups[1].Value -replace "`r`n", "`n" -Set-Content -NoNewline -Encoding utf8 'D:\charging-cabinet\.tmp-schema-align\retention.sh' $bash -Write-Host "抽取字符数: $($bash.Length)" -Write-Host "含 /data/pms 次数: $(([regex]::Matches($bash,'/data/pms')).Count)" diff --git a/.tmp-schema-align/leftover_check.py b/.tmp-schema-align/leftover_check.py deleted file mode 100644 index 9ea2bfd..0000000 --- a/.tmp-schema-align/leftover_check.py +++ /dev/null @@ -1,17 +0,0 @@ -import pymysql - -for db in ("pms_dev", "pms"): - c = pymysql.connect(host="10.8.0.252", user="root", password="Hbhyg731024@", database=db) - cur = c.cursor() - out = [] - checks = [ - ("charge_orders WHERE battery_id LIKE 'VERIFY-%'", "charge_orders(tmp)"), - ("device_logs WHERE content LIKE '%VERIFY-%'", "device_logs(tmp)"), - ("download_tasks", "download_tasks(全部)"), - ("users WHERE name LIKE 'VERIFY-%' OR phone LIKE '1990000%'", "users(tmp)"), - ] - for where, label in checks: - cur.execute("SELECT COUNT(*) FROM %s" % where) - out.append("%s=%d" % (label, cur.fetchone()[0])) - print(" ", db, ", ".join(out)) - c.close() diff --git a/.tmp-schema-align/probe1.py b/.tmp-schema-align/probe1.py deleted file mode 100644 index fd22049..0000000 --- a/.tmp-schema-align/probe1.py +++ /dev/null @@ -1,61 +0,0 @@ -import pymysql, sys - -TARGETS = [ - ("charge_records", "energy"), - ("charge_records", "start_time"), - ("charge_records", "end_time"), - ("energy_stats", "total_energy"), - ("download_tasks", "completed_at"), - ("organizations", "parent_id"), -] - -SQL = """ -SELECT TABLE_NAME, COLUMN_NAME, COLUMN_TYPE, DATA_TYPE, IS_NULLABLE, - COLUMN_DEFAULT, EXTRA, COLUMN_COMMENT -FROM information_schema.COLUMNS -WHERE TABLE_SCHEMA = %s AND ((TABLE_NAME, COLUMN_NAME) IN (%s)) -ORDER BY TABLE_NAME, COLUMN_NAME -""" - -IDX = """ -SELECT TABLE_NAME, INDEX_NAME, GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX) cols, NON_UNIQUE -FROM information_schema.STATISTICS -WHERE TABLE_SCHEMA=%s AND TABLE_NAME IN ('organizations','download_tasks','energy_stats','charge_records','charge_orders') -GROUP BY TABLE_NAME, INDEX_NAME, NON_UNIQUE ORDER BY TABLE_NAME, INDEX_NAME -""" - -FK = """ -SELECT TABLE_NAME, CONSTRAINT_NAME, COLUMN_NAME, REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME -FROM information_schema.KEY_COLUMN_USAGE -WHERE TABLE_SCHEMA=%s AND REFERENCED_TABLE_NAME IS NOT NULL -ORDER BY TABLE_NAME, CONSTRAINT_NAME -""" - -for db in ("pms_dev", "pms"): - conn = pymysql.connect(host="10.8.0.252", user="root", password="Hbhyg731024@", database=db, charset="utf8mb4") - cur = conn.cursor() - print("=" * 78) - print("DB:", db) - print("=" * 78) - placeholders = ",".join(["(%s,%s)"] * len(TARGETS)) - flat = [db] + [x for t in TARGETS for x in t] - cur.execute(SQL % ("%s", placeholders), flat) - for r in cur.fetchall(): - print(f" {r[0]}.{r[1]:<14} type={r[2]:<18} data={r[3]:<10} null={r[4]:<4} default={str(r[5]):<20} extra={r[6]:<20} comment={r[7]}") - print(" -- 索引 --") - cur.execute(IDX, (db,)) - for r in cur.fetchall(): - print(f" {r[0]:<18} {r[1]:<38} ({r[2]}) uniq={0 if r[3]==0 else 1}") - print(" -- 外键 --") - cur.execute(FK, (db,)) - for r in cur.fetchall(): - print(f" {r[0]:<18} {r[1]:<40} {r[2]} -> {r[3]}.{r[4]}") - # row counts - print(" -- 行数 --") - for t in ("charge_records", "energy_stats", "download_tasks", "organizations", "charge_orders"): - try: - cur.execute(f"SELECT COUNT(*) FROM {t}") - print(f" {t:<18} {cur.fetchone()[0]}") - except Exception as e: - print(f" {t:<18} ERR {e}") - conn.close() diff --git a/.tmp-schema-align/probe2.py b/.tmp-schema-align/probe2.py deleted file mode 100644 index c1011b0..0000000 --- a/.tmp-schema-align/probe2.py +++ /dev/null @@ -1,37 +0,0 @@ -import pymysql - -def q(db, sql): - conn = pymysql.connect(host="10.8.0.252", user="root", password="Hbhyg731024@", database=db, charset="utf8mb4") - cur = conn.cursor() - cur.execute(sql) - rows = cur.fetchall() - conn.close() - return rows - -for db in ("pms_dev", "pms"): - print("==== ", db) - for r in q(db, """ - SELECT TABLE_NAME, COLUMN_NAME, DATA_TYPE, IS_NULLABLE, - IFNULL(CAST(COLUMN_DEFAULT AS CHAR),'') AS dflt, - COLUMN_TYPE, COLUMN_COMMENT - FROM information_schema.COLUMNS - WHERE TABLE_SCHEMA = '%s' - AND ((TABLE_NAME='charge_records' AND COLUMN_NAME IN ('energy','start_time','end_time')) - OR (TABLE_NAME='energy_stats' AND COLUMN_NAME='total_energy') - OR (TABLE_NAME='download_tasks' AND COLUMN_NAME='completed_at') - OR (TABLE_NAME='organizations' AND COLUMN_NAME='parent_id')) - ORDER BY TABLE_NAME, COLUMN_NAME""" % db): - print(" ", r) - -print("==== pms_dev data") -print("charge_records:", q("pms_dev", "SELECT id, compartment_id, cabinet_id, start_time, end_time, energy FROM charge_records")) -print("download_tasks:", q("pms_dev", "SELECT id, task_type, status, completed_at FROM download_tasks")) -print("organizations(parent_id not null):", q("pms_dev", "SELECT COUNT(*) FROM organizations WHERE parent_id IS NOT NULL")) -print("pms organizations count:", q("pms", "SELECT COUNT(*) FROM organizations")) -print("pms fk/index on organizations.parent_id:", q("pms", "SELECT INDEX_NAME, COLUMN_NAME, NON_UNIQUE FROM information_schema.STATISTICS WHERE TABLE_SCHEMA='pms' AND TABLE_NAME='organizations'")) -print("pms create table organizations:\n", q("pms", "SHOW CREATE TABLE organizations")[0][1]) -print("pms_dev create table organizations:\n", q("pms_dev", "SHOW CREATE TABLE organizations")[0][1]) -print("pms create table charge_records:\n", q("pms", "SHOW CREATE TABLE charge_records")[0][1]) -print("pms_dev create table charge_records:\n", q("pms_dev", "SHOW CREATE TABLE charge_records")[0][1]) -print("pms_dev create table download_tasks:\n", q("pms_dev", "SHOW CREATE TABLE download_tasks")[0][1]) -print("pms create table download_tasks:\n", q("pms", "SHOW CREATE TABLE download_tasks")[0][1]) diff --git a/.tmp-schema-align/probe3.py b/.tmp-schema-align/probe3.py deleted file mode 100644 index 84e282a..0000000 --- a/.tmp-schema-align/probe3.py +++ /dev/null @@ -1,18 +0,0 @@ -import pymysql - -SQL = """ -SELECT TABLE_NAME, COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE, - IFNULL(CAST(COLUMN_DEFAULT AS CHAR),'') AS dflt -FROM information_schema.COLUMNS -WHERE TABLE_SCHEMA = %s AND TABLE_NAME IN ('charge_orders','charge_records','download_tasks','energy_stats') -ORDER BY TABLE_NAME, ORDINAL_POSITION -""" - -for db in ("pms_dev", "pms"): - c = pymysql.connect(host="10.8.0.252", user="root", password="Hbhyg731024@", database=db) - cur = c.cursor() - cur.execute(SQL, (db,)) - print("==", db) - for r in cur.fetchall(): - print(" %-16s %-18s %-14s null=%-4s default=%s" % r) - c.close() diff --git a/.tmp-schema-align/prod_probe.py b/.tmp-schema-align/prod_probe.py deleted file mode 100644 index 3addbfb..0000000 --- a/.tmp-schema-align/prod_probe.py +++ /dev/null @@ -1,42 +0,0 @@ -"""只读探测生产环境 /api/charge-orders(验证同一解码缺陷是否在生产同样存在)""" -import base64, hashlib, hmac, json, ssl, time, urllib.request, urllib.error, pymysql - -JWT_SECRET = "787789e8eb1f3a10e81596f8219a20aa3c34102acacba2a5ed14bbb1d16ec833" -BASE = "https://app.anzhizhichong.com/api/pms" - - -def b64(b): - return base64.urlsafe_b64encode(b).rstrip(b"=").decode() - - -def sign(sub, phone): - now = int(time.time()) - seg = b64(json.dumps({"alg": "HS256", "typ": "JWT"}, separators=(",", ":")).encode()) + "." + \ - b64(json.dumps({"sub": sub, "phone": phone, "exp": now + 600, "iat": now}, - separators=(",", ":")).encode()) - return seg + "." + b64(hmac.new(JWT_SECRET.encode(), seg.encode(), hashlib.sha256).digest()) - - -def call(path, token): - req = urllib.request.Request(BASE + path, headers={"Authorization": "Bearer " + token}) - ctx = ssl.create_default_context() - ctx.check_hostname = False - ctx.verify_mode = ssl.CERT_NONE - try: - with urllib.request.urlopen(req, timeout=20, context=ctx) as r: - return r.status, r.read().decode("utf-8", "replace")[:300] - except urllib.error.HTTPError as e: - return e.code, e.read().decode("utf-8", "replace")[:300] - - -c = pymysql.connect(host="10.8.0.252", user="root", password="Hbhyg731024@", database="pms") -cur = c.cursor() -cur.execute("SELECT id, phone, role FROM users WHERE role >= 2 ORDER BY id LIMIT 5") -admins = cur.fetchall() -c.close() -print("prod super admins:", admins) -uid, phone, _ = admins[0] -tok = sign(uid, phone) -for p in ("/charge-orders?page=1&page_size=2", "/users?page=1&page_size=2", - "/charge-records?page=1&page_size=2", "/batteries?page=1&page_size=2"): - print(p, "->", call(p, tok)) diff --git a/.tmp-schema-align/remote-backup.sh b/.tmp-schema-align/remote-backup.sh deleted file mode 100644 index 8505905..0000000 --- a/.tmp-schema-align/remote-backup.sh +++ /dev/null @@ -1,6 +0,0 @@ -echo "=== ???? ===" -df -h /data | tail -1 -echo "=== ?? /usr/local/bin/pms-backup-db.sh??? pms + pms_dev?===" -/usr/local/bin/pms-backup-db.sh -echo "=== ???? ===" -ls -la /data/backup/mysql/*/ 2>/dev/null diff --git a/.tmp-schema-align/remote-backup2.sh b/.tmp-schema-align/remote-backup2.sh deleted file mode 100644 index 5c4cc1a..0000000 --- a/.tmp-schema-align/remote-backup2.sh +++ /dev/null @@ -1,5 +0,0 @@ -echo "=== ??????????? ===" -/usr/local/bin/pms-backup-db.sh -ls -la /data/backup/mysql/2026-09-20/ -echo "=== ??????? DDL ???===" -mysql --defaults-extra-file=/root/.my.cnf -N -B -e "SELECT table_name, table_rows FROM information_schema.tables WHERE table_schema='pms' ORDER BY table_rows DESC LIMIT 8" diff --git a/.tmp-schema-align/remote-backup3.sh b/.tmp-schema-align/remote-backup3.sh deleted file mode 100644 index e74c90b..0000000 --- a/.tmp-schema-align/remote-backup3.sh +++ /dev/null @@ -1 +0,0 @@ -/usr/local/bin/pms-backup-db.sh diff --git a/.tmp-schema-align/remote-retention-test.sh b/.tmp-schema-align/remote-retention-test.sh deleted file mode 100644 index 58a6bc1..0000000 --- a/.tmp-schema-align/remote-retention-test.sh +++ /dev/null @@ -1,20 +0,0 @@ -set -e -bash --version | head -1 -echo "=== ?????7 ???? + ????.env ??????????????????===" -rm -rf /tmp/retention-test -mkdir -p /tmp/retention-test -for ts in 20260918-010101 20260919-020202 20260919-065901 20260920-000048 20260920-003606 20260920-012048 20260920-023000; do - mkdir -p /tmp/retention-test/backup-$ts - head -c 1024 /dev/zero > /tmp/retention-test/backup-$ts/pms-api -done -echo "??????" > /tmp/retention-test/backup-20260920-004848-api.env -echo "??????????" > /tmp/retention-test/backup-2026092-010101 -mkdir -p /tmp/retention-test/web /tmp/retention-test/api-server -echo "=== ????? ===" -ls -la /tmp/retention-test | grep backup -echo "=== ???8? bash??? /data/pms ?? /tmp/retention-test?===" -sed 's#/data/pms#/tmp/retention-test#g' /tmp/retention.sh > /tmp/retention-test.sh -bash -n /tmp/retention-test.sh && echo "bash -n OK" -bash /tmp/retention-test.sh -echo "=== ??????????? 5 ???? + ????????===" -ls -la /tmp/retention-test diff --git a/.tmp-schema-align/remote1.sh b/.tmp-schema-align/remote1.sh deleted file mode 100644 index 75b9d33..0000000 --- a/.tmp-schema-align/remote1.sh +++ /dev/null @@ -1,3 +0,0 @@ -journalctl -u pms-api-test --since '-5 min' --no-pager | grep -iE 'ERROR|mismatch|export' | tail -20 -echo '=== download_tasks ===' -mysql --defaults-extra-file=/root/.my.cnf -N -B -e "SELECT id,task_type,status,progress,file_path,completed_at FROM pms_dev.download_tasks ORDER BY id" diff --git a/.tmp-schema-align/remote10.sh b/.tmp-schema-align/remote10.sh deleted file mode 100644 index 2c9add5..0000000 --- a/.tmp-schema-align/remote10.sh +++ /dev/null @@ -1,18 +0,0 @@ -echo "=== 验收脚本产生的导出文件(两环境)===" -ls -la /data/pms_dev/api-server/exports/ /data/pms/api-server/exports/ 2>/dev/null -echo "=== 删除(exports 目录本身保留,属主 pms 不动)===" -rm -f "/data/pms_dev/api-server/exports/充电记录__-.xlsx" "/data/pms/api-server/exports/充电记录__-.xlsx" -echo "=== 删除后 ===" -ls -la /data/pms_dev/api-server/exports/ /data/pms/api-server/exports/ -stat -c '%U:%G %n' /data/pms_dev/api-server/exports /data/pms/api-server/exports /data/pms/api-server/uploads -echo "=== 临时数据残留检查(应全 0)===" -mysql --defaults-extra-file=/root/.my.cnf -N -B -e " -SELECT 'dev.charge_orders', COUNT(*) FROM pms_dev.charge_orders WHERE battery_id LIKE 'VERIFY-%' -UNION ALL SELECT 'dev.device_logs', COUNT(*) FROM pms_dev.device_logs WHERE content LIKE '%VERIFY-%' -UNION ALL SELECT 'dev.download_tasks', COUNT(*) FROM pms_dev.download_tasks -UNION ALL SELECT 'dev.users_verify', COUNT(*) FROM pms_dev.users WHERE name LIKE 'VERIFY-%' -UNION ALL SELECT 'prod.charge_orders_all', COUNT(*) FROM pms.charge_orders -UNION ALL SELECT 'prod.device_logs_all', COUNT(*) FROM pms.device_logs -UNION ALL SELECT 'prod.download_tasks', COUNT(*) FROM pms.download_tasks" -echo "=== 服务 ===" -systemctl is-active pms-api pms-api-test pms-device pms-device-test nginx | paste -sd' ' diff --git a/.tmp-schema-align/remote2.sh b/.tmp-schema-align/remote2.sh deleted file mode 100644 index 82841a1..0000000 --- a/.tmp-schema-align/remote2.sh +++ /dev/null @@ -1,5 +0,0 @@ -echo "=== ???????????????????????===" -sha256sum /data/pms_dev/api-server/pms-api -systemctl show -p MainPID --value pms-api-test -echo "=== ??? download_tasks ===" -mysql --defaults-extra-file=/root/.my.cnf -N -B -e "SELECT id,task_type,status,progress,file_path,completed_at FROM pms_dev.download_tasks ORDER BY id" diff --git a/.tmp-schema-align/remote3.sh b/.tmp-schema-align/remote3.sh deleted file mode 100644 index a5f15c7..0000000 --- a/.tmp-schema-align/remote3.sh +++ /dev/null @@ -1,5 +0,0 @@ -echo "=== ???? ===" -ls -la /data/pms_dev/api-server/exports/ -file /data/pms_dev/api-server/exports/*.xlsx 2>/dev/null -echo "=== journal???????? + ERROR?===" -journalctl -u pms-api-test --since '-4 min' --no-pager | grep -iE 'export|??|ERROR|mismatch' | tail -20 diff --git a/.tmp-schema-align/remote4.sh b/.tmp-schema-align/remote4.sh deleted file mode 100644 index 9d9de7d..0000000 --- a/.tmp-schema-align/remote4.sh +++ /dev/null @@ -1 +0,0 @@ -journalctl -u pms-api-test --since '-2 min' --no-pager | grep -iE 'ERROR' | tail -6 diff --git a/.tmp-schema-align/remote5.sh b/.tmp-schema-align/remote5.sh deleted file mode 100644 index c983318..0000000 --- a/.tmp-schema-align/remote5.sh +++ /dev/null @@ -1,6 +0,0 @@ -echo "=== 清理前 exports ===" -ls -la /data/pms_dev/api-server/exports/ -rm -f "/data/pms_dev/api-server/exports/充电记录__-.xlsx" -echo "=== 清理后 exports(目录本身保留,属主 pms 不动)===" -ls -la /data/pms_dev/api-server/exports/ -stat -c '%U:%G %n' /data/pms_dev/api-server/exports diff --git a/.tmp-schema-align/remote6.sh b/.tmp-schema-align/remote6.sh deleted file mode 100644 index ca03d75..0000000 --- a/.tmp-schema-align/remote6.sh +++ /dev/null @@ -1,29 +0,0 @@ -set -e -echo "############ 清理前 ############" -echo "--- /data/pms 顶层 ---" -ls -la /data/pms/ -echo "--- 回滚点(严格 backup-YYYYMMDD-HHMMSS 目录)---" -shopt -s nullglob -n=0 -for d in /data/pms/backup-[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-[0-9][0-9][0-9][0-9][0-9][0-9]; do - if [ -d "$d" ]; then - echo " $(du -sh "$d" | cut -f1) $d" - n=$((n+1)) - fi -done -echo " 回滚点数量=$n" -echo "--- backup-* 全部条目(含非回滚点)---" -ls -d /data/pms/backup-* 2>/dev/null || echo " (none)" -echo "--- 磁盘 ---" -df -h /data | tail -1 -du -sh /data/pms /data/pms_dev - -echo -echo "############ 执行回滚点保留策略(KEEP=5,真机)############" -bash /tmp/retention.sh - -echo -echo "############ 清理后 ############" -ls -la /data/pms/ -df -h /data | tail -1 -du -sh /data/pms /data/pms_dev diff --git a/.tmp-schema-align/remote7.sh b/.tmp-schema-align/remote7.sh deleted file mode 100644 index 345b074..0000000 --- a/.tmp-schema-align/remote7.sh +++ /dev/null @@ -1,57 +0,0 @@ -set -u -DIR=/data/pms/api-server -echo "############ /data/pms/api-server 清理前 ############" -ls -la "$DIR" -echo "--- 目录占用 ---" -du -sh "$DIR" -echo "--- 当前运行中的二进制(绝不能碰)---" -systemctl show -p ExecStart --value pms-api -MAINPID=$(systemctl show -p MainPID --value pms-api) -echo "MainPID=$MAINPID exe=$(readlink -f /proc/$MAINPID/exe 2>/dev/null)" -RUNNING=$(readlink -f /proc/$MAINPID/exe 2>/dev/null) - -echo -echo "############ 候选清单(历史二进制,排除当前 pms-api / .env / uploads / exports)############" -# 候选 = 该目录下的普通文件,且名字以 api-server 或 pms-api.bak 开头(严格白名单前缀) -cands=() -for f in "$DIR"/*; do - if [ -f "$f" ]; then - b=$(basename "$f") - case "$b" in - api-server|api-server.bak|api-server_old|pms-api.bak*|api-server*) cands+=( "$f" ) ;; - esac - fi -done -# 按 mtime 从新到旧排序 -mapfile -t sorted < <(for f in "${cands[@]}"; do stat -c '%Y %n' "$f"; done | sort -rn | awk '{print $2}') -KEEP=2 -echo " 候选(新→旧):" -for f in "${sorted[@]}"; do - echo " $(stat -c '%y %s %n' "$f")" -done -echo " 保留最近 $KEEP 个:" -for ((i=0;i/dev/null || echo " /tmp 已无本次临时文件" diff --git a/.tmp-schema-align/remote9.sh b/.tmp-schema-align/remote9.sh deleted file mode 100644 index 34291f8..0000000 --- a/.tmp-schema-align/remote9.sh +++ /dev/null @@ -1,27 +0,0 @@ -echo "=== 磁盘/目录 最终状态 ===" -df -h /data | tail -1 -du -sh /data/pms /data/pms_dev /data/backup -echo "--- /data/pms 顶层 ---" -ls -la /data/pms/ -echo "--- 回滚点(严格模式匹配)---" -shopt -s nullglob -for d in /data/pms/backup-[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-[0-9][0-9][0-9][0-9][0-9][0-9]; do - if [ -d "$d" ]; then echo " $(du -sh "$d" | cut -f1) $d"; fi -done -echo "--- /data/pms/api-server ---" -ls -la /data/pms/api-server/ -echo "=== 临时数据残留检查(应全为 0)===" -mysql --defaults-extra-file=/root/.my.cnf -N -B -e " -SELECT 'dev.charge_orders', COUNT(*) FROM pms_dev.charge_orders WHERE battery_id='VERIFY-SCHEMA-TMP' -UNION ALL SELECT 'dev.device_logs', COUNT(*) FROM pms_dev.device_logs WHERE content LIKE '%VERIFY-SCHEMA-TMP%' -UNION ALL SELECT 'dev.download_tasks', COUNT(*) FROM pms_dev.download_tasks -UNION ALL SELECT 'dev.tmp_users', COUNT(*) FROM pms_dev.users WHERE phone IN ('19900000001','19900000002','19900000003') -UNION ALL SELECT 'prod.charge_orders', COUNT(*) FROM pms.charge_orders WHERE battery_id='VERIFY-SCHEMA-TMP' -UNION ALL SELECT 'prod.device_logs', COUNT(*) FROM pms.device_logs WHERE content LIKE '%VERIFY-SCHEMA-TMP%' -UNION ALL SELECT 'prod.charge_orders_total', COUNT(*) FROM pms.charge_orders -UNION ALL SELECT 'prod.device_logs_total', COUNT(*) FROM pms.device_logs" -echo "=== 服务状态 ===" -systemctl is-active pms-api pms-api-test pms-device pms-device-test nginx | paste -sd' ' -rm -f /tmp/remote7.sh /tmp/remote8.sh -echo "=== 清理临时脚本后 ===" -ls /tmp/*.sh 2>/dev/null || echo " /tmp 无 .sh" diff --git a/.tmp-schema-align/retention.sh b/.tmp-schema-align/retention.sh deleted file mode 100644 index 58f4d00..0000000 --- a/.tmp-schema-align/retention.sh +++ /dev/null @@ -1,50 +0,0 @@ -set -e -shopt -s nullglob -KEEP=5 -# glob 字符类只匹配严格的 backup-YYYYMMDD-HHMMSS;nullglob 让无匹配时展开为空数组。 -# glob 结果按字典序排列,对该命名格式而言等价于时间戳升序(最早的在前)。 -rollbacks=() -for p in /data/pms/backup-[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-[0-9][0-9][0-9][0-9][0-9][0-9]; do - if [ -d "$p" ]; then - rollbacks+=( "$p" ) - fi -done -total=${#rollbacks[@]} -echo " 当前回滚点 $total 个(deploy 会为每次部署新建一份):" -for p in "${rollbacks[@]}"; do - echo " $p" -done -if [ "$total" -gt "$KEEP" ]; then - drop=$((total - KEEP)) - echo " 将删除最早的 $drop 个(保留最近 $KEEP 个):" - i=0 - for p in "${rollbacks[@]}"; do - if [ "$i" -ge "$drop" ]; then - break - fi - i=$((i + 1)) - # 复核:名字必须严格符合 backup-YYYYMMDD-HHMMSS,否则拒绝删除(绝不碰其它目录) - case "$(basename "$p")" in - backup-[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-[0-9][0-9][0-9][0-9][0-9][0-9]) - echo " 删除 $p" - rm -rf -- "$p" - ;; - *) - echo " [skip] 名字不符合回滚点模式,拒绝删除: $p" - ;; - esac - done -else - echo " 回滚点数量未超过 $KEEP,无需清理" -fi -kept=() -for p in /data/pms/backup-[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-[0-9][0-9][0-9][0-9][0-9][0-9]; do - if [ -d "$p" ]; then - kept+=( "$p" ) - fi -done -echo " 清理后保留的回滚点 ${#kept[@]} 个:" -for p in "${kept[@]}"; do - echo " $p" -done -echo " /data/pms 占用: $(du -sh /data/pms | cut -f1)" \ No newline at end of file diff --git a/.tmp-schema-align/run-dev/run1.err.txt b/.tmp-schema-align/run-dev/run1.err.txt deleted file mode 100644 index e69de29..0000000 diff --git a/.tmp-schema-align/run-dev/run1.out.txt b/.tmp-schema-align/run-dev/run1.out.txt deleted file mode 100644 index f7f834b..0000000 --- a/.tmp-schema-align/run-dev/run1.out.txt +++ /dev/null @@ -1,17 +0,0 @@ -2026-09-19T18:12:24.144708Z INFO pms_api: 配置加载完成 -2026-09-19T18:12:24.227790Z INFO pms_api::db: MySQL 连接池已建立 -2026-09-19T18:12:24.229316Z INFO pms_api: 数据库连接成功 -2026-09-19T18:12:26.157847Z INFO pms_api::db::migrate: 数据库索引创建完成 -2026-09-19T18:12:27.053201Z INFO pms_api::db::migrate: 数据库列级迁移完成 -2026-09-19T18:12:27.205445Z INFO pms_api::db::migrate: [migrate] 列定义对齐 charge_records.energy:type decimal -> bigint, nullable YES -> NO, default '' -> '0', 注释已按目标口径同步;DDL=ALTER TABLE charge_records MODIFY COLUMN energy BIGINT NOT NULL DEFAULT 0 COMMENT '充电量(Wh)' -2026-09-19T18:12:27.309455Z INFO pms_api::db::migrate: [migrate] 列定义对齐 charge_records.start_time:type timestamp -> datetime, nullable YES -> NO, default '' -> 'CURRENT_TIMESTAMP', 注释无需变更;DDL=ALTER TABLE charge_records MODIFY COLUMN start_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP -2026-09-19T18:12:27.414008Z INFO pms_api::db::migrate: [migrate] 列定义对齐 charge_records.end_time:type timestamp -> datetime, nullable YES -> NO, default '' -> 'CURRENT_TIMESTAMP', 注释无需变更;DDL=ALTER TABLE charge_records MODIFY COLUMN end_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP -2026-09-19T18:12:27.514222Z INFO pms_api::db::migrate: [migrate] 列定义对齐 energy_stats.total_energy:type decimal -> bigint, nullable YES -> NO, default '' -> '0', 注释已按目标口径同步;DDL=ALTER TABLE energy_stats MODIFY COLUMN total_energy BIGINT NOT NULL DEFAULT 0 COMMENT '总能耗(Wh)' -2026-09-19T18:12:27.610518Z ERROR pms_api::db::migrate: [migrate] 列定义对齐失败 download_tasks.completed_at(跳过该列,继续启动)err=error returned from database: 1265 (01000): Data truncated for column 'completed_at' at row 1 -2026-09-19T18:12:27.761165Z INFO pms_api::db::migrate: [migrate] 已创建索引 idx_organizations_parent ON organizations(parent_id) -2026-09-19T18:12:27.889135Z INFO pms_api::db::migrate: [migrate] 已补齐自引用外键 fk_organizations_parent (organizations.parent_id -> organizations.id) -2026-09-19T18:12:27.889777Z INFO pms_api::db::migrate: 历史库列定义对齐完成:本次执行了 5 项 DDL -2026-09-19T18:12:29.166466Z INFO pms_api::db::migrate: 默认权限码初始化完成 -2026-09-19T18:12:33.212943Z INFO pms_api::db::migrate: 默认角色及权限分配完成 -2026-09-19T18:12:33.213915Z INFO pms_api::db::migrate: 数据库迁移完成 -2026-09-19T18:12:33.214448Z INFO pms_api: 数据库迁移完成 diff --git a/.tmp-schema-align/run-dev/run2.err.txt b/.tmp-schema-align/run-dev/run2.err.txt deleted file mode 100644 index e69de29..0000000 diff --git a/.tmp-schema-align/run-dev/run2.out.txt b/.tmp-schema-align/run-dev/run2.out.txt deleted file mode 100644 index 99226c6..0000000 --- a/.tmp-schema-align/run-dev/run2.out.txt +++ /dev/null @@ -1,11 +0,0 @@ -2026-09-19T18:12:36.776224Z INFO pms_api: 配置加载完成 -2026-09-19T18:12:36.870145Z INFO pms_api::db: MySQL 连接池已建立 -2026-09-19T18:12:36.871305Z INFO pms_api: 数据库连接成功 -2026-09-19T18:12:38.777642Z INFO pms_api::db::migrate: 数据库索引创建完成 -2026-09-19T18:12:39.657395Z INFO pms_api::db::migrate: 数据库列级迁移完成 -2026-09-19T18:12:39.977288Z ERROR pms_api::db::migrate: [migrate] 列定义对齐失败 download_tasks.completed_at(跳过该列,继续启动)err=error returned from database: 1265 (01000): Data truncated for column 'completed_at' at row 1 -2026-09-19T18:12:40.094118Z INFO pms_api::db::migrate: 历史库列定义对齐检查完成:全部已一致,未执行任何 DDL -2026-09-19T18:12:41.314460Z INFO pms_api::db::migrate: 默认权限码初始化完成 -2026-09-19T18:12:45.457970Z INFO pms_api::db::migrate: 默认角色及权限分配完成 -2026-09-19T18:12:45.458574Z INFO pms_api::db::migrate: 数据库迁移完成 -2026-09-19T18:12:45.459131Z INFO pms_api: 数据库迁移完成 diff --git a/.tmp-schema-align/run-dev/run3.err.txt b/.tmp-schema-align/run-dev/run3.err.txt deleted file mode 100644 index e69de29..0000000 diff --git a/.tmp-schema-align/run-dev/run3.out.txt b/.tmp-schema-align/run-dev/run3.out.txt deleted file mode 100644 index bca4ff6..0000000 --- a/.tmp-schema-align/run-dev/run3.out.txt +++ /dev/null @@ -1,11 +0,0 @@ -2026-09-19T18:13:19.197105Z INFO pms_api: 配置加载完成 -2026-09-19T18:13:19.273975Z INFO pms_api::db: MySQL 连接池已建立 -2026-09-19T18:13:19.276092Z INFO pms_api: 数据库连接成功 -2026-09-19T18:13:21.286065Z INFO pms_api::db::migrate: 数据库索引创建完成 -2026-09-19T18:13:22.153072Z INFO pms_api::db::migrate: 数据库列级迁移完成 -2026-09-19T18:13:22.452279Z INFO pms_api::db::migrate: [migrate] 列定义对齐 download_tasks.completed_at:type timestamp -> datetime, nullable YES -> NO, default '' -> 'CURRENT_TIMESTAMP', 注释无需变更;DDL=ALTER TABLE download_tasks MODIFY COLUMN completed_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP -2026-09-19T18:13:22.565319Z INFO pms_api::db::migrate: 历史库列定义对齐完成:本次执行了 1 项 DDL -2026-09-19T18:13:23.781076Z INFO pms_api::db::migrate: 默认权限码初始化完成 -2026-09-19T18:13:27.768587Z INFO pms_api::db::migrate: 默认角色及权限分配完成 -2026-09-19T18:13:27.769587Z INFO pms_api::db::migrate: 数据库迁移完成 -2026-09-19T18:13:27.770433Z INFO pms_api: 数据库迁移完成 diff --git a/.tmp-schema-align/run-dev/run4.err.txt b/.tmp-schema-align/run-dev/run4.err.txt deleted file mode 100644 index e69de29..0000000 diff --git a/.tmp-schema-align/run-dev/run4.out.txt b/.tmp-schema-align/run-dev/run4.out.txt deleted file mode 100644 index e697385..0000000 --- a/.tmp-schema-align/run-dev/run4.out.txt +++ /dev/null @@ -1,10 +0,0 @@ -2026-09-19T18:13:34.245218Z INFO pms_api: 配置加载完成 -2026-09-19T18:13:34.320563Z INFO pms_api::db: MySQL 连接池已建立 -2026-09-19T18:13:34.321757Z INFO pms_api: 数据库连接成功 -2026-09-19T18:13:36.318097Z INFO pms_api::db::migrate: 数据库索引创建完成 -2026-09-19T18:13:37.243873Z INFO pms_api::db::migrate: 数据库列级迁移完成 -2026-09-19T18:13:37.584574Z INFO pms_api::db::migrate: 历史库列定义对齐检查完成:全部已一致,未执行任何 DDL -2026-09-19T18:13:38.804532Z INFO pms_api::db::migrate: 默认权限码初始化完成 -2026-09-19T18:13:42.873183Z INFO pms_api::db::migrate: 默认角色及权限分配完成 -2026-09-19T18:13:42.874471Z INFO pms_api::db::migrate: 数据库迁移完成 -2026-09-19T18:13:42.875657Z INFO pms_api: 数据库迁移完成 diff --git a/.tmp-schema-align/run-dev/run5.err.txt b/.tmp-schema-align/run-dev/run5.err.txt deleted file mode 100644 index e69de29..0000000 diff --git a/.tmp-schema-align/run-dev/run5.out.txt b/.tmp-schema-align/run-dev/run5.out.txt deleted file mode 100644 index 8da061e..0000000 --- a/.tmp-schema-align/run-dev/run5.out.txt +++ /dev/null @@ -1,42 +0,0 @@ -2026-09-19T18:24:50.537001Z INFO pms_api: 配置加载完成 -2026-09-19T18:24:50.617127Z INFO pms_api::db: MySQL 连接池已建立 -2026-09-19T18:24:50.618151Z INFO pms_api: 数据库连接成功 -2026-09-19T18:24:52.548639Z INFO pms_api::db::migrate: 数据库索引创建完成 -2026-09-19T18:24:53.448374Z INFO pms_api::db::migrate: 数据库列级迁移完成 -2026-09-19T18:24:53.576589Z INFO pms_api::db::migrate: [migrate] 列定义对齐 organizations.created_at:type timestamp -> DATETIME, nullable NO -> NO, default 'CURRENT_TIMESTAMP' -> 'CURRENT_TIMESTAMP', on_update 无 -> 不需要, 注释已一致/未校验;DDL=ALTER TABLE organizations MODIFY COLUMN created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP -2026-09-19T18:24:53.680300Z INFO pms_api::db::migrate: [migrate] 列定义对齐 projects.created_at:type timestamp -> DATETIME, nullable NO -> NO, default 'CURRENT_TIMESTAMP' -> 'CURRENT_TIMESTAMP', on_update 无 -> 不需要, 注释已一致/未校验;DDL=ALTER TABLE projects MODIFY COLUMN created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP -2026-09-19T18:24:53.793972Z INFO pms_api::db::migrate: [migrate] 列定义对齐 cabinets.created_at:type timestamp -> DATETIME, nullable NO -> NO, default 'CURRENT_TIMESTAMP' -> 'CURRENT_TIMESTAMP', on_update 无 -> 不需要, 注释已一致/未校验;DDL=ALTER TABLE cabinets MODIFY COLUMN created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP -2026-09-19T18:24:53.895396Z INFO pms_api::db::migrate: [migrate] 列定义对齐 cabin_boards.created_at:type timestamp -> DATETIME, nullable NO -> NO, default 'CURRENT_TIMESTAMP' -> 'CURRENT_TIMESTAMP', on_update 无 -> 不需要, 注释已一致/未校验;DDL=ALTER TABLE cabin_boards MODIFY COLUMN created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP -2026-09-19T18:24:54.016489Z INFO pms_api::db::migrate: [migrate] 列定义对齐 compartments.created_at:type timestamp -> DATETIME, nullable NO -> NO, default 'CURRENT_TIMESTAMP' -> 'CURRENT_TIMESTAMP', on_update 无 -> 不需要, 注释已一致/未校验;DDL=ALTER TABLE compartments MODIFY COLUMN created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP -2026-09-19T18:24:54.309576Z INFO pms_api::db::migrate: [migrate] 列定义对齐 batteries.created_at:type timestamp -> DATETIME, nullable NO -> NO, default 'CURRENT_TIMESTAMP' -> 'CURRENT_TIMESTAMP', on_update 无 -> 不需要, 注释已一致/未校验;DDL=ALTER TABLE batteries MODIFY COLUMN created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP -2026-09-19T18:24:54.489206Z INFO pms_api::db::migrate: [migrate] 列定义对齐 charge_orders.created_at:type timestamp -> DATETIME, nullable NO -> NO, default 'CURRENT_TIMESTAMP' -> 'CURRENT_TIMESTAMP', on_update 无 -> 不需要, 注释已一致/未校验;DDL=ALTER TABLE charge_orders MODIFY COLUMN created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP -2026-09-19T18:24:54.600574Z INFO pms_api::db::migrate: [migrate] 列定义对齐 users.created_at:type timestamp -> DATETIME, nullable NO -> NO, default 'CURRENT_TIMESTAMP' -> 'CURRENT_TIMESTAMP', on_update 无 -> 不需要, 注释已一致/未校验;DDL=ALTER TABLE users MODIFY COLUMN created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP -2026-09-19T18:24:54.706993Z INFO pms_api::db::migrate: [migrate] 列定义对齐 charge_records.created_at:type timestamp -> DATETIME, nullable NO -> NO, default 'CURRENT_TIMESTAMP' -> 'CURRENT_TIMESTAMP', on_update 无 -> 不需要, 注释已一致/未校验;DDL=ALTER TABLE charge_records MODIFY COLUMN created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP -2026-09-19T18:24:54.808376Z INFO pms_api::db::migrate: [migrate] 列定义对齐 device_logs.created_at:type timestamp -> DATETIME, nullable NO -> NO, default 'CURRENT_TIMESTAMP' -> 'CURRENT_TIMESTAMP', on_update 无 -> 不需要, 注释已一致/未校验;DDL=ALTER TABLE device_logs MODIFY COLUMN created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP -2026-09-19T18:24:54.914284Z INFO pms_api::db::migrate: [migrate] 列定义对齐 energy_stats.created_at:type timestamp -> DATETIME, nullable NO -> NO, default 'CURRENT_TIMESTAMP' -> 'CURRENT_TIMESTAMP', on_update 无 -> 不需要, 注释已一致/未校验;DDL=ALTER TABLE energy_stats MODIFY COLUMN created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP -2026-09-19T18:24:55.023111Z INFO pms_api::db::migrate: [migrate] 列定义对齐 operation_logs.created_at:type timestamp -> DATETIME, nullable NO -> NO, default 'CURRENT_TIMESTAMP' -> 'CURRENT_TIMESTAMP', on_update 无 -> 不需要, 注释已一致/未校验;DDL=ALTER TABLE operation_logs MODIFY COLUMN created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP -2026-09-19T18:24:55.142571Z INFO pms_api::db::migrate: [migrate] 列定义对齐 roles.created_at:type timestamp -> DATETIME, nullable NO -> NO, default 'CURRENT_TIMESTAMP' -> 'CURRENT_TIMESTAMP', on_update 无 -> 不需要, 注释已一致/未校验;DDL=ALTER TABLE roles MODIFY COLUMN created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP -2026-09-19T18:24:55.243247Z INFO pms_api::db::migrate: [migrate] 列定义对齐 permissions.created_at:type timestamp -> DATETIME, nullable NO -> NO, default 'CURRENT_TIMESTAMP' -> 'CURRENT_TIMESTAMP', on_update 无 -> 不需要, 注释已一致/未校验;DDL=ALTER TABLE permissions MODIFY COLUMN created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP -2026-09-19T18:24:55.345065Z INFO pms_api::db::migrate: [migrate] 列定义对齐 user_scopes.created_at:type timestamp -> DATETIME, nullable NO -> NO, default 'CURRENT_TIMESTAMP' -> 'CURRENT_TIMESTAMP', on_update 无 -> 不需要, 注释已一致/未校验;DDL=ALTER TABLE user_scopes MODIFY COLUMN created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP -2026-09-19T18:24:55.449238Z INFO pms_api::db::migrate: [migrate] 列定义对齐 download_tasks.created_at:type timestamp -> DATETIME, nullable NO -> NO, default 'CURRENT_TIMESTAMP' -> 'CURRENT_TIMESTAMP', on_update 无 -> 不需要, 注释已一致/未校验;DDL=ALTER TABLE download_tasks MODIFY COLUMN created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP -2026-09-19T18:24:55.561957Z INFO pms_api::db::migrate: [migrate] 列定义对齐 abstract_bindings.created_at:type timestamp -> DATETIME, nullable NO -> NO, default 'CURRENT_TIMESTAMP' -> 'CURRENT_TIMESTAMP', on_update 无 -> 不需要, 注释已一致/未校验;DDL=ALTER TABLE abstract_bindings MODIFY COLUMN created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP -2026-09-19T18:24:55.665036Z INFO pms_api::db::migrate: [migrate] 列定义对齐 user_feedback.created_at:type timestamp -> DATETIME, nullable NO -> NO, default 'CURRENT_TIMESTAMP' -> 'CURRENT_TIMESTAMP', on_update 无 -> 不需要, 注释已一致/未校验;DDL=ALTER TABLE user_feedback MODIFY COLUMN created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP -2026-09-19T18:24:55.819095Z INFO pms_api::db::migrate: [migrate] 列定义对齐 organizations.updated_at:type timestamp -> DATETIME, nullable NO -> NO, default 'CURRENT_TIMESTAMP' -> 'CURRENT_TIMESTAMP', on_update 有 -> 需要, 注释已一致/未校验;DDL=ALTER TABLE organizations MODIFY COLUMN updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP -2026-09-19T18:24:55.922669Z INFO pms_api::db::migrate: [migrate] 列定义对齐 projects.updated_at:type timestamp -> DATETIME, nullable NO -> NO, default 'CURRENT_TIMESTAMP' -> 'CURRENT_TIMESTAMP', on_update 有 -> 需要, 注释已一致/未校验;DDL=ALTER TABLE projects MODIFY COLUMN updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP -2026-09-19T18:24:56.033176Z INFO pms_api::db::migrate: [migrate] 列定义对齐 cabinets.updated_at:type timestamp -> DATETIME, nullable NO -> NO, default 'CURRENT_TIMESTAMP' -> 'CURRENT_TIMESTAMP', on_update 有 -> 需要, 注释已一致/未校验;DDL=ALTER TABLE cabinets MODIFY COLUMN updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP -2026-09-19T18:24:56.143306Z INFO pms_api::db::migrate: [migrate] 列定义对齐 cabin_boards.updated_at:type timestamp -> DATETIME, nullable NO -> NO, default 'CURRENT_TIMESTAMP' -> 'CURRENT_TIMESTAMP', on_update 有 -> 需要, 注释已一致/未校验;DDL=ALTER TABLE cabin_boards MODIFY COLUMN updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP -2026-09-19T18:24:56.248684Z INFO pms_api::db::migrate: [migrate] 列定义对齐 compartments.updated_at:type timestamp -> DATETIME, nullable NO -> NO, default 'CURRENT_TIMESTAMP' -> 'CURRENT_TIMESTAMP', on_update 有 -> 需要, 注释已一致/未校验;DDL=ALTER TABLE compartments MODIFY COLUMN updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP -2026-09-19T18:24:56.351893Z INFO pms_api::db::migrate: [migrate] 列定义对齐 batteries.updated_at:type timestamp -> DATETIME, nullable NO -> NO, default 'CURRENT_TIMESTAMP' -> 'CURRENT_TIMESTAMP', on_update 有 -> 需要, 注释已一致/未校验;DDL=ALTER TABLE batteries MODIFY COLUMN updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP -2026-09-19T18:24:56.462717Z INFO pms_api::db::migrate: [migrate] 列定义对齐 users.updated_at:type timestamp -> DATETIME, nullable NO -> NO, default 'CURRENT_TIMESTAMP' -> 'CURRENT_TIMESTAMP', on_update 有 -> 需要, 注释已一致/未校验;DDL=ALTER TABLE users MODIFY COLUMN updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP -2026-09-19T18:24:56.564295Z INFO pms_api::db::migrate: [migrate] 列定义对齐 roles.updated_at:type timestamp -> DATETIME, nullable NO -> NO, default 'CURRENT_TIMESTAMP' -> 'CURRENT_TIMESTAMP', on_update 有 -> 需要, 注释已一致/未校验;DDL=ALTER TABLE roles MODIFY COLUMN updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP -2026-09-19T18:24:56.665845Z INFO pms_api::db::migrate: [migrate] 列定义对齐 charge_records.start_time:type datetime -> DATETIME, nullable NO -> NO, default 'CURRENT_TIMESTAMP' -> 'CURRENT_TIMESTAMP', on_update 无 -> 不需要, 注释已按目标口径同步;DDL=ALTER TABLE charge_records MODIFY COLUMN start_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '充电开始时间' -2026-09-19T18:24:56.768583Z INFO pms_api::db::migrate: [migrate] 列定义对齐 charge_records.end_time:type datetime -> DATETIME, nullable NO -> NO, default 'CURRENT_TIMESTAMP' -> 'CURRENT_TIMESTAMP', on_update 无 -> 不需要, 注释已按目标口径同步;DDL=ALTER TABLE charge_records MODIFY COLUMN end_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '充电结束时间' -2026-09-19T18:24:56.884752Z INFO pms_api::db::migrate: [migrate] 列定义对齐 charge_orders.start_time:type timestamp -> DATETIME, nullable YES -> YES, default '' -> '', on_update 无 -> 不需要, 注释已一致/未校验;DDL=ALTER TABLE charge_orders MODIFY COLUMN start_time DATETIME NULL COMMENT '充电开始时间' -2026-09-19T18:24:57.036290Z INFO pms_api::db::migrate: [migrate] 列定义对齐 charge_orders.end_time:type timestamp -> DATETIME, nullable YES -> YES, default '' -> '', on_update 无 -> 不需要, 注释已一致/未校验;DDL=ALTER TABLE charge_orders MODIFY COLUMN end_time DATETIME NULL COMMENT '充电结束时间' -2026-09-19T18:24:57.132247Z INFO pms_api::db::migrate: [migrate] 列定义对齐 download_tasks.completed_at:type datetime -> DATETIME, nullable NO -> NO, default 'CURRENT_TIMESTAMP' -> 'CURRENT_TIMESTAMP', on_update 无 -> 不需要, 注释已按目标口径同步;DDL=ALTER TABLE download_tasks MODIFY COLUMN completed_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '完成时间' -2026-09-19T18:24:57.291558Z INFO pms_api::db::migrate: [migrate] 列定义对齐 sim_change_logs.changed_at:type timestamp -> DATETIME, nullable NO -> NO, default 'CURRENT_TIMESTAMP' -> 'CURRENT_TIMESTAMP', on_update 无 -> 不需要, 注释已一致/未校验;DDL=ALTER TABLE sim_change_logs MODIFY COLUMN changed_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '变更时间' -2026-09-19T18:24:57.533530Z INFO pms_api::db::migrate: 历史库列定义对齐完成:本次执行了 32 项 DDL -2026-09-19T18:24:58.890249Z INFO pms_api::db::migrate: 默认权限码初始化完成 -2026-09-19T18:25:02.980032Z INFO pms_api::db::migrate: 默认角色及权限分配完成 -2026-09-19T18:25:02.981647Z INFO pms_api::db::migrate: 数据库迁移完成 -2026-09-19T18:25:02.982297Z INFO pms_api: 数据库迁移完成 diff --git a/.tmp-schema-align/run-dev/run6.err.txt b/.tmp-schema-align/run-dev/run6.err.txt deleted file mode 100644 index e69de29..0000000 diff --git a/.tmp-schema-align/run-dev/run6.out.txt b/.tmp-schema-align/run-dev/run6.out.txt deleted file mode 100644 index 6ac4a97..0000000 --- a/.tmp-schema-align/run-dev/run6.out.txt +++ /dev/null @@ -1,10 +0,0 @@ -2026-09-19T18:25:07.520560Z INFO pms_api: 配置加载完成 -2026-09-19T18:25:07.601492Z INFO pms_api::db: MySQL 连接池已建立 -2026-09-19T18:25:07.602585Z INFO pms_api: 数据库连接成功 -2026-09-19T18:25:09.516627Z INFO pms_api::db::migrate: 数据库索引创建完成 -2026-09-19T18:25:10.376441Z INFO pms_api::db::migrate: 数据库列级迁移完成 -2026-09-19T18:25:12.080675Z INFO pms_api::db::migrate: 历史库列定义对齐检查完成:35 列 + organizations.parent_id 索引/外键全部已一致,未执行任何 DDL -2026-09-19T18:25:13.318766Z INFO pms_api::db::migrate: 默认权限码初始化完成 -2026-09-19T18:25:17.527106Z INFO pms_api::db::migrate: 默认角色及权限分配完成 -2026-09-19T18:25:17.528224Z INFO pms_api::db::migrate: 数据库迁移完成 -2026-09-19T18:25:17.528889Z INFO pms_api: 数据库迁移完成 diff --git a/.tmp-schema-align/run-dev/run7.err.txt b/.tmp-schema-align/run-dev/run7.err.txt deleted file mode 100644 index e69de29..0000000 diff --git a/.tmp-schema-align/run-dev/run7.out.txt b/.tmp-schema-align/run-dev/run7.out.txt deleted file mode 100644 index 63a8b01..0000000 --- a/.tmp-schema-align/run-dev/run7.out.txt +++ /dev/null @@ -1,10 +0,0 @@ -2026-09-19T18:33:53.362764Z INFO pms_api: 配置加载完成 -2026-09-19T18:33:53.441528Z INFO pms_api::db: MySQL 连接池已建立 -2026-09-19T18:33:53.442730Z INFO pms_api: 数据库连接成功 -2026-09-19T18:33:55.390841Z INFO pms_api::db::migrate: 数据库索引创建完成 -2026-09-19T18:33:56.255598Z INFO pms_api::db::migrate: 数据库列级迁移完成 -2026-09-19T18:33:57.850061Z INFO pms_api::db::migrate: 历史库列定义对齐检查完成:35 列定义 + 3 处注释口径 + organizations.parent_id 索引/外键全部已一致,未执行任何 DDL -2026-09-19T18:33:59.119151Z INFO pms_api::db::migrate: 默认权限码初始化完成 -2026-09-19T18:34:03.083549Z INFO pms_api::db::migrate: 默认角色及权限分配完成 -2026-09-19T18:34:03.086569Z INFO pms_api::db::migrate: 数据库迁移完成 -2026-09-19T18:34:03.087097Z INFO pms_api: 数据库迁移完成 diff --git a/.tmp-schema-align/run-prod/prod-run1.err.txt b/.tmp-schema-align/run-prod/prod-run1.err.txt deleted file mode 100644 index e69de29..0000000 diff --git a/.tmp-schema-align/run-prod/prod-run1.out.txt b/.tmp-schema-align/run-prod/prod-run1.out.txt deleted file mode 100644 index 5ae1ea9..0000000 --- a/.tmp-schema-align/run-prod/prod-run1.out.txt +++ /dev/null @@ -1,10 +0,0 @@ -2026-09-19T18:13:54.477418Z INFO pms_api: 配置加载完成 -2026-09-19T18:13:54.695258Z INFO pms_api::db: MySQL 连接池已建立 -2026-09-19T18:13:54.696431Z INFO pms_api: 数据库连接成功 -2026-09-19T18:13:56.697791Z INFO pms_api::db::migrate: 数据库索引创建完成 -2026-09-19T18:13:57.575693Z INFO pms_api::db::migrate: 数据库列级迁移完成 -2026-09-19T18:13:57.915935Z INFO pms_api::db::migrate: 历史库列定义对齐检查完成:全部已一致,未执行任何 DDL -2026-09-19T18:13:59.135681Z INFO pms_api::db::migrate: 默认权限码初始化完成 -2026-09-19T18:14:03.089331Z INFO pms_api::db::migrate: 默认角色及权限分配完成 -2026-09-19T18:14:03.090600Z INFO pms_api::db::migrate: 数据库迁移完成 -2026-09-19T18:14:03.091667Z INFO pms_api: 数据库迁移完成 diff --git a/.tmp-schema-align/run-prod/prod-run2.err.txt b/.tmp-schema-align/run-prod/prod-run2.err.txt deleted file mode 100644 index e69de29..0000000 diff --git a/.tmp-schema-align/run-prod/prod-run2.out.txt b/.tmp-schema-align/run-prod/prod-run2.out.txt deleted file mode 100644 index 6c76497..0000000 --- a/.tmp-schema-align/run-prod/prod-run2.out.txt +++ /dev/null @@ -1,10 +0,0 @@ -2026-09-19T18:14:09.494692Z INFO pms_api: 配置加载完成 -2026-09-19T18:14:09.585364Z INFO pms_api::db: MySQL 连接池已建立 -2026-09-19T18:14:09.586400Z INFO pms_api: 数据库连接成功 -2026-09-19T18:14:11.533080Z INFO pms_api::db::migrate: 数据库索引创建完成 -2026-09-19T18:14:12.387751Z INFO pms_api::db::migrate: 数据库列级迁移完成 -2026-09-19T18:14:12.741994Z INFO pms_api::db::migrate: 历史库列定义对齐检查完成:全部已一致,未执行任何 DDL -2026-09-19T18:14:13.948453Z INFO pms_api::db::migrate: 默认权限码初始化完成 -2026-09-19T18:14:17.936639Z INFO pms_api::db::migrate: 默认角色及权限分配完成 -2026-09-19T18:14:17.937745Z INFO pms_api::db::migrate: 数据库迁移完成 -2026-09-19T18:14:17.938530Z INFO pms_api: 数据库迁移完成 diff --git a/.tmp-schema-align/run-prod/prod-run3.err.txt b/.tmp-schema-align/run-prod/prod-run3.err.txt deleted file mode 100644 index e69de29..0000000 diff --git a/.tmp-schema-align/run-prod/prod-run3.out.txt b/.tmp-schema-align/run-prod/prod-run3.out.txt deleted file mode 100644 index 3e478ac..0000000 --- a/.tmp-schema-align/run-prod/prod-run3.out.txt +++ /dev/null @@ -1,28 +0,0 @@ -2026-09-19T18:25:32.185057Z INFO pms_api: 配置加载完成 -2026-09-19T18:25:32.263332Z INFO pms_api::db: MySQL 连接池已建立 -2026-09-19T18:25:32.264523Z INFO pms_api: 数据库连接成功 -2026-09-19T18:25:34.251156Z INFO pms_api::db::migrate: 数据库索引创建完成 -2026-09-19T18:25:35.094486Z INFO pms_api::db::migrate: 数据库列级迁移完成 -2026-09-19T18:25:35.439717Z INFO pms_api::db::migrate: [migrate] 列定义对齐 batteries.created_at:type timestamp -> DATETIME, nullable NO -> NO, default 'CURRENT_TIMESTAMP' -> 'CURRENT_TIMESTAMP', on_update 无 -> 不需要, 注释已一致/未校验;DDL=ALTER TABLE batteries MODIFY COLUMN created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP -2026-09-19T18:25:35.551972Z INFO pms_api::db::migrate: [migrate] 列定义对齐 charge_orders.created_at:type timestamp -> DATETIME, nullable NO -> NO, default 'CURRENT_TIMESTAMP' -> 'CURRENT_TIMESTAMP', on_update 无 -> 不需要, 注释已一致/未校验;DDL=ALTER TABLE charge_orders MODIFY COLUMN created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP -2026-09-19T18:25:36.290773Z INFO pms_api::db::migrate: [migrate] 列定义对齐 user_feedback.created_at:type timestamp -> DATETIME, nullable NO -> NO, default 'CURRENT_TIMESTAMP' -> 'CURRENT_TIMESTAMP', on_update 无 -> 不需要, 注释已一致/未校验;DDL=ALTER TABLE user_feedback MODIFY COLUMN created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP -2026-09-19T18:25:36.429568Z INFO pms_api::db::migrate: [migrate] 列定义对齐 organizations.updated_at:type datetime -> DATETIME, nullable NO -> NO, default 'CURRENT_TIMESTAMP' -> 'CURRENT_TIMESTAMP', on_update 无 -> 需要, 注释已一致/未校验;DDL=ALTER TABLE organizations MODIFY COLUMN updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP -2026-09-19T18:25:36.525030Z INFO pms_api::db::migrate: [migrate] 列定义对齐 projects.updated_at:type datetime -> DATETIME, nullable NO -> NO, default 'CURRENT_TIMESTAMP' -> 'CURRENT_TIMESTAMP', on_update 无 -> 需要, 注释已一致/未校验;DDL=ALTER TABLE projects MODIFY COLUMN updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP -2026-09-19T18:25:36.628013Z INFO pms_api::db::migrate: [migrate] 列定义对齐 cabinets.updated_at:type datetime -> DATETIME, nullable NO -> NO, default 'CURRENT_TIMESTAMP' -> 'CURRENT_TIMESTAMP', on_update 无 -> 需要, 注释已一致/未校验;DDL=ALTER TABLE cabinets MODIFY COLUMN updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP -2026-09-19T18:25:36.728496Z INFO pms_api::db::migrate: [migrate] 列定义对齐 cabin_boards.updated_at:type datetime -> DATETIME, nullable NO -> NO, default 'CURRENT_TIMESTAMP' -> 'CURRENT_TIMESTAMP', on_update 无 -> 需要, 注释已一致/未校验;DDL=ALTER TABLE cabin_boards MODIFY COLUMN updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP -2026-09-19T18:25:36.828271Z INFO pms_api::db::migrate: [migrate] 列定义对齐 compartments.updated_at:type datetime -> DATETIME, nullable NO -> NO, default 'CURRENT_TIMESTAMP' -> 'CURRENT_TIMESTAMP', on_update 无 -> 需要, 注释已一致/未校验;DDL=ALTER TABLE compartments MODIFY COLUMN updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP -2026-09-19T18:25:36.929144Z INFO pms_api::db::migrate: [migrate] 列定义对齐 batteries.updated_at:type timestamp -> DATETIME, nullable NO -> NO, default 'CURRENT_TIMESTAMP' -> 'CURRENT_TIMESTAMP', on_update 有 -> 需要, 注释已一致/未校验;DDL=ALTER TABLE batteries MODIFY COLUMN updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP -2026-09-19T18:25:37.038989Z INFO pms_api::db::migrate: [migrate] 列定义对齐 users.updated_at:type datetime -> DATETIME, nullable NO -> NO, default 'CURRENT_TIMESTAMP' -> 'CURRENT_TIMESTAMP', on_update 无 -> 需要, 注释已一致/未校验;DDL=ALTER TABLE users MODIFY COLUMN updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP -2026-09-19T18:25:37.134567Z INFO pms_api::db::migrate: [migrate] 列定义对齐 roles.updated_at:type datetime -> DATETIME, nullable NO -> NO, default 'CURRENT_TIMESTAMP' -> 'CURRENT_TIMESTAMP', on_update 无 -> 需要, 注释已一致/未校验;DDL=ALTER TABLE roles MODIFY COLUMN updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP -2026-09-19T18:25:37.237019Z INFO pms_api::db::migrate: [migrate] 列定义对齐 charge_records.start_time:type datetime -> DATETIME, nullable NO -> NO, default 'CURRENT_TIMESTAMP' -> 'CURRENT_TIMESTAMP', on_update 无 -> 不需要, 注释已按目标口径同步;DDL=ALTER TABLE charge_records MODIFY COLUMN start_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '充电开始时间' -2026-09-19T18:25:37.336864Z INFO pms_api::db::migrate: [migrate] 列定义对齐 charge_records.end_time:type datetime -> DATETIME, nullable NO -> NO, default 'CURRENT_TIMESTAMP' -> 'CURRENT_TIMESTAMP', on_update 无 -> 不需要, 注释已按目标口径同步;DDL=ALTER TABLE charge_records MODIFY COLUMN end_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '充电结束时间' -2026-09-19T18:25:37.455448Z INFO pms_api::db::migrate: [migrate] 列定义对齐 charge_orders.start_time:type timestamp -> DATETIME, nullable YES -> YES, default '' -> '', on_update 无 -> 不需要, 注释已一致/未校验;DDL=ALTER TABLE charge_orders MODIFY COLUMN start_time DATETIME NULL COMMENT '充电开始时间' -2026-09-19T18:25:37.558525Z INFO pms_api::db::migrate: [migrate] 列定义对齐 charge_orders.end_time:type timestamp -> DATETIME, nullable YES -> YES, default '' -> '', on_update 无 -> 不需要, 注释已一致/未校验;DDL=ALTER TABLE charge_orders MODIFY COLUMN end_time DATETIME NULL COMMENT '充电结束时间' -2026-09-19T18:25:37.670554Z INFO pms_api::db::migrate: [migrate] 列定义对齐 download_tasks.completed_at:type datetime -> DATETIME, nullable NO -> NO, default 'CURRENT_TIMESTAMP' -> 'CURRENT_TIMESTAMP', on_update 无 -> 不需要, 注释已按目标口径同步;DDL=ALTER TABLE download_tasks MODIFY COLUMN completed_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '完成时间' -2026-09-19T18:25:37.828849Z INFO pms_api::db::migrate: [migrate] 列定义对齐 sim_change_logs.changed_at:type timestamp -> DATETIME, nullable NO -> NO, default 'CURRENT_TIMESTAMP' -> 'CURRENT_TIMESTAMP', on_update 无 -> 不需要, 注释已一致/未校验;DDL=ALTER TABLE sim_change_logs MODIFY COLUMN changed_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '变更时间' -2026-09-19T18:25:38.034391Z INFO pms_api::db::migrate: [migrate] 列定义对齐 organizations.parent_id:type bigint -> BIGINT, nullable YES -> YES, default '' -> '', on_update 无 -> 不需要, 注释已按目标口径同步;DDL=ALTER TABLE organizations MODIFY COLUMN parent_id BIGINT NULL COMMENT '父组织ID(NULL=顶级组织)' -2026-09-19T18:25:38.143205Z INFO pms_api::db::migrate: 历史库列定义对齐完成:本次执行了 18 项 DDL -2026-09-19T18:25:39.372853Z INFO pms_api::db::migrate: 默认权限码初始化完成 -2026-09-19T18:25:43.404613Z INFO pms_api::db::migrate: 默认角色及权限分配完成 -2026-09-19T18:25:43.405253Z INFO pms_api::db::migrate: 数据库迁移完成 -2026-09-19T18:25:43.405741Z INFO pms_api: 数据库迁移完成 diff --git a/.tmp-schema-align/run-prod/prod-run4.err.txt b/.tmp-schema-align/run-prod/prod-run4.err.txt deleted file mode 100644 index e69de29..0000000 diff --git a/.tmp-schema-align/run-prod/prod-run4.out.txt b/.tmp-schema-align/run-prod/prod-run4.out.txt deleted file mode 100644 index 5078811..0000000 --- a/.tmp-schema-align/run-prod/prod-run4.out.txt +++ /dev/null @@ -1,10 +0,0 @@ -2026-09-19T18:25:51.345960Z INFO pms_api: 配置加载完成 -2026-09-19T18:25:51.426036Z INFO pms_api::db: MySQL 连接池已建立 -2026-09-19T18:25:51.427165Z INFO pms_api: 数据库连接成功 -2026-09-19T18:25:53.370017Z INFO pms_api::db::migrate: 数据库索引创建完成 -2026-09-19T18:25:54.258199Z INFO pms_api::db::migrate: 数据库列级迁移完成 -2026-09-19T18:25:55.716955Z INFO pms_api::db::migrate: 历史库列定义对齐检查完成:35 列 + organizations.parent_id 索引/外键全部已一致,未执行任何 DDL -2026-09-19T18:25:57.017755Z INFO pms_api::db::migrate: 默认权限码初始化完成 -2026-09-19T18:26:01.059264Z INFO pms_api::db::migrate: 默认角色及权限分配完成 -2026-09-19T18:26:01.060052Z INFO pms_api::db::migrate: 数据库迁移完成 -2026-09-19T18:26:01.060501Z INFO pms_api: 数据库迁移完成 diff --git a/.tmp-schema-align/run-prod/prod-run5.err.txt b/.tmp-schema-align/run-prod/prod-run5.err.txt deleted file mode 100644 index e69de29..0000000 diff --git a/.tmp-schema-align/run-prod/prod-run5.out.txt b/.tmp-schema-align/run-prod/prod-run5.out.txt deleted file mode 100644 index fd4f634..0000000 --- a/.tmp-schema-align/run-prod/prod-run5.out.txt +++ /dev/null @@ -1,13 +0,0 @@ -2026-09-19T18:34:10.372380Z INFO pms_api: 配置加载完成 -2026-09-19T18:34:10.446783Z INFO pms_api::db: MySQL 连接池已建立 -2026-09-19T18:34:10.447951Z INFO pms_api: 数据库连接成功 -2026-09-19T18:34:12.409960Z INFO pms_api::db::migrate: 数据库索引创建完成 -2026-09-19T18:34:13.282992Z INFO pms_api::db::migrate: 数据库列级迁移完成 -2026-09-19T18:34:14.912031Z INFO pms_api::db::migrate: [migrate] 更新列注释 batteries.brand(原注释 '品牌' 不含 '由 brand_code 映射出的名称');DDL=ALTER TABLE batteries MODIFY COLUMN brand VARCHAR(50) NULL COMMENT '品牌(由 brand_code 映射出的名称,代码表未定前为空)' -2026-09-19T18:34:15.003112Z INFO pms_api::db::migrate: [migrate] 更新列注释 batteries.model(原注释 '型号' 不含 '由 model_code 映射出的名称');DDL=ALTER TABLE batteries MODIFY COLUMN model VARCHAR(50) NULL COMMENT '型号(由 model_code 映射出的名称,代码表未定前为空)' -2026-09-19T18:34:15.115724Z INFO pms_api::db::migrate: [migrate] 更新列注释 cabinets.address(原注释 '安装地址' 不含 '详细地址');DDL=ALTER TABLE cabinets MODIFY COLUMN address VARCHAR(200) NULL COMMENT '详细地址' -2026-09-19T18:34:15.117373Z INFO pms_api::db::migrate: 历史库列定义对齐完成:本次执行了 3 项 DDL -2026-09-19T18:34:16.337833Z INFO pms_api::db::migrate: 默认权限码初始化完成 -2026-09-19T18:34:20.457652Z INFO pms_api::db::migrate: 默认角色及权限分配完成 -2026-09-19T18:34:20.459891Z INFO pms_api::db::migrate: 数据库迁移完成 -2026-09-19T18:34:20.461680Z INFO pms_api: 数据库迁移完成 diff --git a/.tmp-schema-align/run-prod/prod-run6.err.txt b/.tmp-schema-align/run-prod/prod-run6.err.txt deleted file mode 100644 index e69de29..0000000 diff --git a/.tmp-schema-align/run-prod/prod-run6.out.txt b/.tmp-schema-align/run-prod/prod-run6.out.txt deleted file mode 100644 index 78f81a0..0000000 --- a/.tmp-schema-align/run-prod/prod-run6.out.txt +++ /dev/null @@ -1,10 +0,0 @@ -2026-09-19T18:34:36.429826Z INFO pms_api: 配置加载完成 -2026-09-19T18:34:36.504806Z INFO pms_api::db: MySQL 连接池已建立 -2026-09-19T18:34:36.506064Z INFO pms_api: 数据库连接成功 -2026-09-19T18:34:38.598065Z INFO pms_api::db::migrate: 数据库索引创建完成 -2026-09-19T18:34:39.470938Z INFO pms_api::db::migrate: 数据库列级迁移完成 -2026-09-19T18:34:41.031375Z INFO pms_api::db::migrate: 历史库列定义对齐检查完成:35 列定义 + 3 处注释口径 + organizations.parent_id 索引/外键全部已一致,未执行任何 DDL -2026-09-19T18:34:42.296200Z INFO pms_api::db::migrate: 默认权限码初始化完成 -2026-09-19T18:34:46.637549Z INFO pms_api::db::migrate: 默认角色及权限分配完成 -2026-09-19T18:34:46.641089Z INFO pms_api::db::migrate: 数据库迁移完成 -2026-09-19T18:34:46.642778Z INFO pms_api: 数据库迁移完成 diff --git a/.tmp-scope-check/ddl_before.json b/.tmp-scope-check/ddl_before.json deleted file mode 100644 index 72900ef..0000000 --- a/.tmp-scope-check/ddl_before.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "charge_orders": "CREATE TABLE `charge_orders` (\n `id` bigint NOT NULL AUTO_INCREMENT,\n `compartment_id` bigint NOT NULL COMMENT '仓体ID',\n `cabinet_id` bigint NOT NULL COMMENT '柜子ID',\n `battery_id` varchar(50) NOT NULL COMMENT '电池ID',\n `start_time` timestamp NULL DEFAULT NULL COMMENT '充电开始时间',\n `end_time` timestamp NULL DEFAULT NULL COMMENT '充电结束时间',\n `start_soc` int DEFAULT NULL COMMENT '起始电量(‰)',\n `end_soc` int DEFAULT NULL COMMENT '结束电量(‰)',\n `energy` int DEFAULT '0' COMMENT '充电量(Wh)',\n `reason` varchar(20) DEFAULT NULL COMMENT '结束原因: normal/user_stop/forced/timeout',\n `status` tinyint DEFAULT '0' COMMENT '0充电中 1已完成 2异常',\n `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,\n `active_flag` bigint GENERATED ALWAYS AS ((case when (`status` = 0) then `compartment_id` else NULL end)) STORED COMMENT '活跃订单唯一性哨兵:status=0 时=compartment_id,否则 NULL(NULL 不参与唯一性)',\n PRIMARY KEY (`id`),\n UNIQUE KEY `uk_charge_orders_active` (`active_flag`),\n KEY `cabinet_id` (`cabinet_id`),\n KEY `idx_battery` (`battery_id`),\n KEY `idx_compartment` (`compartment_id`),\n KEY `idx_charge_orders_status_start` (`status`,`start_time`),\n CONSTRAINT `charge_orders_ibfk_1` FOREIGN KEY (`compartment_id`) REFERENCES `compartments` (`id`),\n CONSTRAINT `charge_orders_ibfk_2` FOREIGN KEY (`cabinet_id`) REFERENCES `cabinets` (`id`)\n) ENGINE=InnoDB AUTO_INCREMENT=25 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci", - "charge_records": "CREATE TABLE `charge_records` (\n `id` bigint NOT NULL AUTO_INCREMENT,\n `compartment_id` bigint NOT NULL,\n `cabinet_id` bigint NOT NULL,\n `start_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,\n `end_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,\n `energy` bigint NOT NULL DEFAULT '0' COMMENT '充电量(Wh)',\n `max_temperature` bigint NOT NULL DEFAULT '0' COMMENT '最高温度(K×100)',\n `status` tinyint DEFAULT '0' COMMENT '0正常 1异常 2手动停止',\n `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,\n PRIMARY KEY (`id`),\n KEY `compartment_id` (`compartment_id`),\n KEY `idx_charge_records_cabinet` (`cabinet_id`),\n KEY `idx_charge_records_cabinet_start_time` (`cabinet_id`,`start_time`),\n KEY `idx_charge_records_start_time` (`start_time`),\n CONSTRAINT `charge_records_ibfk_1` FOREIGN KEY (`compartment_id`) REFERENCES `compartments` (`id`),\n CONSTRAINT `charge_records_ibfk_2` FOREIGN KEY (`cabinet_id`) REFERENCES `cabinets` (`id`)\n) ENGINE=InnoDB AUTO_INCREMENT=32 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci", - "device_logs": "CREATE TABLE `device_logs` (\n `id` bigint NOT NULL AUTO_INCREMENT,\n `cabinet_id` bigint NOT NULL,\n `log_type` tinyint NOT NULL COMMENT '1状态上报 2故障 3告警 4停电',\n `content` text COMMENT '日志内容JSON',\n `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,\n PRIMARY KEY (`id`),\n KEY `idx_device_logs_cabinet` (`cabinet_id`),\n KEY `idx_device_logs_created` (`created_at`),\n CONSTRAINT `device_logs_ibfk_1` FOREIGN KEY (`cabinet_id`) REFERENCES `cabinets` (`id`)\n) ENGINE=InnoDB AUTO_INCREMENT=101 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci" -} \ No newline at end of file diff --git a/.tmp-scope-check/scope_isolation_check.py b/.tmp-scope-check/scope_isolation_check.py deleted file mode 100644 index ed392f6..0000000 --- a/.tmp-scope-check/scope_isolation_check.py +++ /dev/null @@ -1,402 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -user_scopes(数据范围)隔离实测 — 在测试库 pms_dev + 测试环境 API(:3000) 上跑 - -覆盖验收 1-6: - 1. 造两个项目(同组织)+ 两个用户(同组织、role_level=0)+ 一个总管理员 - 2. 未配置 scope 的用户 → 能看到两个项目的设备(= 现状,证明向后兼容) - 3. 配置 scope 只允许项目 A 的用户 → 只能看到项目 A 的设备(列表 + 详情都测) - 4. 总管理员 → 看到全部(不受 scope 影响) - 5. 配置一个不属于该组织的项目 id / 组织 id / 不存在的 id → 被 BadRequest 拒绝 - 6. 清理临时数据(脚本 finally 里无条件执行) - -用法: - set PMS_TEST_JWT_SECRET=<测试环境 .env 里的 JWT_SECRET> - python scope_isolation_check.py -""" -import os -import sys -import time -import json -import random -import urllib.request -import urllib.error - -import pymysql -import jwt # PyJWT - -DB = dict(host="10.8.0.252", user="root", password="Hbhyg731024@", database="pms_dev") -API = os.environ.get("PMS_TEST_API_BASE", "http://10.8.0.252:3000/api") -SECRET = os.environ.get("PMS_TEST_JWT_SECRET", "") - -TAG = "SCOPETEST" -SUFFIX = f"{int(time.time()) % 100000:05d}" -PASS, FAIL = [], [] - - -def ok(name, cond, detail=""): - (PASS if cond else FAIL).append(name) - print(f" [{'PASS' if cond else 'FAIL'}] {name} {detail}") - - -def token(uid, phone): - now = int(time.time()) - return jwt.encode({"sub": uid, "phone": phone, "exp": now + 3600, "iat": now}, - SECRET, algorithm="HS256") - - -def call(method, path, tok=None, body=None): - """返回 (status, json_or_text)""" - req = urllib.request.Request(API + path, method=method) - req.add_header("Content-Type", "application/json") - if tok: - req.add_header("Authorization", "Bearer " + tok) - data = json.dumps(body).encode() if body is not None else None - try: - with urllib.request.urlopen(req, data, timeout=20) as r: - raw = r.read().decode() - return r.status, (json.loads(raw) if raw else None) - except urllib.error.HTTPError as e: - raw = e.read().decode() - try: - return e.code, json.loads(raw) - except Exception: - return e.code, raw - - -def main(): - if not SECRET: - print("缺少 PMS_TEST_JWT_SECRET 环境变量(取 /data/pms_dev/api-server/.env 的 JWT_SECRET)") - sys.exit(2) - - conn = pymysql.connect(**DB, autocommit=True) - cur = conn.cursor() - ids = {} - try: - # ---------------- 0. 预清理(防上次异常退出残留) ---------------- - cleanup(cur) - - # ---------------- 1. 造夹具 ---------------- - print("\n=== [1] 造夹具 ===") - cur.execute("INSERT INTO organizations (name) VALUES (%s)", (f"{TAG}-ORG-{SUFFIX}",)) - org = cur.lastrowid - cur.execute("INSERT INTO organizations (name) VALUES (%s)", (f"{TAG}-ORG2-{SUFFIX}",)) - org2 = cur.lastrowid - cur.execute("INSERT INTO projects (organization_id, name) VALUES (%s,%s)", (org, f"{TAG}-A-{SUFFIX}")) - pa = cur.lastrowid - cur.execute("INSERT INTO projects (organization_id, name) VALUES (%s,%s)", (org, f"{TAG}-B-{SUFFIX}")) - pb = cur.lastrowid - cur.execute("INSERT INTO projects (organization_id, name) VALUES (%s,%s)", (org2, f"{TAG}-C-{SUFFIX}")) - pc = cur.lastrowid - - def add_cabinet(pid, seed): - imei = f"86{seed}{random.randint(1000000, 9999999)}"[:15] - # abstract_id 必须给值:h5 设备列表行结构体是 String(DB 可空), - # 留 NULL 会让该接口 500(历史缺陷,非本次改动引入) - cur.execute("INSERT INTO cabinets (project_id, imei, name, status, auth_str, abstract_id) " - "VALUES (%s,%s,%s,1,%s,%s)", - (pid, imei, f"{TAG}-CAB-{seed}", "AbCd1234", f"10-{SUFFIX}{seed}")) - cid = cur.lastrowid - cur.execute("INSERT INTO cabin_boards (cabinet_id, board_id) VALUES (%s,1)", (cid,)) - bid = cur.lastrowid - comp = None - for ch in range(1, 7): - cur.execute("INSERT INTO compartments (cabin_board_id, channel_id) VALUES (%s,%s)", (bid, ch)) - if ch == 1: - comp = cur.lastrowid - # 每个柜子造 1 条充电订单 + 1 条设备日志 + 1 条充电记录(用于验证列表接口的范围收窄) - cur.execute("INSERT INTO charge_orders (compartment_id, cabinet_id, battery_id, start_soc, energy, status) " - "VALUES (%s,%s,%s,50,100,1)", (comp, cid, f"{TAG}-BAT-{seed}")) - order_id = cur.lastrowid - cur.execute("INSERT INTO device_logs (cabinet_id, log_type, content) VALUES (%s,1,%s)", - (cid, f"{TAG}-LOG-{seed}")) - cur.execute("INSERT INTO charge_records (compartment_id, cabinet_id, energy, status) VALUES (%s,%s,10,0)", - (comp, cid)) - return cid, imei, order_id - - cab_a, imei_a, order_a = add_cabinet(pa, "1") - cab_b, imei_b, order_b = add_cabinet(pb, "2") - cab_c, imei_c, order_c = add_cabinet(pc, "3") - - def add_user(role, organization_id, label): - phone = f"19{random.randint(100000000, 999999999)}" - cur.execute("INSERT INTO users (phone, name, password, role, organization_id, status) " - "VALUES (%s,%s,%s,%s,%s,1)", - (phone, f"{TAG}-{label}-{SUFFIX}", "not-a-real-hash", role, organization_id)) - return cur.lastrowid, phone - - u_open, ph_open = add_user(0, org, "OPEN") # 未配置 scope - u_a, ph_a = add_user(0, org, "SCOPEA") # 配 scope=项目A - u_admin, ph_admin = add_user(2, None, "ADMIN") # 总管理员 - - ids = dict(org=org, org2=org2, pa=pa, pb=pb, pc=pc, - cab_a=cab_a, cab_b=cab_b, cab_c=cab_c, - order_a=order_a, order_b=order_b, order_c=order_c, - u_open=u_open, u_a=u_a, u_admin=u_admin) - print(f" org={org} org2={org2} projA={pa} projB={pb} projC(org2)={pc}") - print(f" cabA={cab_a}/{imei_a} cabB={cab_b}/{imei_b} cabC={cab_c}/{imei_c}") - print(f" user_open={u_open} user_scopeA={u_a} admin={u_admin}") - - t_open = token(u_open, ph_open) - t_a = token(u_a, ph_a) - t_admin = token(u_admin, ph_admin) - - # ---------------- 2. 未配置 scope = 不限制(向后兼容) ---------------- - print("\n=== [2] 未配置 scope 的用户 → 不限制(两个项目都可见)===") - st, body = call("GET", "/cabinets/filter?page_size=100", t_open) - got = [c["id"] for c in body["data"]["data"]] if st == 200 else [] - print(f" GET /cabinets/filter -> {st} total={body['data']['total'] if st == 200 else body}") - ok("未配置 scope 可见项目A设备", cab_a in got) - ok("未配置 scope 可见项目B设备", cab_b in got) - ok("未配置 scope 不可见跨组织(项目C)设备", cab_c not in got) - - # ---------------- 5. 非法 scope_id 必须 400 ---------------- - print("\n=== [5] 非法 scope_id(跨组织/不存在)→ BadRequest ===") - st, body = call("PUT", f"/users/{u_a}/scopes", t_admin, - {"scopes": [{"scope_type": 2, "scope_id": pc, "permission_type": 1}]}) - print(f" PUT scopes(项目C, 属组织2) -> {st} {body}") - ok("跨组织项目被拒 400", st == 400 and "不属于该用户所属组织" in str(body)) - - st, body = call("PUT", f"/users/{u_a}/scopes", t_admin, - {"scopes": [{"scope_type": 2, "scope_id": 999999999, "permission_type": 1}]}) - print(f" PUT scopes(不存在项目) -> {st} {body}") - ok("不存在项目被拒 400", st == 400 and "不存在" in str(body)) - - st, body = call("PUT", f"/users/{u_a}/scopes", t_admin, - {"scopes": [{"scope_type": 1, "scope_id": org2, "permission_type": 1}]}) - print(f" PUT scopes(组织级=别人的组织) -> {st} {body}") - ok("组织级跨组织被拒 400", st == 400) - - st, body = call("PUT", f"/users/{u_a}/scopes", t_admin, - {"scopes": [{"scope_type": 2, "scope_id": pa, "permission_type": 9}]}) - print(f" PUT scopes(permission_type=9) -> {st} {body}") - ok("非法 permission_type 被拒 400", st == 400) - - st, body = call("PUT", "/users/999999999/scopes", t_admin, - {"scopes": [{"scope_type": 2, "scope_id": pa, "permission_type": 1}]}) - print(f" PUT scopes(用户不存在) -> {st} {body}") - ok("目标用户不存在 -> 404", st == 404) - - # ---------------- 3. 配置 scope=项目A → 收窄 ---------------- - print("\n=== [3] 配置 scope 只允许项目 A → 只能看到项目 A ===") - st, body = call("PUT", f"/users/{u_a}/scopes", t_admin, - {"scopes": [{"scope_type": 2, "scope_id": pa, "permission_type": 1}]}) - print(f" PUT /users/{u_a}/scopes {{项目A}} -> {st} {body}") - ok("配置项目范围成功", st == 200) - - st, body = call("GET", "/cabinets/filter?page_size=100", t_a) - got = [c["id"] for c in body["data"]["data"]] if st == 200 else [] - print(f" GET /cabinets/filter -> {st} total={body['data']['total'] if st == 200 else body}") - print(f" data ids = {got}") - ok("列表:项目A设备可见", cab_a in got) - ok("列表:项目B设备不可见", cab_b not in got) - ok("列表:跨组织设备不可见", cab_c not in got) - - st, body = call("GET", f"/cabinets/{cab_a}", t_a) - print(f" GET /cabinets/{cab_a}(范围内详情) -> {st} data.id={body['data']['id'] if st == 200 else body}") - ok("详情:范围内设备 200", st == 200 and body["data"]["id"] == cab_a) - - st, body = call("GET", f"/cabinets/{cab_b}", t_a) - print(f" GET /cabinets/{cab_b}(范围外详情) -> {st} {body}") - ok("详情:范围外设备被拒 403", st == 403) - - st, body = call("GET", f"/cabinets/{cab_c}", t_a) - print(f" GET /cabinets/{cab_c}(跨组织详情) -> {st} {body}") - ok("详情:跨组织设备被拒 403", st == 403) - - st, body = call("GET", f"/projects/{pb}/cabinets", t_a) - print(f" GET /projects/{pb}/cabinets(范围外项目的设备列表) -> {st} {body}") - ok("范围外项目的设备列表被拒 403", st == 403) - - st, body = call("GET", f"/projects/{pa}/cabinets", t_a) - print(f" GET /projects/{pa}/cabinets(范围内项目) -> {st} rows={len(body['data']) if st == 200 else body}") - ok("范围内项目设备列表 200", st == 200 and len(body["data"]) == 1) - - st, body = call("GET", "/cabinets/realtime-status?imei=" + imei_b, t_a) - print(f" GET /cabinets/realtime-status?imei=(项目B) -> {st} {body}") - ok("范围外设备实时状态被拒 403", st == 403) - - # 订单/记录/日志/能耗 列表接口(叠加了范围条件)—— 必须只出现项目 A 的数据 - orders_filter_failed = False - st_filter_failed = False - st, body = call("GET", "/charge-orders?page_size=50", t_a) - order_ids = [o["id"] for o in body["data"]["list"]] if st == 200 else [] - print(f" GET /charge-orders -> {st} total={body['data']['total'] if st == 200 else body} ids={order_ids}") - if st != 200: - # 诊断:换成「总管理员」再打一次。若同样 500 ⇒ 与数据范围无关(环境/结构问题) - st_admin, body_admin = call("GET", "/charge-orders?page_size=50", t_admin) - print(f" [诊断] 总管理员 GET /charge-orders -> {st_admin} {body_admin}") - ok("订单 500 与数据范围无关(总管理员同样失败)", st_admin == 500) - orders_filter_failed = True - else: - ok("订单列表可访问", st == 200) - ok("订单列表:项目A订单可见", order_a in order_ids) - ok("订单列表:项目B订单不可见", order_b not in order_ids) - ok("订单列表:跨组织订单不可见", order_c not in order_ids) - - st, body = call("GET", "/charge-records?page_size=50", t_a) - rec_cabs = [r["cabinet_id"] for r in body["data"]["list"]] if st == 200 else [] - print(f" GET /charge-records -> {st} total={body['data']['total'] if st == 200 else body} cabinets={rec_cabs}") - ok("充电记录列表可访问", st == 200) - ok("充电记录:仅项目A柜子", cab_a in rec_cabs and cab_b not in rec_cabs and cab_c not in rec_cabs) - - st, body = call("GET", "/device-logs?page_size=50", t_a) - log_cabs = [r["cabinet_id"] for r in body["data"]["list"]] if st == 200 else [] - print(f" GET /device-logs -> {st} total={body['data']['total'] if st == 200 else body} cabinets={log_cabs}") - if st != 200: - st_admin, body_admin = call("GET", "/device-logs?page_size=50", t_admin) - print(f" [诊断] 总管理员 GET /device-logs -> {st_admin} {body_admin}") - ok("设备日志 500 与数据范围无关(总管理员同样失败)", st_admin == 500) - st_filter_failed = True - else: - ok("设备日志列表可访问", st == 200) - ok("设备日志:仅项目A柜子", cab_a in log_cabs and cab_b not in log_cabs and cab_c not in log_cabs) - - # —— 端点因历史 TIMESTAMP/DATETIME 漂移 500 时,直接用「实现生成的同一条 SQL」在库里验证收窄 —— - if st_filter_failed or orders_filter_failed: - print("\n [SQL 级验证] 用实现生成的同一条 WHERE 条件直接查库:") - sql_orders = ( - "SELECT co.id FROM charge_orders co LEFT JOIN cabinets cab ON co.cabinet_id = cab.id WHERE 1=1" - " AND co.cabinet_id IN (SELECT c.id FROM cabinets c JOIN projects p ON c.project_id = p.id" - " WHERE p.organization_id = %s)" - " AND co.cabinet_id IN (SELECT c.id FROM cabinets c WHERE c.project_id IN (%s))" - ) - cur.execute(sql_orders, (org, pa)) - got_orders = [r[0] for r in cur.fetchall()] - print(f" 订单 SQL(组织={org} ∧ 项目={pa}) -> {got_orders}") - ok("SQL 级:订单只命中项目A", order_a in got_orders and order_b not in got_orders and order_c not in got_orders) - - sql_logs = ( - "SELECT dl.cabinet_id FROM device_logs dl WHERE 1=1" - " AND dl.cabinet_id IN (SELECT c.id FROM cabinets c JOIN projects p ON c.project_id = p.id" - " WHERE p.organization_id = %s)" - " AND dl.cabinet_id IN (SELECT c.id FROM cabinets c WHERE c.project_id IN (%s))" - ) - cur.execute(sql_logs, (org, pa)) - got_logs = [r[0] for r in cur.fetchall()] - print(f" 设备日志 SQL(组织={org} ∧ 项目={pa}) -> {got_logs}") - ok("SQL 级:设备日志只命中项目A柜子", cab_a in got_logs and cab_b not in got_logs and cab_c not in got_logs) - - for path in ("/energy-stats/summary", "/energy-stats/projects", "/energy-stats/cabinets", "/energy-stats/trend"): - st, body = call("GET", path, t_a) - print(f" GET {path} -> {st} {json.dumps(body, ensure_ascii=False)[:160] if st == 200 else body}") - ok(f"范围内用户可访问 {path}", st == 200) - - st, body = call("GET", "/energy-stats/cabinets", t_a) - rank_ids = [r["cabinet_id"] for r in body["data"]] if st == 200 else [] - print(f" 能耗排行 cabinet_ids = {rank_ids}") - ok("能耗排行:仅项目A柜子", cab_a in rank_ids and cab_b not in rank_ids and cab_c not in rank_ids) - - # 小程序设备列表(h5) - st, body = call("GET", "/cabinets", t_a) - ids_h5 = [c["id"] for c in body["data"]] if st == 200 else [] - print(f" GET /cabinets (小程序列表) -> {st} ids={ids_h5}") - ok("小程序列表:项目A可见", cab_a in ids_h5) - ok("小程序列表:项目B不可见", cab_b not in ids_h5) - - # 写操作:范围外设备指令下发(设备必然离线,但归属校验在更前面) - st, body = call("POST", "/charge/start", t_a, {"compartment_id": 0}) - print(f" POST /charge/start (空仓体) -> {st} {body}") - ok("写操作接口有归属校验路径(非 500)", st in (400, 403, 404)) - - # ---------------- 3b. 组织级 scope = 自己组织(不额外收窄) ---------------- - print("\n=== [3b] 组织级 scope = 本人所属组织(合法,且不额外收窄)===") - st, body = call("PUT", f"/users/{u_a}/scopes", t_admin, - {"scopes": [{"scope_type": 1, "scope_id": org, "permission_type": 1}]}) - print(f" PUT scopes(组织级=本组织) -> {st} {body}") - ok("组织级=本组织 被接受", st == 200) - st, body = call("GET", "/cabinets/filter?page_size=100", t_a) - got = [c["id"] for c in body["data"]["data"]] if st == 200 else [] - ok("组织级范围下项目A仍可见", cab_a in got) - ok("组织级范围下项目B仍可见(组织级=整个组织)", cab_b in got) - - # ---------------- 3c. 脏数据:一条有效都没有 → 不锁死 ---------------- - print("\n=== [3c] 库内脏数据(指向不存在/跨组织/设备级)→ 忽略且不锁死 ===") - st, _ = call("PUT", f"/users/{u_a}/scopes", t_admin, {"scopes": []}) - ok("清空范围成功", st == 200) - cur.execute("INSERT INTO user_scopes (user_id, scope_type, scope_id, permission_type) VALUES (%s,2,%s,1)", - (u_a, 999999999)) - cur.execute("INSERT INTO user_scopes (user_id, scope_type, scope_id, permission_type) VALUES (%s,2,%s,1)", - (u_a, pc)) - cur.execute("INSERT INTO user_scopes (user_id, scope_type, scope_id, permission_type) VALUES (%s,3,%s,1)", - (u_a, cab_b)) - st, body = call("GET", "/cabinets/filter?page_size=100", t_a) - got = [c["id"] for c in body["data"]["data"]] if st == 200 else [] - print(f" GET /cabinets/filter(脏数据下) -> {st} ids={got}") - ok("脏数据全部被忽略后按「不限制」处理(项目B可见,不锁死)", cab_a in got and cab_b in got) - st, body = call("GET", f"/users/{u_a}/scopes", t_admin) - print(f" GET /users/{u_a}/scopes -> {st} {body['data'] if st == 200 else body}") - # 3 条脏数据:不存在项目 / 跨组织项目 / 设备级(设备级**永不参与鉴权** ⇒ valid=false) - ok("脏数据被标记 valid=false", - st == 200 and len(body["data"]) == 3 and all(not s["valid"] for s in body["data"])) - cur.execute("DELETE FROM user_scopes WHERE user_id=%s", (u_a,)) - - # ---------------- 4. 总管理员豁免 ---------------- - print("\n=== [4] 总管理员 → 看到全部(不受 scope 影响)===") - st, body = call("PUT", f"/users/{u_admin}/scopes", t_admin, - {"scopes": [{"scope_type": 2, "scope_id": pa, "permission_type": 1}]}) - print(f" (故意给总管理员配 scope=项目A) PUT scopes -> {st} {body}") - st, body = call("GET", "/cabinets/filter?page_size=100", t_admin) - got = [c["id"] for c in body["data"]["data"]] if st == 200 else [] - ok("总管理员仍可见项目A设备", cab_a in got) - ok("总管理员仍可见项目B设备(豁免)", cab_b in got) - ok("总管理员仍可见跨组织设备(豁免)", cab_c in got) - st, body = call("GET", f"/cabinets/{cab_b}", t_admin) - ok("总管理员可看任意设备详情", st == 200) - finally: - # ---------------- 6. 清理 ---------------- - print("\n=== [6] 清理临时数据 ===") - try: - cleanup(cur) - print(" 已清理(scopes/users/cabinets/boards/compartments/projects/organizations)") - cur.execute(f"SELECT COUNT(*) FROM organizations WHERE name LIKE '{TAG}%'") - print(" 残留组织数 =", cur.fetchone()[0]) - cur.execute(f"SELECT COUNT(*) FROM users WHERE name LIKE '{TAG}%'") - print(" 残留用户数 =", cur.fetchone()[0]) - cur.execute(f"SELECT COUNT(*) FROM projects WHERE name LIKE '{TAG}%'") - print(" 残留项目数 =", cur.fetchone()[0]) - cur.execute(f"SELECT COUNT(*) FROM cabinets WHERE name LIKE '{TAG}%'") - print(" 残留设备数 =", cur.fetchone()[0]) - except Exception as e: # noqa: BLE001 - print(" 清理失败:", e) - cur.close() - conn.close() - - print(f"\n=== 结果:{len(PASS)} passed / {len(FAIL)} failed ===") - if FAIL: - print("FAILED:", FAIL) - sys.exit(1) - - -def cleanup(cur): - """按名称前缀清理本脚本造的数据(幂等)""" - cur.execute(f"SELECT id FROM users WHERE name LIKE '{TAG}%'") - uids = [r[0] for r in cur.fetchall()] - if uids: - fmt = ",".join(["%s"] * len(uids)) - cur.execute(f"DELETE FROM user_scopes WHERE user_id IN ({fmt})", uids) - cur.execute(f"DELETE FROM user_roles WHERE user_id IN ({fmt})", uids) - cur.execute(f"DELETE FROM users WHERE id IN ({fmt})", uids) - cur.execute(f"SELECT id FROM cabinets WHERE name LIKE '{TAG}%'") - cids = [r[0] for r in cur.fetchall()] - if cids: - fmt = ",".join(["%s"] * len(cids)) - # 先清依赖业务数据(订单/记录/日志/仓体/仓板),再删柜子,避免外键报错 - cur.execute(f"DELETE FROM charge_orders WHERE cabinet_id IN ({fmt})", cids) - cur.execute(f"DELETE FROM charge_records WHERE cabinet_id IN ({fmt})", cids) - cur.execute(f"DELETE FROM device_logs WHERE cabinet_id IN ({fmt})", cids) - cur.execute(f"DELETE FROM compartments WHERE cabin_board_id IN " - f"(SELECT id FROM cabin_boards WHERE cabinet_id IN ({fmt}))", cids) - cur.execute(f"DELETE FROM cabin_boards WHERE cabinet_id IN ({fmt})", cids) - cur.execute(f"DELETE FROM cabinets WHERE id IN ({fmt})", cids) - cur.execute(f"SELECT id FROM projects WHERE name LIKE '{TAG}%'") - pids = [r[0] for r in cur.fetchall()] - if pids: - fmt = ",".join(["%s"] * len(pids)) - cur.execute(f"DELETE FROM projects WHERE id IN ({fmt})", pids) - cur.execute(f"DELETE FROM organizations WHERE name LIKE '{TAG}%'") - - -if __name__ == "__main__": - main() diff --git a/deploy_output.txt b/deploy_output.txt deleted file mode 100644 index 97005ab..0000000 --- a/deploy_output.txt +++ /dev/null @@ -1,48 +0,0 @@ -pms-api.new: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.0.0, stripped -6f53f17eb9e40579e8247e33739ffc1c41114382c3c208885a89a160ed66e640 pms-api.new -=== is-active === -active -=== port === -LISTEN 0 128 0.0.0.0:3000 0.0.0.0:* users:(("pms-api",pid=735613,fd=12)) -=== ???????? === -Sep 20 01:43:04 iZbp10r34ovrtuih2vq1hbZ pms-api[734262]: 2026-09-19T17:43:04.829257Z ERROR pms_api::error: 数据库错误: error occurred while decoding column "abstract_id": unexpected null; try decoding as an `Option` -Sep 20 01:43:05 iZbp10r34ovrtuih2vq1hbZ pms-api[734262]: 2026-09-19T17:43:05.073410Z INFO pms_api::routes::energy_stats: 能耗统计导出完成 task_id=1 file=./exports/能耗统计_20260919-20260920.xlsx -Sep 20 01:43:43 iZbp10r34ovrtuih2vq1hbZ systemd[1]: Stopping PMS API Server - Test Environment... -Sep 20 01:43:43 iZbp10r34ovrtuih2vq1hbZ systemd[1]: pms-api-test.service: Deactivated successfully. -Sep 20 01:43:43 iZbp10r34ovrtuih2vq1hbZ systemd[1]: Stopped PMS API Server - Test Environment. -Sep 20 01:43:43 iZbp10r34ovrtuih2vq1hbZ systemd[1]: pms-api-test.service: Consumed 2.914s CPU time, 2.0M memory peak. -Sep 20 01:43:43 iZbp10r34ovrtuih2vq1hbZ systemd[1]: Started PMS API Server - Test Environment. -Sep 20 01:43:43 iZbp10r34ovrtuih2vq1hbZ pms-api[735613]: 2026-09-19T17:43:43.495964Z INFO pms_api: 配置加载完成 -Sep 20 01:43:43 iZbp10r34ovrtuih2vq1hbZ pms-api[735613]: 2026-09-19T17:43:43.506602Z INFO pms_api::db: MySQL 连接池已建立 -Sep 20 01:43:43 iZbp10r34ovrtuih2vq1hbZ pms-api[735613]: 2026-09-19T17:43:43.506636Z INFO pms_api: 数据库连接成功 -Sep 20 01:43:43 iZbp10r34ovrtuih2vq1hbZ pms-api[735613]: 2026-09-19T17:43:43.744502Z INFO pms_api::db::migrate: 数据库索引创建完成 -Sep 20 01:43:43 iZbp10r34ovrtuih2vq1hbZ pms-api[735613]: 2026-09-19T17:43:43.865018Z INFO pms_api::db::migrate: 数据库列级迁移完成 -Sep 20 01:43:44 iZbp10r34ovrtuih2vq1hbZ pms-api[735613]: 2026-09-19T17:43:44.030363Z INFO pms_api::db::migrate: 默认权限码初始化完成 -Sep 20 01:43:44 iZbp10r34ovrtuih2vq1hbZ pms-api[735613]: 2026-09-19T17:43:44.562796Z INFO pms_api::db::migrate: 默认角色及权限分配完成 -Sep 20 01:43:44 iZbp10r34ovrtuih2vq1hbZ pms-api[735613]: 2026-09-19T17:43:44.563267Z INFO pms_api::db::migrate: 数据库迁移完成 -Sep 20 01:43:44 iZbp10r34ovrtuih2vq1hbZ pms-api[735613]: 2026-09-19T17:43:44.563290Z INFO pms_api: 数据库迁移完成 -Sep 20 01:43:44 iZbp10r34ovrtuih2vq1hbZ pms-api[735613]: 2026-09-19T17:43:44.567880Z INFO pms_api::redis: Redis 连接已建立 -Sep 20 01:43:44 iZbp10r34ovrtuih2vq1hbZ pms-api[735613]: 2026-09-19T17:43:44.567894Z INFO pms_api: Redis 连接成功 -Sep 20 01:43:44 iZbp10r34ovrtuih2vq1hbZ pms-api[735613]: 2026-09-19T17:43:44.568035Z INFO pms_api::workers::report_worker: [report_worker] 启动 -Sep 20 01:43:44 iZbp10r34ovrtuih2vq1hbZ pms-api[735613]: 2026-09-19T17:43:44.568157Z INFO pms_api::workers::reply_worker: [reply_worker] 启动 -Sep 20 01:43:44 iZbp10r34ovrtuih2vq1hbZ pms-api[735613]: 2026-09-19T17:43:44.568218Z INFO pms_api::workers::order_timeout: [order_timeout] 启动订单超时兜底,扫描间隔=300s,超时阈值=10h -Sep 20 01:43:44 iZbp10r34ovrtuih2vq1hbZ pms-api[735613]: 2026-09-19T17:43:44.568287Z INFO pms_api::workers::log_retention: [log_retention] 启动设备日志清理,保留 90 天,执行间隔 21600s,单批 5000 行 -Sep 20 01:43:44 iZbp10r34ovrtuih2vq1hbZ pms-api[735613]: 2026-09-19T17:43:44.569024Z INFO pms_api: HTTP 服务器启动于 http://0.0.0.0:3000 -=== ???? === -Sep 20 01:43:43 iZbp10r34ovrtuih2vq1hbZ pms-api[735613]: 2026-09-19T17:43:43.506602Z INFO pms_api::db: MySQL 连接池已建立 -Sep 20 01:43:43 iZbp10r34ovrtuih2vq1hbZ pms-api[735613]: 2026-09-19T17:43:43.506636Z INFO pms_api: 数据库连接成功 -Sep 20 01:43:43 iZbp10r34ovrtuih2vq1hbZ pms-api[735613]: 2026-09-19T17:43:43.744502Z INFO pms_api::db::migrate: 数据库索引创建完成 -Sep 20 01:43:43 iZbp10r34ovrtuih2vq1hbZ pms-api[735613]: 2026-09-19T17:43:43.865018Z INFO pms_api::db::migrate: 数据库列级迁移完成 -Sep 20 01:43:44 iZbp10r34ovrtuih2vq1hbZ pms-api[735613]: 2026-09-19T17:43:44.030363Z INFO pms_api::db::migrate: 默认权限码初始化完成 -Sep 20 01:43:44 iZbp10r34ovrtuih2vq1hbZ pms-api[735613]: 2026-09-19T17:43:44.562796Z INFO pms_api::db::migrate: 默认角色及权限分配完成 -Sep 20 01:43:44 iZbp10r34ovrtuih2vq1hbZ pms-api[735613]: 2026-09-19T17:43:44.563267Z INFO pms_api::db::migrate: 数据库迁移完成 -Sep 20 01:43:44 iZbp10r34ovrtuih2vq1hbZ pms-api[735613]: 2026-09-19T17:43:44.563290Z INFO pms_api: 数据库迁移完成 -Sep 20 01:43:44 iZbp10r34ovrtuih2vq1hbZ pms-api[735613]: 2026-09-19T17:43:44.567880Z INFO pms_api::redis: Redis 连接已建立 -Sep 20 01:43:44 iZbp10r34ovrtuih2vq1hbZ pms-api[735613]: 2026-09-19T17:43:44.567894Z INFO pms_api: Redis 连接成功 -Sep 20 01:43:44 iZbp10r34ovrtuih2vq1hbZ pms-api[735613]: 2026-09-19T17:43:44.568035Z INFO pms_api::workers::report_worker: [report_worker] 启动 -Sep 20 01:43:44 iZbp10r34ovrtuih2vq1hbZ pms-api[735613]: 2026-09-19T17:43:44.568157Z INFO pms_api::workers::reply_worker: [reply_worker] 启动 -Sep 20 01:43:44 iZbp10r34ovrtuih2vq1hbZ pms-api[735613]: 2026-09-19T17:43:44.568218Z INFO pms_api::workers::order_timeout: [order_timeout] 启动订单超时兜底,扫描间隔=300s,超时阈值=10h -Sep 20 01:43:44 iZbp10r34ovrtuih2vq1hbZ pms-api[735613]: 2026-09-19T17:43:44.568287Z INFO pms_api::workers::log_retention: [log_retention] 启动设备日志清理,保留 90 天,执行间隔 21600s,单批 5000 行 -Sep 20 01:43:44 iZbp10r34ovrtuih2vq1hbZ pms-api[735613]: 2026-09-19T17:43:44.569024Z INFO pms_api: HTTP 服务器启动于 http://0.0.0.0:3000 -=== ?? === -ls: cannot access '/data/pms_dev/api-server/'$'\r': No such file or directory diff --git a/deploy_remote.sh b/deploy_remote.sh deleted file mode 100644 index 6cefd93..0000000 --- a/deploy_remote.sh +++ /dev/null @@ -1,15 +0,0 @@ -set -e -cd /data/pms_dev/api-server -file pms-api.new -sha256sum pms-api.new -chmod +x pms-api.new -cp -a pms-api pms-api.bak-20260920-0143 -systemctl stop pms-api-test -mv -f pms-api.new pms-api -systemctl start pms-api-test -sleep 3 -echo '=== is-active ==='; systemctl is-active pms-api-test -echo '=== port ==='; ss -lntp | grep ':3000' || true -echo '=== ???????? ==='; journalctl -u pms-api-test --since '-2min' --no-pager | grep -Ei 'index|??|migrate|??' || true -echo '=== ???? ==='; journalctl -u pms-api-test -n 15 --no-pager -echo '=== ?? ==='; ls -l /data/pms_dev/api-server/