This commit is contained in:
shanshanzhong 2026-03-30 07:40:42 -07:00
parent 2a9c01ff8b
commit d072217e85
3 changed files with 274 additions and 5 deletions

View File

@ -582,11 +582,6 @@ func (l *ActivateOrderLogic) handleCommission(ctx context.Context, userInfo *use
return return
} }
// 渠道路径(佣金比例>0被邀请人首单赠N天
if orderInfo.IsNew {
_ = l.grantGiftDays(ctx, userInfo, int(l.svc.Config.Invite.GiftDays), orderInfo.OrderNo, "邀请赠送")
}
referer, err := l.svc.UserModel.FindOne(ctx, userInfo.RefererId) referer, err := l.svc.UserModel.FindOne(ctx, userInfo.RefererId)
if err != nil { if err != nil {
logger.WithContext(ctx).Error("Find referer failed", logger.WithContext(ctx).Error("Find referer failed",

274
scripts/gen_test_excel.py Normal file
View File

@ -0,0 +1,274 @@
"""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()

Binary file not shown.