feat: 新增作息报时器功能并替换短链接前缀为/to/

- 新增作息报时器独立模块,包含完整的日程管理、语音播报和待机功能
- 短链接正式前缀由/l/改为/to/,解决手机小屏下与I、i、1字形混淆问题
- 保留旧前缀/l/、/L/、/I/、/i/兼容已分发的旧短链
- 新增短码大小写兜底匹配,仅唯一匹配时生效避免歧义
- 新增防爆紧急替换功能,可轮换首页鉴权并替换短链接
- 新增多个调试和集成测试脚本,保障数据安全和功能回归
This commit is contained in:
yangxiangyuan
2026-10-09 14:04:37 +08:00
parent ac475b6659
commit a3e8911deb
131 changed files with 7695 additions and 60 deletions
@@ -0,0 +1,104 @@
# -*- coding: utf-8 -*-
# 调试:宝龙导视牌 logo 裁切 · 掩膜参数迭代
# 目标:7 行连通域计数 = [11,11,10,11,11,11,11](r2 行牌子上只有 10 格)
# 用法:python dev_test_scripts\debug\debug_mall_crop_mask.py
import os
import sys
import cv2
import numpy as np
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'tools'))
from tool_crop_mall_logos import imread_u, imwrite_u, detect_board_quad, warp_board # noqa: E402
SRC = r'D:\Temp文件\baolong-mall\照片.jpg'
HERE = os.path.dirname(os.path.abspath(__file__))
img = imread_u(SRC)
quad = detect_board_quad(img)
warped = warp_board(img, quad)
wh, ww = warped.shape[:2]
gray = cv2.cvtColor(warped, cv2.COLOR_BGR2GRAY)
hsv = cv2.cvtColor(warped, cv2.COLOR_BGR2HSV)
S = hsv[:, :, 1].astype(np.float32)
V = hsv[:, :, 2].astype(np.float32)
# 局部纹理(box filter 标准差):过曝区颜色失效时靠纹理兜底
gf = gray.astype(np.float32)
m1 = cv2.boxFilter(gf, -1, (9, 9))
m2 = cv2.boxFilter(gf * gf, -1, (9, 9))
STD = np.sqrt(np.maximum(m2 - m1 * m1, 0))
def build_mask(s_th, v_th, t_th, close_k, open_k):
mask = (((S > s_th) | (V < v_th) | (STD > t_th)).astype(np.uint8)) * 255
mask[:60, :] = 0
mask[-60:, :] = 0
mask[:, :60] = 0
mask[:, -60:] = 0
mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, np.ones((close_k, close_k), np.uint8))
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, np.ones((open_k, open_k), np.uint8))
return mask
def row_counts(mask):
n, labels, stats, cents = cv2.connectedComponentsWithStats(mask, 8)
blocks = []
for i in range(1, n):
x, y, bw, bh, area = stats[i]
cx, cy = cents[i]
if area < 8000 or bw < 100 or bh < 80 or bw > 0.35 * ww or bh > 0.25 * wh:
continue
if cy < 0.17 * wh or cx < 0.06 * ww:
continue
blocks.append((cy, cx, x, y, bw, bh, area))
blocks.sort()
rows = []
for b in blocks:
if rows and abs(b[0] - rows[-1][-1][0]) < 60:
rows[-1].append(b)
else:
rows.append([b])
return [len(r) for r in rows], blocks, rows
combos = [
(45, 140, 999, 9, 7), # 旧参数(仅颜色)
(30, 170, 12, 21, 9),
(25, 180, 10, 25, 11),
(25, 180, 14, 25, 13),
(20, 190, 12, 29, 13),
(30, 170, 16, 21, 11),
]
best = None
for c in combos:
mask = build_mask(*c)
counts, blocks, rows = row_counts(mask)
print(c, '->', counts)
if counts == [11, 11, 10, 11, 11, 11, 11]:
best = (c, mask, rows)
break
if best:
c, mask, rows = best
scale = 1600.0 / ww
vis = cv2.resize(warped, (1600, int(wh * scale))).copy()
for r in rows:
for (cy, cx, x, y, bw, bh, area) in r:
cv2.rectangle(vis, (int(x * scale), int(y * scale)),
(int((x + bw) * scale), int((y + bh) * scale)), (0, 255, 0), 2)
imwrite_u(os.path.join(HERE, 'blocks_preview.jpg'), vis)
imwrite_u(os.path.join(HERE, 'mask_preview.jpg'), cv2.resize(mask, (1600, int(wh * scale))))
print('BEST', c)
else:
print('无完美组合,取最后一组出预览')
mask = build_mask(*combos[-1])
counts, blocks, rows = row_counts(mask)
scale = 1600.0 / ww
vis = cv2.resize(warped, (1600, int(wh * scale))).copy()
for r in rows:
for (cy, cx, x, y, bw, bh, area) in r:
cv2.rectangle(vis, (int(x * scale), int(y * scale)),
(int((x + bw) * scale), int((y + bh) * scale)), (0, 255, 0), 2)
imwrite_u(os.path.join(HERE, 'blocks_preview.jpg'), vis)
imwrite_u(os.path.join(HERE, 'mask_preview.jpg'), cv2.resize(mask, (1600, int(wh * scale))))