chore: 清理误入库的临时产物(.tmp-* 代理工作目录、部署脚本输出)并加入 .gitignore
This commit is contained in:
parent
ee92102f3c
commit
4e752953b0
7
.gitignore
vendored
7
.gitignore
vendored
@ -54,3 +54,10 @@ uploads/
|
|||||||
tmp/
|
tmp/
|
||||||
.codebuddy/
|
.codebuddy/
|
||||||
|
|
||||||
|
|
||||||
|
# 代理/部署临时产物(不入库)
|
||||||
|
.tmp-*/
|
||||||
|
.cargo-*/
|
||||||
|
deploy_output.txt
|
||||||
|
deploy_remote.sh
|
||||||
|
/tmp/
|
||||||
|
|||||||
@ -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]
|
|
||||||
@ -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()
|
|
||||||
@ -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()
|
|
||||||
@ -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'))
|
|
||||||
|
|
||||||
临时数据清理完成
|
|
||||||
@ -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临时数据清理完成")
|
|
||||||
@ -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)
|
|
||||||
Binary file not shown.
@ -1,2 +0,0 @@
|
|||||||
SUPER=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjcsInBob25lIjoiMTk5MDAwMDAwMDEiLCJleHAiOjE3ODk5MjYzMTUsImlhdCI6MTc4OTgzOTkxNX0.36ocLLksl-sZ_PK8M1mI8M9i9NOq0OcYv_xOdz2iCQA
|
|
||||||
ORG1=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjcsInBob25lIjoiMTk5MDAwMDAwMDEiLCJleHAiOjE3ODk5MjYzMTYsImlhdCI6MTc4OTgzOTkxNn0.OpOgTeYh9UdjReUVLh698TN9OOjmuPMxbao8LBZHQDM
|
|
||||||
@ -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 列)
|
|
||||||
@ -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]
|
|
||||||
@ -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]
|
|
||||||
@ -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())
|
|
||||||
@ -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()
|
|
||||||
@ -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()
|
|
||||||
@ -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 行
|
|
||||||
@ -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
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"org_id": 4,
|
|
||||||
"project_id": 3,
|
|
||||||
"cabinet_id": 4,
|
|
||||||
"board_id": 5,
|
|
||||||
"compartment_id": 7,
|
|
||||||
"record_id": 16
|
|
||||||
}
|
|
||||||
@ -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)))
|
|
||||||
@ -1 +0,0 @@
|
|||||||
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjcsInBob25lIjoiMTk5MDAwMDAwMDEiLCJleHAiOjE3ODk5MjU5NDYsImlhdCI6MTc4OTgzOTU0Nn0.X-T6uBTENj9UK9seDIyyJJXopJcNJfSfDZ1C60AP-yA
|
|
||||||
@ -1 +0,0 @@
|
|||||||
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjcsInBob25lIjoiMTk5MDAwMDAwMDEiLCJleHAiOjE3ODk5MjU5ODksImlhdCI6MTc4OTgzOTU4OX0.ZiKHmR2fKvfSLaINjDq_R9npcFldGqP4WOQessAotxg
|
|
||||||
@ -1 +0,0 @@
|
|||||||
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjcsInBob25lIjoiMTk5MDAwMDAwMDEiLCJleHAiOjE3ODk5MjU5ODksImlhdCI6MTc4OTgzOTU4OX0.Y0Tju_TsC07S-qyIhD4cRpT8I1uqzTuCGWVH6QOorJM
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# 能耗报表接口验证脚本(在测试服务器上执行,直连本机 :3000,绕开 nginx)
|
|
||||||
# 用法: bash verify_endpoints.sh <JWT> [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"
|
|
||||||
@ -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()
|
|
||||||
@ -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
|
|
||||||
@ -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]
|
|
||||||
@ -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()
|
|
||||||
Binary file not shown.
@ -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
|
|
||||||
@ -1 +0,0 @@
|
|||||||
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.80s
|
|
||||||
@ -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
|
|
||||||
|
|
||||||
@ -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
|
|
||||||
|
|
||||||
@ -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),'<NULL>'),
|
|
||||||
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 ("<MISSING>",) * 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))
|
|
||||||
@ -1,58 +0,0 @@
|
|||||||
"""只读比对脚本:逐列比对 pms_dev(测试) 与 pms(生产) 的**全部**列定义
|
|
||||||
|
|
||||||
比对维度:COLUMN_TYPE / IS_NULLABLE / COLUMN_DEFAULT / EXTRA / COLUMN_COMMENT
|
|
||||||
另外单独统计两库是否还残留 `TIMESTAMP` 列(本项目禁止:sqlx 只能把它解码成 DateTime<Utc>,
|
|
||||||
而 Rust 侧时间列全是 NaiveDateTime/Option<NaiveDateTime> ⇒ 接口 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),'<NULL>'),
|
|
||||||
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]))
|
|
||||||
@ -1,42 +0,0 @@
|
|||||||
"""列出两库全部时间列的类型,并做差异比对(只读)"""
|
|
||||||
import pymysql
|
|
||||||
|
|
||||||
SQL = """
|
|
||||||
SELECT TABLE_NAME, COLUMN_NAME, DATA_TYPE, IS_NULLABLE,
|
|
||||||
IFNULL(CAST(COLUMN_DEFAULT AS CHAR),'<NULL>')
|
|
||||||
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, ("<missing>",) * 3), prd.get(k, ("<missing>",) * 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])
|
|
||||||
@ -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)
|
|
||||||
@ -1,41 +0,0 @@
|
|||||||
"""列出两库时间列的完整定义(含 EXTRA),用于生成对齐清单(只读)"""
|
|
||||||
import pymysql
|
|
||||||
|
|
||||||
SQL = """
|
|
||||||
SELECT TABLE_NAME, COLUMN_NAME, DATA_TYPE, IS_NULLABLE,
|
|
||||||
IFNULL(CAST(COLUMN_DEFAULT AS CHAR),'<NULL>'),
|
|
||||||
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]))
|
|
||||||
@ -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 <pwd> root@118.31.119.206 "systemctl stop pms-api pms-device && cp /data/pms/backup-<TS>/* /data/pms/... && systemctl start pms-api pms-device"
|
|
||||||
备份目录见 /data/pms/backup-<时间戳>/(脚本已把 .env / 前端 / 单元一并备份;只保留最近 5 次)
|
|
||||||
@ -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()
|
|
||||||
@ -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))
|
|
||||||
@ -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)
|
|
||||||
@ -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)"
|
|
||||||
@ -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()
|
|
||||||
@ -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()
|
|
||||||
@ -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),'<NULL>') 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])
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
import pymysql
|
|
||||||
|
|
||||||
SQL = """
|
|
||||||
SELECT TABLE_NAME, COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE,
|
|
||||||
IFNULL(CAST(COLUMN_DEFAULT AS CHAR),'<NULL>') 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()
|
|
||||||
@ -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))
|
|
||||||
@ -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
|
|
||||||
@ -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"
|
|
||||||
@ -1 +0,0 @@
|
|||||||
/usr/local/bin/pms-backup-db.sh
|
|
||||||
@ -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
|
|
||||||
@ -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"
|
|
||||||
@ -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' '
|
|
||||||
@ -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"
|
|
||||||
@ -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
|
|
||||||
@ -1 +0,0 @@
|
|||||||
journalctl -u pms-api-test --since '-2 min' --no-pager | grep -iE 'ERROR' | tail -6
|
|
||||||
@ -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
|
|
||||||
@ -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
|
|
||||||
@ -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<KEEP && i<${#sorted[@]};i++)); do echo " ${sorted[$i]}"; done
|
|
||||||
echo " 将删除:"
|
|
||||||
for ((i=KEEP;i<${#sorted[@]};i++)); do echo " ${sorted[$i]}"; done
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "############ 执行删除 ############"
|
|
||||||
for ((i=KEEP;i<${#sorted[@]};i++)); do
|
|
||||||
f="${sorted[$i]}"
|
|
||||||
if [ "$(readlink -f "$f")" = "$RUNNING" ]; then
|
|
||||||
echo " [skip] 正在运行的二进制,拒绝删除: $f"
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
case "$(basename "$f")" in
|
|
||||||
pms-api) echo " [skip] 当前二进制 pms-api,拒绝删除"; continue ;;
|
|
||||||
esac
|
|
||||||
rm -f -- "$f"
|
|
||||||
echo " 已删除 $f"
|
|
||||||
done
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "############ 清理后 ############"
|
|
||||||
ls -la "$DIR"
|
|
||||||
du -sh "$DIR"
|
|
||||||
echo "--- 服务仍在运行 ---"
|
|
||||||
systemctl is-active pms-api
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
echo "=== 1) 导出产物证据 ==="
|
|
||||||
ls -la /data/pms_dev/api-server/exports/
|
|
||||||
file /data/pms_dev/api-server/exports/*.xlsx
|
|
||||||
echo "=== 2) 测试环境日志:导出完成 + 近 5 分钟 ERROR ==="
|
|
||||||
journalctl -u pms-api-test --since '-5 min' --no-pager | grep -E '导出完成|ERROR' | tail -8
|
|
||||||
echo "=== 3) 删除本次验收产生的 xlsx(exports 目录本身保留,属主 pms 不动)==="
|
|
||||||
rm -f "/data/pms_dev/api-server/exports/充电记录__-.xlsx"
|
|
||||||
ls -la /data/pms_dev/api-server/exports/
|
|
||||||
stat -c '%U:%G %n' /data/pms_dev/api-server/exports /data/pms_dev/api-server/uploads
|
|
||||||
echo "=== 4) 清理本次会话在服务器上的临时文件 ==="
|
|
||||||
rm -rf /tmp/retention-test /tmp/retention-test.sh /tmp/retention.sh /tmp/remote*.sh
|
|
||||||
ls /tmp/*.sh /tmp/retention* 2>/dev/null || echo " /tmp 已无本次临时文件"
|
|
||||||
@ -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"
|
|
||||||
@ -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)"
|
|
||||||
@ -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: 数据库迁移完成
|
|
||||||
@ -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: 数据库迁移完成
|
|
||||||
@ -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: 数据库迁移完成
|
|
||||||
@ -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: 数据库迁移完成
|
|
||||||
@ -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: 数据库迁移完成
|
|
||||||
@ -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: 数据库迁移完成
|
|
||||||
@ -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: 数据库迁移完成
|
|
||||||
@ -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: 数据库迁移完成
|
|
||||||
@ -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: 数据库迁移完成
|
|
||||||
@ -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: 数据库迁移完成
|
|
||||||
@ -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: 数据库迁移完成
|
|
||||||
@ -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: 数据库迁移完成
|
|
||||||
@ -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: 数据库迁移完成
|
|
||||||
@ -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"
|
|
||||||
}
|
|
||||||
@ -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()
|
|
||||||
@ -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
|
|
||||||
@ -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/
|
|
||||||
Loading…
x
Reference in New Issue
Block a user