hi-server/scripts/gen_test_excel.py
2026-03-30 07:40:42 -07:00

275 lines
20 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Generate ppanel-server test case Excel file."""
import os
from openpyxl import Workbook
from openpyxl.styles import (
Font, PatternFill, Alignment, Border, Side
)
from openpyxl.utils import get_column_letter
OUTPUT_PATH = os.path.join(os.path.dirname(__file__), "..", "tests", "ppanel_test_cases.xlsx")
# ── Color palette ──────────────────────────────────────────────────────────────
C_HEADER_BG = "1F4E79" # dark blue header row
C_HEADER_FONT = "FFFFFF" # white
C_SHEET_TITLE = "2E75B6" # mid blue sheet title row
C_P0_BG = "FFE2E2" # light red P0
C_P1_BG = "FFF2CC" # light yellow P1
C_P2_BG = "E2EFDA" # light green P2
C_BORDER = "BFBFBF"
def thin_border():
s = Side(style="thin", color=C_BORDER)
return Border(left=s, right=s, top=s, bottom=s)
def header_fill(hex_color):
return PatternFill("solid", fgColor=hex_color)
def row_fill(hex_color):
return PatternFill("solid", fgColor=hex_color)
# ── Column definitions ─────────────────────────────────────────────────────────
COLUMNS = ["用例ID", "模块", "功能点", "前置条件", "测试步骤", "预期结果",
"实际结果", "测试状态", "优先级", "备注"]
COL_WIDTHS = [16, 16, 28, 32, 40, 40, 20, 12, 8, 20]
# ── Test data ──────────────────────────────────────────────────────────────────
SHEET1_ORDER = {
"name": "订单核心流程",
"rows": [
# id, 模块, 功能点, 前置, 步骤, 预期, 优先
("TC-ORDER-001","订单/预创建","正常预览订单价格","用户已登录,套餐存在且在售","传入有效 subscribe_id, quantity=1","返回 price/amount/discount 字段正确","P0"),
("TC-ORDER-002","订单/预创建","数量为0时自动修正为1","用户已登录","quantity=0","自动设为1正常返回价格","P1"),
("TC-ORDER-003","订单/预创建","套餐购买数量限制Quota","用户已达到该套餐购买上限","再次预创建同套餐订单","返回 SubscribeQuotaLimit 错误","P0"),
("TC-ORDER-004","订单/预创建","新用户专属折扣24h内注册","用户注册在24h内套餐有 new_user_only 折扣","预创建订单","折扣生效amount < price","P0"),
("TC-ORDER-005","订单/预创建","老用户不享受新用户折扣","用户注册超过24h","预创建有 new_user_only 折扣的套餐","返回 SubscribeNewUserOnly 错误","P0"),
("TC-ORDER-006","订单/预创建","新用户已购过不重复享受新用户折扣","用户24h内注册但已购买过该套餐","预创建同套餐","折扣不生效,按原价计算","P0"),
("TC-ORDER-007","订单/预创建","优惠券不存在","用户已登录","传入不存在的 coupon code","返回 CouponNotExist 错误","P1"),
("TC-ORDER-008","订单/预创建","优惠券已用完count限制","优惠券 used_count >= count","传入该优惠券","返回 CouponAlreadyUsed 错误","P1"),
("TC-ORDER-009","订单/预创建","优惠券个人使用次数超限","用户已使用该优惠券达 user_limit 次","再次使用","返回 CouponInsufficientUsage 错误","P1"),
("TC-ORDER-010","订单/预创建","优惠券不适用于该套餐","优惠券绑定了特定套餐,与当前套餐不符","传入该优惠券","返回 CouponNotApplicable 错误","P1"),
("TC-ORDER-011","订单/预创建","支付手续费计算","支付方式有手续费配置","传入 payment_id","feeAmount 正确amount = 原金额 + 手续费","P1"),
("TC-ORDER-012","订单/预创建","礼品金额抵扣","用户 gift_amount > 0","预创建订单","deduction_amount 正确amount 减去礼品金额","P1"),
("TC-ORDER-013","订单/预创建","礼品金额全额抵扣amount归零","用户 gift_amount >= 订单金额","预创建订单","amount=0deduction_amount = 原订单金额","P1"),
("TC-ORDER-014","订单/购买","正常购买订阅","用户已登录,套餐在售有库存","发起购买请求","订单创建成功,返回 order_no","P0"),
("TC-ORDER-015","订单/购买","套餐库存为0不允许购买","套餐 inventory=0","发起购买","返回 SubscribeOutOfStock 错误","P0"),
("TC-ORDER-016","订单/购买","单订阅模式:已有 pending 订单自动关闭","SingleModel=true用户已有 pending 订单","对同套餐再次购买","旧 pending 订单关闭,新订单创建成功","P0"),
("TC-ORDER-017","订单/购买","单订阅模式:自动路由为续费","SingleModel=true用户已有有效订阅","购买相同套餐","订单类型=2续费parent_id 指向原订单","P0"),
("TC-ORDER-018","订单/购买","数量超过 MaxQuantity 限制","","quantity > MaxQuantity","返回 InvalidParams 错误","P1"),
("TC-ORDER-019","订单/购买","金额超过 MaxOrderAmount","套餐单价极高","购买","返回 InvalidParams 错误","P1"),
("TC-ORDER-020","订单/购买","15分钟后自动关闭未支付订单","订单已创建,未支付","等待15分钟后触发队列","订单状态变为 Close(3)","P0"),
("TC-ORDER-021","订单/激活","订单激活NewPurchase","订单状态=已支付(2),类型=1","触发激活队列","用户订阅创建,订单状态变为 Finished(5)","P0"),
("TC-ORDER-022","订单/激活","订单激活Renewal","订单状态=已支付,类型=2","触发激活","订阅到期时间延长","P0"),
("TC-ORDER-023","订单/激活","订单激活ResetTraffic","订单状态=已支付,类型=3","触发激活","用户流量重置","P0"),
("TC-ORDER-024","订单/激活","订单激活Recharge","订单状态=已支付,类型=4","触发激活","用户余额增加","P0"),
("TC-ORDER-025","订单/激活","订单激活Redemption","订单状态=已支付,类型=5","触发激活","兑换码激活成功","P0"),
("TC-ORDER-026","订单/激活","幂等性:已完成订单不重复处理","订单状态=Finished(5)","再次触发激活","直接跳过,不重复执行","P0"),
("TC-ORDER-027","订单/激活","非已支付状态订单不处理","订单状态=Pending(1) 或 Close(3)","触发激活","跳过,返回 ErrInvalidOrderStatus","P0"),
]
}
SHEET2_USER = {
"name": "用户模块",
"rows": [
("TC-USER-001","用户/注册","邮箱注册","邮箱未注册","提交有效邮箱+密码","用户创建成功,返回 token","P0"),
("TC-USER-002","用户/登录","邮箱密码登录","用户已注册","提交正确邮箱+密码","返回 JWT tokensession 写入 Redis","P0"),
("TC-USER-003","用户/设备登录","AES-CBC 加密设备登录","配置 security_secret","Body 使用正确密钥加密","登录成功","P0"),
("TC-USER-004","用户/设备登录","错误密钥设备登录","","Body 使用错误密钥加密","返回认证失败错误","P0"),
("TC-USER-005","用户/退出登录","解绑设备(退出家庭组)","用户在家庭组中","调用 unbind_device","用户从家庭组移除device 记录不删除、不禁用","P0"),
("TC-USER-006","用户/注销账号","正常注销","用户已登录","调用 delete_account","账号软删除auth_methods 软删除Redis 缓存清理","P0"),
("TC-USER-007","用户/注销账号","家主注销 → 解散家庭","用户是家庭组家主","注销账号","家庭所有成员 status=removedfamily status=disabled","P0"),
("TC-USER-008","用户/注销账号","成员注销 → 仅退出家庭","用户是家庭组成员","注销账号","仅该成员退出,家庭组继续存在","P0"),
("TC-USER-009","用户/注销","缓存清理email key 残留问题)","用户已注销email 缓存可能残留","注销后检查 Redis","cache:user:email:{email} 已删除","P0"),
("TC-USER-010","用户/邀请","绑定邀请码","用户未绑定过邀请码","提交有效邀请码","referer_id 写入,邀请关系建立","P1"),
("TC-USER-011","用户/邀请","重复绑定邀请码","用户已绑定邀请码","再次绑定","返回错误,不允许重复绑定","P1"),
("TC-USER-012","用户/佣金","首购返佣","用户通过邀请码注册,完成首次付款","订单激活","邀请人佣金增加","P1"),
("TC-USER-013","用户/佣金","only_first_purchase=true 仅首购返佣","配置仅首购","被邀请人第二次购买","不再发佣金","P1"),
("TC-USER-014","用户/佣金","赠送天数(双方)","邀请关系建立,被邀请人购买","订单激活","邀请人和被邀请人各获得赠送天数","P1"),
("TC-USER-015","用户/家庭组","踢出家庭成员","用户是家庭组家主","踢出某成员","该成员退出家庭组,设备记录不变","P1"),
("TC-USER-016","用户/订阅","查看订阅状态(含节点分组名和限速时间)","用户有有效订阅","查询订阅状态","返回节点分组名、限速起止时间","P1"),
]
}
SHEET3_SUB = {
"name": "订阅套餐",
"rows": [
("TC-SUB-001","套餐/列表","获取可用套餐列表","","调用套餐列表接口","返回所有在售套餐","P1"),
("TC-SUB-002","套餐/列表","老版本客户端裁剪套餐列表","请求头含 X-App-Id老版本标识","调用套餐列表","每个套餐的 discount 列表去掉最后一项","P1"),
("TC-SUB-003","套餐/购买限制","Quota 限制(每用户购买上限)","套餐设置 quota=1","用户购买2次同套餐","第二次返回 SubscribeQuotaLimit","P0"),
("TC-SUB-004","套餐/折扣","数量折扣梯度","套餐配置多级数量折扣","购买不同数量","对应折扣率正确应用","P1"),
("TC-SUB-005","套餐/库存","库存充足时正常购买","inventory > 0","购买","成功inventory -1","P1"),
("TC-SUB-006","套餐/库存","库存=-1无限库存","inventory=-1","多次购买","不减少库存,始终可购","P1"),
]
}
SHEET4_PAY = {
"name": "支付与优惠券",
"rows": [
("TC-PAY-001","支付/方式","获取可用支付方式列表","","调用支付方式接口","返回当前配置的支付方式","P1"),
("TC-PAY-002","支付/手续费","固定手续费计算","支付方式配置固定手续费","下单","feeAmount = 配置值","P1"),
("TC-PAY-003","支付/手续费","百分比手续费计算","支付方式配置百分比手续费","下单","feeAmount = amount × 百分比","P1"),
("TC-PAY-004","支付/手续费","amount=0 时不计算手续费","礼品金额全额抵扣后 amount=0","下单","feeAmount=0","P1"),
("TC-CPN-001","优惠券/固定减免","固定金额优惠券","优惠券类型=固定value=100","使用优惠券","订单减免100","P1"),
("TC-CPN-002","优惠券/百分比","百分比优惠券","优惠券类型=百分比value=0.8","使用优惠券","订单金额×0.8","P1"),
("TC-CPN-003","优惠券/过期","过期优惠券不可用","优惠券 expire_at < now","使用","返回错误CouponExpired","P1"),
("TC-CPN-004","优惠券/套餐绑定","仅限指定套餐使用","优惠券绑定套餐A","用于套餐B","返回 CouponNotApplicable","P1"),
]
}
SHEET5_IAP = {
"name": "IAP苹果内购",
"rows": [
("TC-IAP-001","IAP/绑定","绑定苹果内购 transaction","苹果 transaction 有效","提交 transaction_id","订单创建并激活,订阅开通","P1"),
("TC-IAP-002","IAP/绑定","重复绑定同一 transaction","transaction 已绑定","再次提交","幂等处理,不重复创建订单","P1"),
("TC-IAP-003","IAP/单订阅模式","内购续费路由","SingleModel=true用户已有订阅","提交续费 transaction","路由为续费类型订单","P1"),
("TC-IAP-004","IAP/对账","日对账任务","配置了 IAP 对账","触发日对账","检查并补处理漏掉的 transaction","P2"),
]
}
SHEET6_LOG = {
"name": "日志与缓存",
"rows": [
("TC-LOG-001","日志/佣金","佣金记录写入 system_logs","发生佣金发放","触发订单激活","type=33 的记录写入content.type 为 331 或 332","P2"),
("TC-LOG-002","日志/礼品金额","礼品金额扣除记录","用户有 gift_amount下单扣除","购买","GiftTypeReduce 记录写入 system_logs","P2"),
("TC-CACHE-001","缓存/用户","注销后 user email 缓存清理","用户已注销","检查 Redis","cache:user:email:{email} 已删除","P0"),
("TC-CACHE-002","缓存/订阅","订阅 token 缓存有效","用户有订阅","查询订阅","从 cache:user:subscribe:token:{token} 命中","P2"),
("TC-CACHE-003","缓存/签名","X-App-Id 签名验证","AppSecrets 已配置","发送带签名请求","验签通过,正常处理","P1"),
("TC-CACHE-004","缓存/签名","无 X-App-Id 跳过签名","","发送无签名请求","直接通过,不验签","P1"),
]
}
SHEET7_QUEUE = {
"name": "队列任务",
"rows": [
("TC-QUEUE-001","队列/订单关闭","超时自动关闭订单","未支付订单存在","等待15分钟","订单状态=Close","P0"),
("TC-QUEUE-002","队列/订阅检查","定期检查订阅到期","用户订阅即将到期","触发 checkSubscription","到期通知发送","P2"),
("TC-QUEUE-003","队列/流量统计","服务器流量统计写入","有流量数据上报","触发 trafficStat","流量数据正确写入 DB","P2"),
("TC-QUEUE-004","队列/邮件","批量发送邮件任务","已创建批量邮件任务","触发队列","邮件发送成功,任务状态更新","P2"),
("TC-QUEUE-005","队列/流量重置","定期重置用户流量","配置了流量重置周期","触发 resetTraffic","用户流量归零","P2"),
]
}
ALL_SHEETS = [SHEET1_ORDER, SHEET2_USER, SHEET3_SUB, SHEET4_PAY, SHEET5_IAP, SHEET6_LOG, SHEET7_QUEUE]
PRIORITY_FILL = {
"P0": row_fill(C_P0_BG),
"P1": row_fill(C_P1_BG),
"P2": row_fill(C_P2_BG),
}
def write_sheet(wb: Workbook, sheet_def: dict):
ws = wb.create_sheet(title=sheet_def["name"])
rows = sheet_def["rows"]
# ── Title row ──────────────────────────────────────────────────────────────
ws.merge_cells(start_row=1, start_column=1, end_row=1, end_column=len(COLUMNS))
title_cell = ws.cell(row=1, column=1, value=f"ppanel-server 测试用例 — {sheet_def['name']}")
title_cell.font = Font(name="微软雅黑", bold=True, size=13, color=C_HEADER_FONT)
title_cell.fill = header_fill(C_SHEET_TITLE)
title_cell.alignment = Alignment(horizontal="center", vertical="center")
ws.row_dimensions[1].height = 28
# ── Header row ─────────────────────────────────────────────────────────────
for col_idx, col_name in enumerate(COLUMNS, start=1):
cell = ws.cell(row=2, column=col_idx, value=col_name)
cell.font = Font(name="微软雅黑", bold=True, size=10, color=C_HEADER_FONT)
cell.fill = header_fill(C_HEADER_BG)
cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
cell.border = thin_border()
ws.row_dimensions[2].height = 22
# ── Data rows ──────────────────────────────────────────────────────────────
for r_idx, row in enumerate(rows, start=3):
tc_id, module, feature, precond, steps, expected, priority = row
values = [tc_id, module, feature, precond, steps, expected, "", "", priority, ""]
fill = PRIORITY_FILL.get(priority, None)
for c_idx, val in enumerate(values, start=1):
cell = ws.cell(row=r_idx, column=c_idx, value=val)
cell.font = Font(name="微软雅黑", size=9)
cell.alignment = Alignment(horizontal="left", vertical="center", wrap_text=True)
cell.border = thin_border()
if fill:
cell.fill = fill
ws.row_dimensions[r_idx].height = 45
# ── Column widths ──────────────────────────────────────────────────────────
for col_idx, width in enumerate(COL_WIDTHS, start=1):
ws.column_dimensions[get_column_letter(col_idx)].width = width
# ── Freeze panes ──────────────────────────────────────────────────────────
ws.freeze_panes = "A3"
# ── Auto filter ───────────────────────────────────────────────────────────
ws.auto_filter.ref = f"A2:{get_column_letter(len(COLUMNS))}2"
def write_legend_sheet(wb: Workbook):
ws = wb.create_sheet(title="说明", index=0)
ws.column_dimensions["A"].width = 18
ws.column_dimensions["B"].width = 50
title = ws.cell(row=1, column=1, value="ppanel-server 测试用例说明")
ws.merge_cells("A1:B1")
title.font = Font(name="微软雅黑", bold=True, size=13, color=C_HEADER_FONT)
title.fill = header_fill(C_SHEET_TITLE)
title.alignment = Alignment(horizontal="center", vertical="center")
ws.row_dimensions[1].height = 28
legend_data = [
("项目", "说明"),
("测试框架", "ppanel-server — go-zero + Gin"),
("数据库", "本地 MySQL真实禁止 SQLite"),
("Redis", "本地 Redis 或 miniredis"),
("时间戳规范", "后端统一返回秒级 Unix(),前端 ×1000"),
("", ""),
("优先级", "含义"),
("P0红色", "核心业务,必须通过。订单/认证/缓存清理等"),
("P1黄色", "重要功能,强烈建议测试。折扣/优惠券/邀请等"),
("P2绿色", "辅助功能,建议测试。日志/队列/IAP 等"),
("", ""),
("测试状态", "填写规范"),
("Pass", "用例通过"),
("Fail", "用例失败,需记录实际结果"),
("Block", "用例被阻塞(依赖功能未就绪)"),
("Skip", "本轮跳过"),
("", ""),
("Sheet 说明", ""),
("Sheet1 订单核心流程", "27 条:预创建/购买/激活全流程"),
("Sheet2 用户模块", "16 条:注册/登录/注销/邀请/家庭组"),
("Sheet3 订阅套餐", "6 条:库存/折扣/限额"),
("Sheet4 支付与优惠券", "8 条:手续费/优惠券各类型"),
("Sheet5 IAP苹果内购", "4 条:内购/对账"),
("Sheet6 日志与缓存", "6 条:日志写入/缓存清理"),
("Sheet7 队列任务", "5 条:队列任务验证"),
]
for r_idx, (key, val) in enumerate(legend_data, start=2):
c1 = ws.cell(row=r_idx, column=1, value=key)
c2 = ws.cell(row=r_idx, column=2, value=val)
for c in (c1, c2):
c.font = Font(name="微软雅黑", size=9)
c.alignment = Alignment(vertical="center", wrap_text=True)
c.border = thin_border()
if key in ("项目", "优先级", "测试状态", "Sheet 说明"):
for c in (c1, c2):
c.font = Font(name="微软雅黑", bold=True, size=9, color=C_HEADER_FONT)
c.fill = header_fill(C_HEADER_BG)
ws.row_dimensions[r_idx].height = 18
def main():
os.makedirs(os.path.dirname(os.path.abspath(OUTPUT_PATH)), exist_ok=True)
wb = Workbook()
wb.remove(wb.active) # remove default sheet
write_legend_sheet(wb)
for sheet_def in ALL_SHEETS:
write_sheet(wb, sheet_def)
wb.save(OUTPUT_PATH)
print(f"Excel saved: {os.path.abspath(OUTPUT_PATH)}")
if __name__ == "__main__":
main()