feat: add web application
This commit is contained in:
Binary file not shown.
@@ -0,0 +1,800 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import Image, ImageDraw
|
||||
from docx import Document
|
||||
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
||||
from docx.oxml import OxmlElement
|
||||
from docx.oxml.ns import qn
|
||||
from docx.shared import Inches, Pt, RGBColor
|
||||
|
||||
from build_reference_prd import (
|
||||
INK, MUTED, add_page_number, add_picture, add_table, bullet, font,
|
||||
heading, numbered, p, page_break, rounded, set_run_font, text,
|
||||
)
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
OUT_DIR = ROOT / "docs" / "admin_reference_prd"
|
||||
ASSET_DIR = OUT_DIR / "assets"
|
||||
OUT_DOCX = OUT_DIR / "后台管理系统需求文档.docx"
|
||||
|
||||
NAV_BG = "#191B20"
|
||||
TOP_BG = "#FFFFFF"
|
||||
PAGE_BG = "#F3F5F8"
|
||||
TEAL = "#4F958E"
|
||||
BLUE = "#5A7BEF"
|
||||
RED = "#D95E62"
|
||||
ORANGE = "#E39B3C"
|
||||
PURPLE = "#8C54CC"
|
||||
GREEN = "#45A06B"
|
||||
|
||||
|
||||
def ui_text(draw, xy, value, size=20, fill="#202631", bold=False, anchor=None):
|
||||
text(draw, xy, value, size=size, fill=fill, bold=bold, anchor=anchor)
|
||||
|
||||
|
||||
def desktop_base(title: str, active: str):
|
||||
im = Image.new("RGB", (1600, 900), PAGE_BG)
|
||||
d = ImageDraw.Draw(im)
|
||||
d.rectangle((0, 0, 250, 900), fill=NAV_BG)
|
||||
d.rectangle((250, 0, 1600, 66), fill=TOP_BG)
|
||||
ui_text(d, (125, 34), "DD后台", 23, "white", anchor="mm")
|
||||
ui_text(d, (278, 34), "☰", 24, "#333A46", anchor="mm")
|
||||
ui_text(d, (330, 34), "↻", 24, "#333A46", anchor="mm")
|
||||
ui_text(d, (1530, 34), "admin ▾", 18, "#333A46", anchor="mm")
|
||||
groups = [
|
||||
("主页", ["首页", "网站设置", "文本管理", "Banner设置", "手工商城"]),
|
||||
("会员管理", ["会员列表", "会员等级", "任务大厅"]),
|
||||
("交易管理", ["订单列表", "充值管理", "提现管理", "客服管理"]),
|
||||
("商城管理", ["商品列表"]),
|
||||
("任务管理", ["任务列表", "会员任务审核"]),
|
||||
("管理员", ["后台管理员", "角色管理", "权限管理", "数据字典"]),
|
||||
("设置", ["基本资料", "修改密码"]),
|
||||
]
|
||||
y = 85
|
||||
for group, children in groups:
|
||||
ui_text(d, (24, y), group, 17, "#D9DEE7")
|
||||
y += 34
|
||||
if active in children or active == group:
|
||||
for child in children:
|
||||
if child == active:
|
||||
d.rectangle((0, y - 6, 250, y + 27), fill=TEAL)
|
||||
ui_text(d, (50, y + 10), child, 16, "white", anchor="lm")
|
||||
else:
|
||||
ui_text(d, (50, y + 10), child, 15, "#C9CFD8", anchor="lm")
|
||||
y += 34
|
||||
y += 10
|
||||
d.rectangle((250, 66, 1600, 112), fill="#FFFFFF")
|
||||
ui_text(d, (282, 89), "⌂", 19, "#596273", anchor="mm")
|
||||
ui_text(d, (322, 89), title, 17, "#596273", anchor="lm")
|
||||
return im, d
|
||||
|
||||
|
||||
def metric_card(d, box, label, value, sub1, sub2, color):
|
||||
rounded(d, box, 8, color)
|
||||
x1, y1, x2, y2 = box
|
||||
ui_text(d, (x1 + 28, y1 + 36), label, 20, "white")
|
||||
ui_text(d, (x1 + 28, y1 + 84), value, 32, "white", bold=True)
|
||||
ui_text(d, (x1 + 28, y1 + 123), sub1, 16, "white")
|
||||
ui_text(d, (x1 + 28, y1 + 151), sub2, 16, "white")
|
||||
|
||||
|
||||
def dashboard_proto():
|
||||
im, d = desktop_base("首页", "首页")
|
||||
ui_text(d, (275, 142), "商城统计", 18, "#333A46")
|
||||
cards = [
|
||||
("用户总量", "5,038", "今日注册 13", "昨日注册 21", "#D95B83"),
|
||||
("充值用户总量", "2,609", "今日新增 7", "昨日新增 15", "#E86866"),
|
||||
("商品总量", "705", "今日新增 0", "昨日新增 0", "#5BA4CF"),
|
||||
("订单总量", "75,548", "今日新增 243", "昨日新增 264", "#9851CF"),
|
||||
("订单总金额", "87,576,426.20", "今日新增 155,616.00", "昨日新增 554,758.90", "#5755B7"),
|
||||
("用户充值", "40,145,160.82", "今日新增 59,828.00", "昨日新增 230,469.00", "#E6AF1E"),
|
||||
("用户提现", "3,500,782.79", "今日新增 2,316.04", "昨日新增 4,648.47", "#D96E19"),
|
||||
("抢单佣金", "2,315,790.44", "今日新增 6,073.24", "昨日新增 13,803.84", "#833052"),
|
||||
]
|
||||
for i, c in enumerate(cards):
|
||||
col, row = i % 2, i // 2
|
||||
x = 275 + col * 645
|
||||
y = 165 + row * 170
|
||||
metric_card(d, (x, y, x + 615, y + 150), *c)
|
||||
return im
|
||||
|
||||
|
||||
def list_proto(title: str, active: str, filters, headers, rows, action_label, sensitive=False):
|
||||
im, d = desktop_base(title, active)
|
||||
x0, y0 = 280, 140
|
||||
for i, label in enumerate(filters):
|
||||
x = x0 + (i % 4) * 300
|
||||
y = y0 + (i // 4) * 58
|
||||
ui_text(d, (x, y), label, 15, "#374151")
|
||||
rounded(d, (x + 72, y - 10, x + 270, y + 25), 4, "white", "#D5DAE2")
|
||||
ui_text(d, (x + 86, y + 7), "请输入 / 请选择", 13, "#9AA2AE", anchor="lm")
|
||||
top = y0 + ((len(filters) - 1) // 4 + 1) * 58
|
||||
rounded(d, (280, top, 360, top + 38), 4, BLUE)
|
||||
ui_text(d, (320, top + 20), "搜索", 15, "white", anchor="mm")
|
||||
rounded(d, (370, top, 465, top + 38), 4, RED)
|
||||
ui_text(d, (418, top + 20), "批量删除", 15, "white", anchor="mm")
|
||||
rounded(d, (475, top, 555, top + 38), 4, TEAL)
|
||||
ui_text(d, (515, top + 20), "添加", 15, "white", anchor="mm")
|
||||
table_y = top + 58
|
||||
widths = [120] + [max(100, int(1050 / max(1, len(headers) - 1))) for _ in headers[1:]]
|
||||
scale = 1270 / sum(widths)
|
||||
widths = [int(w * scale) for w in widths]
|
||||
x = 275
|
||||
for i, h in enumerate(headers):
|
||||
d.rectangle((x, table_y, x + widths[i], table_y + 46), fill="#405D8E")
|
||||
ui_text(d, (x + widths[i] / 2, table_y + 24), h, 13, "white", anchor="mm")
|
||||
x += widths[i]
|
||||
for ridx, row in enumerate(rows):
|
||||
y = table_y + 46 + ridx * 54
|
||||
x = 275
|
||||
for i, val in enumerate(row):
|
||||
fill = "white" if ridx % 2 == 0 else "#F7F9FC"
|
||||
d.rectangle((x, y, x + widths[i], y + 54), fill=fill, outline="#DDE2EA")
|
||||
display = val
|
||||
if sensitive and i in (1, 2, 3):
|
||||
display = val
|
||||
ui_text(d, (x + 8, y + 27), display[:18], 12, "#2C3440", anchor="lm")
|
||||
x += widths[i]
|
||||
rounded(d, (1180, table_y + 240, 1515, table_y + 288), 6, "#FFF7E6", "#E5B45A")
|
||||
ui_text(d, (1195, table_y + 264), action_label, 13, "#915F14", anchor="lm")
|
||||
ui_text(d, (280, 860), "共 5,038 条 30 条/页 1 2 3 …", 13, "#667085")
|
||||
return im
|
||||
|
||||
|
||||
def modal_proto():
|
||||
im = list_proto(
|
||||
"会员列表", "会员列表", ["用户名", "手机号", "注册时间", "状态", "自动派单"],
|
||||
["ID", "账号", "绑卡姓名", "用户名", "余额", "状态", "操作"],
|
||||
[["8165", "155****7676", "王**", "会员 A", "299.00", "正常", "编辑 / 银行卡 / 地址 / 团队"],
|
||||
["8164", "180****8046", "—", "会员 B", "0.00", "正常", "账变 / 奖励金 / 禁用"]],
|
||||
"敏感信息默认脱敏;查看需授权并留痕", True,
|
||||
)
|
||||
overlay = Image.new("RGBA", im.size, (0, 0, 0, 85))
|
||||
im = Image.alpha_composite(im.convert("RGBA"), overlay).convert("RGB")
|
||||
d = ImageDraw.Draw(im)
|
||||
rounded(d, (560, 155, 1285, 785), 10, "white", "#D2D7E0")
|
||||
d.rectangle((560, 155, 1285, 208), fill="#F6F7F9")
|
||||
ui_text(d, (590, 182), "添加会员", 21, "#202631", bold=True, anchor="lm")
|
||||
ui_text(d, (1250, 182), "×", 23, "#667085", anchor="mm")
|
||||
fields = ["标记", "用户名称", "手机号码", "账户余额", "冻结金额", "会员等级", "交易状态", "登录密码", "交易密码", "上级ID", "信用分"]
|
||||
for i, lab in enumerate(fields):
|
||||
col, row = i % 2, i // 2
|
||||
x = 595 + col * 330
|
||||
y = 238 + row * 74
|
||||
ui_text(d, (x, y), lab, 15, "#3C4450")
|
||||
rounded(d, (x, y + 20, x + 290, y + 56), 4, "white", "#CBD2DC")
|
||||
rounded(d, (975, 725, 1065, 765), 5, BLUE)
|
||||
rounded(d, (1078, 725, 1168, 765), 5, "#E8EBF0")
|
||||
ui_text(d, (1020, 746), "确定", 15, "white", anchor="mm")
|
||||
ui_text(d, (1123, 746), "取消", 15, "#3F4651", anchor="mm")
|
||||
return im
|
||||
|
||||
|
||||
def finance_proto():
|
||||
im, d = desktop_base("提现管理", "提现管理")
|
||||
x0 = 280
|
||||
ui_text(d, (x0, 150), "订单号", 15)
|
||||
rounded(d, (350, 132, 560, 170), 4, "white", "#D4DAE3")
|
||||
ui_text(d, (600, 150), "用户名", 15)
|
||||
rounded(d, (670, 132, 880, 170), 4, "white", "#D4DAE3")
|
||||
rounded(d, (900, 132, 980, 170), 4, BLUE)
|
||||
ui_text(d, (940, 151), "搜索", 15, "white", anchor="mm")
|
||||
headers = ["提现用户", "余额", "提现金额", "手续费", "实际到账", "银行", "银行卡号", "发起时间", "处理时间", "操作"]
|
||||
xs = [280, 420, 520, 630, 740, 855, 970, 1120, 1300, 1450, 1570]
|
||||
for i, h in enumerate(headers):
|
||||
d.rectangle((xs[i], 210, xs[i + 1], 258), fill="#405D8E")
|
||||
ui_text(d, ((xs[i] + xs[i + 1]) / 2, 234), h, 12, "white", anchor="mm")
|
||||
rows = [
|
||||
["会员 A", "300.00", "48.38", "0.00", "48.38", "农业银行", "6228 **** **** 7161", "10-07 17:23", "—", "通过 / 驳回"],
|
||||
["会员 B", "2,940.00", "500.00", "2.00", "498.00", "邮储银行", "6217 **** **** 3408", "10-07 16:21", "16:34", "审核通过"],
|
||||
["会员 C", "718.35", "100.00", "1.00", "99.00", "建设银行", "6236 **** **** 5670", "10-07 15:25", "15:48", "审核驳回"],
|
||||
]
|
||||
for r, row in enumerate(rows):
|
||||
y = 258 + r * 62
|
||||
for i, val in enumerate(row):
|
||||
d.rectangle((xs[i], y, xs[i + 1], y + 62), fill="white" if r % 2 == 0 else "#F7F9FC", outline="#DDE2EA")
|
||||
ui_text(d, (xs[i] + 7, y + 32), val, 11, "#2C3440", anchor="lm")
|
||||
rounded(d, (305, 520, 785, 740), 10, "#FFF4F0", "#E98E73")
|
||||
ui_text(d, (335, 558), "P0 资金审核要求", 22, RED, bold=True)
|
||||
for i, line in enumerate(["完整银行卡号仅在授权后临时查看", "通过/驳回必须填写原因", "大额或异常订单双人复核", "幂等、余额校验、审计日志不可缺失"]):
|
||||
ui_text(d, (340, 610 + i * 34), "• " + line, 16, "#523129")
|
||||
rounded(d, (845, 520, 1515, 740), 10, "#EFF6FF", "#7AA2E8")
|
||||
ui_text(d, (875, 558), "审核抽屉", 22, BLUE, bold=True)
|
||||
for i, lab in enumerate(["订单摘要与风险命中", "收款信息(脱敏)", "凭证预览", "处理意见与复核人"]):
|
||||
ui_text(d, (880, 610 + i * 34), "□ " + lab, 16, "#2D466B")
|
||||
return im
|
||||
|
||||
|
||||
def rbac_proto():
|
||||
im, d = desktop_base("角色管理", "角色管理")
|
||||
ui_text(d, (280, 150), "角色与权限矩阵", 22, "#202631", bold=True)
|
||||
headers = ["角色", "会员", "订单", "充值", "提现", "内容", "配置", "账号/权限", "导出"]
|
||||
xs = [280, 470, 590, 710, 830, 950, 1070, 1190, 1400, 1535]
|
||||
for i, h in enumerate(headers):
|
||||
d.rectangle((xs[i], 190, xs[i + 1], 240), fill="#405D8E")
|
||||
ui_text(d, ((xs[i] + xs[i + 1]) / 2, 216), h, 13, "white", anchor="mm")
|
||||
rows = [
|
||||
("超级管理员", ["读写", "读写", "复核", "复核", "发布", "发布", "管理", "授权"]),
|
||||
("运营", ["读", "读", "无", "无", "编辑", "草稿", "无", "无"]),
|
||||
("客服", ["脱敏读", "读", "只读", "只读", "无", "无", "无", "无"]),
|
||||
("财务", ["脱敏读", "读", "审核", "审核", "无", "无", "无", "受控"]),
|
||||
("风控", ["受控查看", "冻结", "复核", "复核", "无", "只读", "无", "受控"]),
|
||||
("审计员", ["只读", "只读", "只读", "只读", "只读", "只读", "只读", "受控"]),
|
||||
]
|
||||
colors = {"无": "#E9EDF3", "只读": "#E8F1FF", "读": "#E8F1FF", "脱敏读": "#E8F1FF", "读写": "#E9F7ED", "编辑": "#E9F7ED", "发布": "#FFF1D8", "审核": "#FFF1D8", "复核": "#FFE4E4", "管理": "#FFE4E4", "授权": "#FFE4E4", "冻结": "#FFE4E4", "受控": "#F2E9FF", "受控查看": "#F2E9FF", "草稿": "#E9F7ED"}
|
||||
for r, (role, vals) in enumerate(rows):
|
||||
y = 240 + r * 62
|
||||
d.rectangle((xs[0], y, xs[1], y + 62), fill="white", outline="#DDE2EA")
|
||||
ui_text(d, (xs[0] + 14, y + 32), role, 14, "#2C3440", anchor="lm")
|
||||
for i, val in enumerate(vals, 1):
|
||||
d.rectangle((xs[i], y, xs[i + 1], y + 62), fill=colors.get(val, "white"), outline="#DDE2EA")
|
||||
ui_text(d, ((xs[i] + xs[i + 1]) / 2, y + 32), val, 12, "#364152", anchor="mm")
|
||||
rounded(d, (280, 665, 1535, 792), 10, "#FFF8E8", "#E1B454")
|
||||
ui_text(d, (310, 705), "关键原则", 20, "#7B5314", bold=True)
|
||||
ui_text(d, (310, 746), "最小权限 · 职责分离 · 敏感字段临时授权 · 权限变更双人复核 · 全量审计", 18, "#7B5314")
|
||||
return im
|
||||
|
||||
|
||||
def flow_proto():
|
||||
im = Image.new("RGB", (1600, 900), "white")
|
||||
d = ImageDraw.Draw(im)
|
||||
ui_text(d, (800, 60), "C 端与后台业务闭环", 32, "#202631", bold=True, anchor="mm")
|
||||
lanes = [("会员", 150, "#EAF1FF"), ("系统", 390, "#EAF8F0"), ("运营/财务", 630, "#FFF4E0")]
|
||||
for name, y, color in lanes:
|
||||
rounded(d, (70, y, 1530, y + 160), 16, color)
|
||||
rounded(d, (90, y + 45, 220, y + 115), 14, "white", "#BCC6D4")
|
||||
ui_text(d, (155, y + 80), name, 21, "#303846", bold=True, anchor="mm")
|
||||
boxes = [
|
||||
("注册/登录", 270, 175, BLUE), ("绑定资料", 500, 175, BLUE), ("领取任务", 730, 175, BLUE), ("提交结果", 960, 175, BLUE), ("查看台账", 1190, 175, BLUE),
|
||||
("身份与风控", 270, 415, GREEN), ("创建订单", 500, 415, GREEN), ("冻结/解冻", 730, 415, GREEN), ("结算与幂等", 960, 415, GREEN), ("通知与申诉", 1190, 415, GREEN),
|
||||
("会员配置", 270, 655, ORANGE), ("内容/商品", 500, 655, ORANGE), ("任务审核", 730, 655, ORANGE), ("充值/提现复核", 960, 655, ORANGE), ("审计/对账", 1190, 655, ORANGE),
|
||||
]
|
||||
for label, x, y, color in boxes:
|
||||
rounded(d, (x, y, x + 175, y + 70), 12, color)
|
||||
ui_text(d, (x + 88, y + 36), label, 16, "white", bold=True, anchor="mm")
|
||||
for y in (210, 450, 690):
|
||||
for x in (450, 680, 910, 1140):
|
||||
ui_text(d, (x, y), "→", 27, "#687386", anchor="mm")
|
||||
for x in (357, 587, 817, 1047, 1277):
|
||||
ui_text(d, (x, 355), "↕", 27, "#687386", anchor="mm")
|
||||
ui_text(d, (x, 595), "↕", 27, "#687386", anchor="mm")
|
||||
return im
|
||||
|
||||
|
||||
def save_assets():
|
||||
ASSET_DIR.mkdir(parents=True, exist_ok=True)
|
||||
assets = {
|
||||
"prototype_01_dashboard.png": dashboard_proto(),
|
||||
"prototype_02_member_list.png": list_proto(
|
||||
"会员列表", "会员列表", ["用户名", "手机号", "注册时间", "状态", "自动派单"],
|
||||
["ID", "账号", "绑卡姓名", "用户名", "余额", "状态", "操作"],
|
||||
[["8165", "155****7676", "王**", "会员 A", "299.00", "正常", "编辑 / 银行卡 / 地址"],
|
||||
["8164", "180****8046", "—", "会员 B", "0.00", "正常", "团队 / 账变 / 奖励金"]],
|
||||
"删除、禁用、设为假人均需二次确认与理由", True,
|
||||
),
|
||||
"prototype_03_member_modal.png": modal_proto(),
|
||||
"prototype_04_withdrawal.png": finance_proto(),
|
||||
"prototype_05_rbac.png": rbac_proto(),
|
||||
"prototype_06_flow.png": flow_proto(),
|
||||
}
|
||||
paths = []
|
||||
for name, image in assets.items():
|
||||
path = ASSET_DIR / name
|
||||
image.save(path, quality=94)
|
||||
paths.append(path)
|
||||
return paths
|
||||
|
||||
|
||||
def configure_doc(doc: Document):
|
||||
sec = doc.sections[0]
|
||||
sec.page_width = Inches(8.5)
|
||||
sec.page_height = Inches(11)
|
||||
sec.top_margin = Inches(0.64)
|
||||
sec.bottom_margin = Inches(0.62)
|
||||
sec.left_margin = Inches(0.68)
|
||||
sec.right_margin = Inches(0.68)
|
||||
normal = doc.styles["Normal"]
|
||||
normal.font.name = "Heiti SC"
|
||||
normal._element.rPr.rFonts.set(qn("w:eastAsia"), "Heiti SC")
|
||||
normal.font.size = Pt(10.2)
|
||||
normal.font.color.rgb = RGBColor.from_string(INK)
|
||||
for name, size in (("Title", 27), ("Heading 1", 17.5), ("Heading 2", 13.5), ("Heading 3", 11.3)):
|
||||
st = doc.styles[name]
|
||||
st.font.name = "Heiti SC"
|
||||
st._element.rPr.rFonts.set(qn("w:eastAsia"), "Heiti SC")
|
||||
st.font.size = Pt(size)
|
||||
st.font.color.rgb = RGBColor(0, 0, 0)
|
||||
st.font.bold = True
|
||||
header = sec.header.paragraphs[0]
|
||||
header.alignment = WD_ALIGN_PARAGRAPH.RIGHT
|
||||
set_run_font(header.add_run("后台管理系统需求文档"), size=8.5, color=MUTED)
|
||||
add_page_number(sec.footer.paragraphs[0])
|
||||
|
||||
|
||||
def add_cover(doc: Document):
|
||||
title = doc.add_paragraph(style="Title")
|
||||
title.alignment = WD_ALIGN_PARAGRAPH.LEFT
|
||||
title.paragraph_format.space_before = Pt(72)
|
||||
title.paragraph_format.space_after = Pt(20)
|
||||
title_pr = title._p.get_or_add_pPr()
|
||||
border = OxmlElement("w:pBdr")
|
||||
for edge in ("top", "left", "bottom", "right", "between"):
|
||||
node = OxmlElement("w:" + edge)
|
||||
node.set(qn("w:val"), "nil")
|
||||
border.append(node)
|
||||
title_pr.append(border)
|
||||
set_run_font(title.add_run("后台管理系统需求文档"), size=27, bold=True, color="000000")
|
||||
sub = doc.add_paragraph()
|
||||
set_run_font(sub.add_run("参考管理端功能拆解、C 端映射与安全重构方案"), size=15.5, bold=True, color="000000")
|
||||
sub.paragraph_format.space_after = Pt(32)
|
||||
p(doc, "目标:基于参考管理系统的实际页面、按钮、链接与弹窗,形成可直接用于产品设计、前后端开发、测试和运维的后台需求基线;保留业务闭环,不复制品牌资产和真实用户数据。")
|
||||
p(doc, "主要读者:产品负责人、交互设计师、前端开发、后端开发、测试、运营、客服、财务、风控、安全与审计。")
|
||||
p(doc, f"观察日期:{date(2026, 10, 7).isoformat()} 参考环境:Web 管理后台 文档版本:V1.0")
|
||||
p(doc, "结论:后台覆盖门户配置、会员、交易、商品、任务、RBAC 与个人设置七大域。复刻时应优先修复敏感数据裸露、资金操作单人审批、配置无校验、接口错误和审计不足。")
|
||||
page_break(doc)
|
||||
|
||||
|
||||
def add_scope(doc):
|
||||
heading(doc, "1 文档范围与走查方法", 1)
|
||||
p(doc, "本需求对后台 21 个可见页面入口进行了逐项走查,并打开新增、编辑、查看、分配、审核等代表性弹窗。为了保护现网数据,没有执行最终确定、上传、删除、禁用、设为假人、审核通过或驳回、资金调整、奖励金发放、权限保存和改密。")
|
||||
heading(doc, "1.1 已覆盖入口", 2)
|
||||
add_table(doc, ["一级模块", "页面", "主要控件"], [
|
||||
("主页", "首页、网站设置、文本管理、Banner 设置、手工商城", "指标卡、保存、上传、编辑、删除、发布"),
|
||||
("会员管理", "会员列表、会员等级、任务大厅", "搜索、新增、编辑、团队、账变、奖励金、等级配置"),
|
||||
("交易管理", "订单、充值、提现、客服", "筛选、自动刷新、匹配、凭证、审核、客服嵌入"),
|
||||
("商城管理", "商品列表", "搜索、新增、编辑、推荐、删除、富文本"),
|
||||
("任务管理", "任务列表、会员任务审核", "任务新增、编辑、通过、驳回"),
|
||||
("管理员", "后台管理员、角色、权限、数据字典", "账号、角色、权限树、配置项"),
|
||||
("设置", "基本资料、修改密码", "资料修改、密码修改"),
|
||||
], [0.95, 2.7, 2.95])
|
||||
heading(doc, "1.2 操作边界", 2)
|
||||
for item in [
|
||||
"只验证页面、按钮和弹窗是否可达,未提交会改变会员、资金、权限或配置的数据。",
|
||||
"删除、禁用、设为假人、审核、账变、奖励金、改密等高风险入口仅记录需求,不执行最终动作。",
|
||||
"原型图基于实测界面脱敏重绘,手机号、姓名、IP、银行卡、订单号均为掩码或虚构值。",
|
||||
"接口名称以推荐实现命名;现网真实接口需在开发前通过代码或服务端文档二次核验。",
|
||||
]:
|
||||
bullet(doc, item)
|
||||
heading(doc, "1.3 证据等级", 2)
|
||||
add_table(doc, ["等级", "定义", "使用方式"], [
|
||||
("A 实测", "页面、按钮、表格或弹窗在走查中直接可见", "可作为复刻基线"),
|
||||
("B 推导", "由相邻页面、C 端闭环或字段关系推导", "开发前需确认"),
|
||||
("C 建议", "为安全、合规、可用性提出的重构要求", "作为新系统强制规范"),
|
||||
], [0.8, 3.3, 2.5])
|
||||
|
||||
|
||||
def add_architecture(doc):
|
||||
heading(doc, "2 产品定位与后台角色", 1)
|
||||
p(doc, "该后台用于运营一个移动端会员任务与账户平台。C 端会员进行注册、余额储入、领取订单、任务提交、佣金结算、提现和客服咨询;后台完成内容配置、会员治理、商品与任务配置、交易审核、财务对账和权限审计。")
|
||||
add_table(doc, ["角色", "职责", "允许范围", "禁止范围"], [
|
||||
("超级管理员", "系统与权限治理", "账号、角色、权限、系统参数", "不得绕过双人复核直接放款"),
|
||||
("运营", "内容、Banner、商品、任务、等级", "草稿、预览、发布申请", "不得查看完整银行卡或改余额"),
|
||||
("客服", "会员与订单查询、工单", "脱敏资料、只读订单、备注", "不得资金审核或导出全量用户"),
|
||||
("财务", "充值、提现、对账", "凭证、审核、支付状态", "不得修改会员身份与权限"),
|
||||
("风控", "异常行为与冻结复核", "设备、IP 风险、冻结申请", "不得单人完成冻结与解冻"),
|
||||
("审计", "只读复核与导出", "日志、审批链、差异报表", "不得修改业务数据"),
|
||||
], [0.9, 1.65, 2.1, 2.25])
|
||||
heading(doc, "2.1 信息架构", 2)
|
||||
p(doc, "采用“一级业务域 + 二级页面 + 多标签工作区”的桌面后台结构。一级菜单固定在左侧,二级页面以标签页打开;标签支持关闭、刷新和回到首页。任何页面刷新后,应保持当前路由但不保留未提交敏感表单。")
|
||||
heading(doc, "2.2 全局交互规范", 2)
|
||||
for item in [
|
||||
"列表页统一:查询区、主操作区、表格、固定操作列、分页;查询条件可重置并写入 URL。",
|
||||
"弹窗统一:标题、关闭、表单、确定、取消;确定前做前后端双重校验,提交后禁止重复点击。",
|
||||
"危险操作使用红色按钮并要求对象、影响、理由和二次确认;资金与权限操作额外要求复核人。",
|
||||
"敏感值默认脱敏;查看完整值需单独权限、再次确认、限定时长,并记录操作者、目标、时间和用途。",
|
||||
"加载超过 2 秒展示骨架与取消,接口失败展示错误码、重试和上报入口,不长期停留空白页。",
|
||||
"操作成功后提供结果提示与审计编号;失败不得只显示“error”。",
|
||||
]:
|
||||
bullet(doc, item)
|
||||
|
||||
|
||||
def add_c_mapping(doc, flow_img):
|
||||
heading(doc, "3 C 端与后台功能映射", 1)
|
||||
p(doc, "后台不是独立工具,其配置和操作必须能解释 C 端每个入口、状态和金额。以下矩阵为联调的单一事实源。")
|
||||
add_table(doc, ["C 端能力", "后台来源", "后台操作", "一致性要求"], [
|
||||
("首页横幅与快捷入口", "Banner、网站设置", "排序、有效期、跳转、开关", "发布后按版本生效"),
|
||||
("平台简介/规则/公告", "文本管理", "富文本编辑、预览、发布", "CSP 与 HTML 白名单"),
|
||||
("手工商城", "手工商城", "商品增删改、图片、价格、时间", "C 端仅显示已上架"),
|
||||
("会员等级", "会员等级", "门槛、赠送、次数、佣金、提现次数", "计算与展示同源"),
|
||||
("领取中心", "网站设置、商品、订单", "营业时间、最低余额、匹配规则", "服务端状态机判定"),
|
||||
("订单记录", "订单列表", "查询、冻结、匹配、异常处理", "业务单号和台账可追溯"),
|
||||
("储入", "充值管理", "凭证、审核、入账、驳回", "幂等入账与自动对账"),
|
||||
("取出", "提现管理、银行卡信息", "收款资料、审核、通道、结果", "金额、手续费、到账同源"),
|
||||
("任务大厅与任务", "任务大厅、任务列表、会员任务审核", "配置、分发、审核", "等级、佣金、状态一致"),
|
||||
("个人资料", "会员列表", "资料、团队、状态、风控", "敏感字段默认脱敏"),
|
||||
("通知与弹幕", "文本、会员弹幕", "发布、定向、撤回、查看记录", "真实性标识和审计"),
|
||||
("客服", "客服管理", "客服入口、工作时间、会话映射", "失败有备用通道"),
|
||||
], [1.2, 1.25, 2.3, 1.8], 7.8)
|
||||
add_picture(doc, flow_img, "图 1 C 端与后台业务闭环(脱敏重绘原型)", width=6.7)
|
||||
|
||||
|
||||
def add_dashboard(doc, img):
|
||||
page_break(doc)
|
||||
heading(doc, "4 首页数据看板", 1)
|
||||
p(doc, "首页展示用户、商品、订单、充值、提现、佣金、利息宝转入、下级返佣与账户余额等汇总卡,并展示微信、支付宝和银行卡渠道统计。新系统应把口径、时间区间、刷新时间和数据延迟显式化。")
|
||||
add_table(doc, ["指标", "定义", "筛选维度", "权限"], [
|
||||
("用户总量/新增", "去重有效会员数", "今日、昨日、自定义日期", "运营只读"),
|
||||
("充值用户", "至少一笔成功充值的会员数", "时间、渠道、等级", "财务/运营"),
|
||||
("订单量与金额", "按订单状态和完成时间统计", "状态、商品、等级", "运营/财务"),
|
||||
("充值/提现", "成功与待处理金额分开", "渠道、状态、时间", "财务"),
|
||||
("佣金/返佣", "任务佣金与层级返佣分开", "类型、等级、时间", "财务/审计"),
|
||||
("账户余额", "钱包可用与冻结余额总和", "币种、账户状态", "财务/审计"),
|
||||
], [1.2, 2.55, 1.75, 1.0])
|
||||
bullet(doc, "卡片可点击进入对应明细页,并继承时间筛选;无权限用户只能看到隐藏金额或不可点击状态。")
|
||||
bullet(doc, "页面顶部展示“数据截至时间”和刷新按钮;自动刷新不得造成重复请求风暴。")
|
||||
add_picture(doc, img, "图 2 后台首页看板(按实测界面脱敏重绘)", width=6.7)
|
||||
|
||||
|
||||
def add_content_modules(doc):
|
||||
heading(doc, "5 门户、内容与 Banner 管理", 1)
|
||||
heading(doc, "5.1 网站设置", 2)
|
||||
add_table(doc, ["配置组", "字段", "要求"], [
|
||||
("品牌", "Logo、站点名称、隐私政策 URL", "Logo 可预览;URL 仅允许 HTTPS 白名单"),
|
||||
("交易", "交易余额、等待时间、每日次数、奖励次数", "数值范围校验;修改按版本发布"),
|
||||
("佣金", "0–5 级普通佣金", "Decimal;总和上限;法务开关"),
|
||||
("时间", "提现、充值、交易、签到时间", "支持多区间;禁止结束早于开始"),
|
||||
("门槛", "最低充值、自动派单时间与金额", "最小值≤最大值;币种与单位清晰"),
|
||||
("开关", "免费领取、手工商城、首页任务、签到、商品模式、验证码", "每项写明 C 端影响和降级文案"),
|
||||
], [0.85, 2.65, 3.1])
|
||||
p(doc, "实测问题:交易等待时间输入框提示语与字段无关;签到时段出现“18:00–17:00”反向区间。新系统保存前必须做语义校验,并支持草稿、预览、发布、回滚。")
|
||||
heading(doc, "5.2 文本管理", 2)
|
||||
p(doc, "内容类型包括平台公告、平台简介、订单规则、代理合作、FAQ、指南、利息宝规则、首页滚动、订单提示、活动、首页弹窗和公司资质。编辑器需支持标题、正文、图片、链接、预览、定时发布、版本和回滚。")
|
||||
bullet(doc, "富文本使用标签/属性白名单;禁止 script、事件属性、javascript: URL 和未知 iframe。")
|
||||
bullet(doc, "现网编辑弹窗标题错误显示“编辑商品”,新系统标题必须与内容类型一致。")
|
||||
heading(doc, "5.3 Banner 设置", 2)
|
||||
p(doc, "支持多张首页 Banner 的上传、删除、排序、上下线、有效期、跳转类型与替代文本。上传前预览并校验尺寸、比例、MIME、文件大小和恶意内容。删除需要二次确认,已发布素材优先下线而非物理删除。")
|
||||
heading(doc, "5.4 手工商城", 2)
|
||||
p(doc, "列表字段为图片、名称、价格、时间与操作;新增/编辑字段为产品名称、价格、图片与展示时间。现网新增弹窗曾长时间停留 loading,需增加 8 秒超时、重试和错误上报。")
|
||||
|
||||
|
||||
def add_member(doc, list_img, modal_img):
|
||||
page_break(doc)
|
||||
heading(doc, "6 会员管理", 1)
|
||||
heading(doc, "6.1 会员列表", 2)
|
||||
p(doc, "查询条件:用户名、手机号、注册时间、状态、自动派单。列表字段:ID、账号、绑卡姓名、用户名、账户余额、冻结金额、上级用户、邀请码、注册时间、最后登录 IP、IP 属地、自动抢单、状态、当日交易次数。")
|
||||
add_table(doc, ["操作", "页面或弹窗", "关键校验", "审计要求"], [
|
||||
("添加/编辑", "会员表单", "手机号唯一;余额不可由普通编辑修改;密码强度", "记录前后值,密码永不回显"),
|
||||
("银行卡信息", "开户人、银行、电话、卡号、数字钱包、云闪付、收款码", "必要性与格式;图片扫描", "完整值查看留痕"),
|
||||
("地址信息", "姓名、电话、地区、详细地址", "最小化收集;字段脱敏", "查看和修改留痕"),
|
||||
("查看团队", "当前用户、层级、人数、余额、佣金、充值、提现", "仅展示授权层级", "导出单独授权"),
|
||||
("账变", "余额调整或流水", "金额、类型、理由、业务单号", "双人复核;不可直接改余额"),
|
||||
("弹幕", "会员定向或模拟内容", "标注来源、有效期、撤回", "禁止伪造真实用户行为"),
|
||||
("奖励金", "奖励金额与原因", "非负、额度、幂等", "财务复核与凭证"),
|
||||
("设为假人", "账号标记", "不得混入真实统计或 C 端荣誉榜", "仅测试环境默认启用"),
|
||||
("禁用", "账号状态", "理由、范围、时长、申诉", "双人复核;通知用户"),
|
||||
("删除", "软删除/注销", "检查资金与订单;不可物理删台账", "高风险二次确认"),
|
||||
], [0.85, 2.0, 2.3, 1.8], 7.6)
|
||||
p(doc, "P0:现网页面默认显示完整手机号、姓名与登录 IP。新系统必须默认脱敏,按角色和目的临时授权查看;列表、导出、日志、埋点和截图统一执行同一脱敏规则。")
|
||||
add_picture(doc, list_img, "图 3 会员列表脱敏原型", width=6.7)
|
||||
add_picture(doc, modal_img, "图 4 添加会员弹窗原型", width=6.7, page_break_before=True)
|
||||
|
||||
|
||||
def add_member_level_taskhall(doc):
|
||||
page_break(doc)
|
||||
heading(doc, "7 会员等级与任务大厅", 1)
|
||||
heading(doc, "7.1 会员等级", 2)
|
||||
p(doc, "列表字段为等级名称、升级价格、赠送金额、接单次数、佣金比例和提现次数;支持编辑与受控删除。等级配置直接影响 C 端充值、领取和提现,必须使用版本化发布,不得修改后立即影响进行中的订单。")
|
||||
add_table(doc, ["规则", "校验", "生效策略"], [
|
||||
("升级价格/赠送金额", "≥0;货币精度;赠送比例上限", "新会员或下个结算周期"),
|
||||
("接单次数", "整数;按日重置时区明确", "发布后次日生效"),
|
||||
("佣金比例", "0–1 Decimal;展示百分比", "锁定到任务订单快照"),
|
||||
("提现次数", "整数;与提现时间窗一致", "发布后次日生效"),
|
||||
("删除等级", "无有效会员与在途订单方可删除", "优先停用并保留历史"),
|
||||
], [1.45, 2.65, 2.5])
|
||||
heading(doc, "7.2 任务大厅", 2)
|
||||
p(doc, "页面字段为名称、图标、比例、简介、创建时间;新增弹窗还包含会员等级。实测列表接口直接显示“数据接口请求异常:error”,但新增弹窗可打开。新系统需要标准错误码、可重试状态、traceId 与空数据区分。")
|
||||
bullet(doc, "任务大厅条目必须绑定可用会员等级、比例单位、图标和 C 端跳转。")
|
||||
bullet(doc, "停用条目不影响已领取任务;删除仅允许无历史引用的数据,建议统一软删除。")
|
||||
|
||||
|
||||
def add_transactions(doc, finance_img):
|
||||
page_break(doc)
|
||||
heading(doc, "8 交易管理", 1)
|
||||
heading(doc, "8.1 订单列表", 2)
|
||||
p(doc, "查询条件:订单号、手机号、用户名、日期;支持开启/关闭自动刷新。列表含绑卡姓名、用户名、自动派单、余额、手机号、当日交易次数、商品、数量、金额、佣金、下单时间、解冻时间、消息、订单状态、冻结状态和操作。")
|
||||
add_table(doc, ["状态", "后台操作", "约束"], [
|
||||
("匹配中", "查看进度、取消匹配", "同一会员仅一笔匹配;需幂等"),
|
||||
("待处理", "查看详情、发送提醒", "不可手动完成;服务端倒计时"),
|
||||
("冻结", "查看风险、发起解冻复核", "冻结理由、时长、申诉路径"),
|
||||
("已完成", "查看台账", "金额、佣金、解冻三方一致"),
|
||||
("异常/失败", "重新匹配、人工处理申请", "人工改状态必须双人审批"),
|
||||
], [1.0, 2.2, 3.35])
|
||||
heading(doc, "8.2 充值管理", 2)
|
||||
p(doc, "顶部展示全部、用户已支付、用户未支付、手动审核、手动驳回等汇总金额。列表字段:订单号、真实姓名、手机号、余额、交易金额、打款凭证、添加时间、支付方式、类型、处理时间、备注和操作。")
|
||||
bullet(doc, "审核通过前校验凭证、订单金额、收款主体、重复订单和账户状态;入账使用幂等键。")
|
||||
bullet(doc, "驳回必须选择标准原因并可补充说明;通知 C 端且允许补充材料。")
|
||||
heading(doc, "8.3 提现管理", 2)
|
||||
p(doc, "查询条件:订单号、用户名、日期。列表含用户余额、提现金额、手续费、实际到账、银行名称、开户名称、银行卡号、云闪付、数字钱包、支付宝/微信收款码、发起与处理时间、操作。")
|
||||
p(doc, "P0:现网直接显示完整银行卡号与多个钱包账号。新系统列表只显示掩码,完整值不进入普通 DOM;付款处理时通过一次性授权查看,超时自动隐藏。")
|
||||
add_picture(doc, finance_img, "图 5 提现审核脱敏原型", width=6.7)
|
||||
heading(doc, "8.4 客服管理", 2)
|
||||
p(doc, "实测页面在加载后保持空白。新系统至少提供客服账号、名称、工作时间、在线状态、跳转 URL、排序、启停和会话映射;第三方 iframe 失败时显示备用联系方式和错误编号。")
|
||||
|
||||
|
||||
def add_products_tasks(doc):
|
||||
page_break(doc)
|
||||
heading(doc, "9 商品与任务管理", 1)
|
||||
heading(doc, "9.1 商品列表", 2)
|
||||
p(doc, "查询条件:商品名称、支付类型。列表:ID、图片、名称、商品价格、店铺名称、创建时间、是否推荐、操作。新增/编辑字段:商品名称、分类、店铺名称、商品单价、展示图片、商品详情富文本。")
|
||||
bullet(doc, "推荐/不推荐为明确状态切换,提交前展示影响范围;列表筛选和 C 端排序一致。")
|
||||
bullet(doc, "价格采用 Decimal;图片与富文本执行内容安全校验;商品被订单引用后只允许停用。")
|
||||
heading(doc, "9.2 任务列表", 2)
|
||||
p(doc, "按会员类型筛选;字段为任务编号、封面、标题、描述、等级、佣金、创建时间。新增弹窗含标题、描述、封面、等级、佣金和详情。实测新增任务弹窗标题误用“添加大厅任务”,需修正。")
|
||||
heading(doc, "9.3 会员任务审核", 2)
|
||||
p(doc, "按手机号、用户名查询;列表含记录 ID、用户名、手机、任务标题、等级、佣金、成功率、状态、备注、创建时间和操作。待审核记录支持通过与驳回。")
|
||||
add_table(doc, ["动作", "前置条件", "提交内容", "结果"], [
|
||||
("通过", "任务存在、会员有效、未重复审核", "审核意见、证据摘要、幂等键", "发放佣金或进入结算队列"),
|
||||
("驳回", "记录为待审核", "标准原因、补充说明、是否允许重提", "通知会员并保留证据"),
|
||||
("撤回审核", "仅异常且未结算", "变更单与复核人", "冲正而非修改历史记录"),
|
||||
], [0.9, 2.35, 2.2, 2.0])
|
||||
|
||||
|
||||
def add_rbac(doc, rbac_img):
|
||||
page_break(doc)
|
||||
heading(doc, "10 管理员、角色与权限", 1)
|
||||
heading(doc, "10.1 后台管理员", 2)
|
||||
p(doc, "查询条件为登录名、手机、邮箱、角色;列表含姓名、登录名、手机号、邮箱、角色、更新时间和创建时间。新增字段为姓名、用户名、密码、手机、邮箱和多选角色。")
|
||||
bullet(doc, "密码强度至少 12 位,禁止默认密码,首次登录强制改密;密码永不回显。")
|
||||
bullet(doc, "禁用优先于删除;删除账号时保留历史审计主体,不得影响日志可追溯性。")
|
||||
heading(doc, "10.2 角色管理", 2)
|
||||
p(doc, "角色列表支持新增、编辑、删除和分配权限。分配弹窗为可展开/折叠的权限树。保存权限属于高风险操作,必须展示新增与移除差异,并由另一名管理员复核。")
|
||||
heading(doc, "10.3 权限管理", 2)
|
||||
p(doc, "权限树字段为 ID、名称、权限标识、父级 ID 和操作;支持添加顶级权限、添加子级、编辑、删除、展开/折叠。权限标识唯一且不可随意改名;被角色引用时禁止删除。")
|
||||
add_picture(doc, rbac_img, "图 6 建议 RBAC 权限矩阵", width=6.7)
|
||||
|
||||
|
||||
def add_dictionary_settings(doc):
|
||||
page_break(doc)
|
||||
heading(doc, "11 数据字典与个人设置", 1)
|
||||
heading(doc, "11.1 数据字典", 2)
|
||||
p(doc, "列表字段为 ID、名称、值、创建时间;支持新增、编辑、删除。现网同时存在英文 key、中文名称和缺失名称的条目,并与网站设置重复维护。建议将字典拆为“系统常量”和“业务配置”,以结构化 Schema 管理。")
|
||||
add_table(doc, ["字段", "要求"], [
|
||||
("key", "全局唯一;小写或大写规范统一;发布后不可改名"),
|
||||
("value", "按 Schema 校验类型、范围、枚举、时间段和单位"),
|
||||
("description", "必填,说明业务含义、C 端影响和默认值"),
|
||||
("scope", "环境、租户、渠道、会员等级等作用域"),
|
||||
("version", "草稿、已发布、已回滚;记录生效时间"),
|
||||
("secret", "敏感配置不得明文显示,使用密钥管理系统"),
|
||||
], [1.3, 5.2])
|
||||
heading(doc, "11.2 基本资料", 2)
|
||||
p(doc, "字段为用户名(不可修改)、名称、手机、邮箱,提交按钮为“确认修改”。修改手机或邮箱需验证当前身份和新联系方式;成功后记录审计并通知原联系方式。")
|
||||
heading(doc, "11.3 修改密码", 2)
|
||||
p(doc, "字段为当前密码、新密码、确认新密码,现网提示 6–16 个字符。新系统建议至少 12 位并支持密码管理器;最终提交必须由用户本人完成,成功后撤销其他会话并发送安全通知。")
|
||||
|
||||
|
||||
def add_state_models(doc):
|
||||
page_break(doc)
|
||||
heading(doc, "12 状态机与数据模型", 1)
|
||||
heading(doc, "12.1 核心状态机", 2)
|
||||
add_table(doc, ["实体", "状态", "允许转换"], [
|
||||
("会员", "正常、受限、禁用、注销", "正常→受限/禁用;复核后恢复;注销不可逆"),
|
||||
("订单", "匹配中、待处理、处理中、完成、超时、冻结、取消、失败", "仅服务端状态机转换"),
|
||||
("充值", "待支付、待审核、成功、驳回、过期、取消", "成功后只允许冲正"),
|
||||
("提现", "待审核、复核中、处理中、成功、失败、驳回、取消", "付款前可取消;成功后只允许冲正"),
|
||||
("任务审核", "待审核、通过、驳回、已结算、已冲正", "结算后不可回到待审核"),
|
||||
("内容", "草稿、待审、已发布、已下线、已归档", "发布需审批;可回滚版本"),
|
||||
("配置", "草稿、待发布、已发布、已回滚", "发布后生成不可变版本"),
|
||||
], [1.0, 2.45, 3.1])
|
||||
heading(doc, "12.2 核心实体", 2)
|
||||
add_table(doc, ["实体", "关键字段", "约束"], [
|
||||
("AdminUser", "id, loginName, roles, status, mfa, lastLogin", "管理员身份与会话"),
|
||||
("Role/Permission", "roleId, permissionKey, scope", "最小权限与数据范围"),
|
||||
("Member", "id, mobileEnc, nameEnc, levelId, status", "敏感字段加密、索引分离"),
|
||||
("Wallet", "available, frozen, currency, version", "只允许台账事务修改"),
|
||||
("LedgerEntry", "bizNo, type, amount, before, after, reversalOf", "不可变、幂等、可冲正"),
|
||||
("Order", "orderNo, memberId, productSnapshot, amount, commission, status", "保存规则快照"),
|
||||
("Deposit/Withdrawal", "bizNo, amount, fee, channel, status, reviewer", "审批链与支付回执"),
|
||||
("Content/Banner", "type, body, version, publishAt, operator", "预览与版本化发布"),
|
||||
("ConfigVersion", "key, value, schema, effectiveAt", "语义校验与回滚"),
|
||||
("AuditLog", "actor, action, target, before, after, reason, traceId", "防篡改与长期留存"),
|
||||
], [1.15, 3.5, 2.0], 7.8)
|
||||
|
||||
|
||||
def add_api(doc):
|
||||
page_break(doc)
|
||||
heading(doc, "13 推荐接口清单", 1)
|
||||
p(doc, "接口为新系统建议设计,统一前缀 /admin-api/v1。响应包含 data、code、message、traceId;所有写接口必须携带 Idempotency-Key,资金与权限写操作同时携带审批单号。")
|
||||
rows = [
|
||||
("GET", "/dashboard/summary", "首页指标", "timeRange, dimensions"),
|
||||
("GET/PUT", "/site-settings", "读取/保存站点配置草稿", "version, settings"),
|
||||
("POST", "/site-settings/publish", "发布配置", "draftVersion, reason"),
|
||||
("GET/POST", "/contents", "内容列表/新建", "type, status, body"),
|
||||
("POST", "/contents/{id}/publish", "内容发布", "publishAt, approver"),
|
||||
("GET/POST", "/banners", "Banner 查询/新建", "imageAssetId, sort, target"),
|
||||
("GET/POST", "/members", "会员查询/新建", "filters, profile"),
|
||||
("GET/PATCH", "/members/{id}", "会员详情/编辑", "editableProfile, version"),
|
||||
("POST", "/members/{id}/adjustment-requests", "账变申请", "type, amount, reason, evidence"),
|
||||
("POST", "/members/{id}/status-requests", "禁用/解禁申请", "targetStatus, reason, duration"),
|
||||
("GET/POST", "/member-levels", "等级查询/草稿", "rules, effectiveAt"),
|
||||
("GET/POST", "/products", "商品查询/新建", "category, price, assets, detail"),
|
||||
("GET/POST", "/tasks", "任务查询/新建", "levelIds, commission, detail"),
|
||||
("GET", "/orders", "订单列表", "orderNo, member, status, timeRange"),
|
||||
("POST", "/orders/{id}/exception-requests", "异常处理申请", "action, reason"),
|
||||
("GET", "/deposits", "充值列表", "status, channel, timeRange"),
|
||||
("POST", "/deposits/{id}/review", "充值审核", "decision, reason, version"),
|
||||
("GET", "/withdrawals", "提现列表", "status, member, timeRange"),
|
||||
("POST", "/withdrawals/{id}/review", "提现审核", "decision, reason, version"),
|
||||
("GET", "/withdrawals/{id}/sensitive-view", "一次性查看收款信息", "purpose, challengeToken"),
|
||||
("GET/POST", "/task-submissions", "任务审核列表/决定", "decision, reason"),
|
||||
("GET/POST", "/admin-users", "管理员查询/新建", "profile, roleIds"),
|
||||
("GET/POST", "/roles", "角色查询/新建", "name, scope"),
|
||||
("POST", "/roles/{id}/permission-change-requests", "权限变更申请", "add, remove, reason"),
|
||||
("GET", "/audit-logs", "审计查询", "actor, action, target, timeRange"),
|
||||
]
|
||||
add_table(doc, ["方法", "路径", "用途", "关键参数"], rows, [0.55, 2.3, 1.75, 2.0], 7.4)
|
||||
|
||||
|
||||
def add_security(doc):
|
||||
page_break(doc)
|
||||
heading(doc, "14 安全、隐私与审计", 1)
|
||||
add_table(doc, ["风险", "现网表现", "新系统要求", "优先级"], [
|
||||
("个人信息裸露", "完整手机号、姓名、IP", "默认脱敏;临时授权;全链路审计", "P0"),
|
||||
("金融信息裸露", "完整银行卡与钱包账号", "应用层加密;非 DOM 展示;一次性查看", "P0"),
|
||||
("资金单人操作", "通过/驳回、账变、奖励入口直接可达", "职责分离、双人复核、额度和幂等", "P0"),
|
||||
("伪造行为", "设为假人、弹幕入口", "测试数据隔离;禁止混入真实榜单与统计", "P0"),
|
||||
("富文本 XSS", "商品与内容支持富文本", "白名单、CSP、上传扫描、预览隔离", "P0"),
|
||||
("配置误发布", "时间反向、字段提示错误", "Schema 校验、预览、审批、回滚", "P0"),
|
||||
("接口不可用", "任务大厅 error、客服空白", "错误码、重试、熔断、监控和降级", "P1"),
|
||||
("权限漂移", "多角色与权限树直接保存", "差异预览、审批、定期复核", "P0"),
|
||||
], [1.0, 1.65, 3.0, 0.65])
|
||||
heading(doc, "14.1 审计日志", 2)
|
||||
for item in [
|
||||
"记录登录、查看敏感信息、导出、查询大范围数据、资金审核、账变、奖励、冻结、配置发布、权限变更和改密。",
|
||||
"字段至少包含操作者、角色、MFA、目标、动作、前值、后值、理由、审批人、IP、设备、traceId、时间和结果。",
|
||||
"日志写入独立存储并防篡改;业务管理员无删除权限;按监管与公司政策设定留存期。",
|
||||
"导出文件水印、加密、限时下载;下载与转发均可追踪。",
|
||||
]:
|
||||
bullet(doc, item)
|
||||
heading(doc, "14.2 登录与会话", 2)
|
||||
for item in [
|
||||
"管理员必须启用 MFA;高风险动作触发再次认证。",
|
||||
"闲置 15–30 分钟自动锁定;会话撤销、设备列表和异地登录提醒。",
|
||||
"连续失败触发限流与告警;不得依赖前端验证码作为唯一防护。",
|
||||
"服务端密码使用 Argon2id 或 bcrypt;任何日志、监控和前端状态都不得保存密码。",
|
||||
]:
|
||||
bullet(doc, item)
|
||||
|
||||
|
||||
def add_nonfunctional(doc):
|
||||
heading(doc, "15 非功能需求", 1)
|
||||
add_table(doc, ["类别", "目标", "验收指标"], [
|
||||
("性能", "常用列表与详情快速可用", "列表 P95<1s;写操作 P95<1.5s;首屏<2.5s"),
|
||||
("可用性", "关键交易全年可用", "资金审核 99.95%;其余 99.9%"),
|
||||
("容量", "支持 10 万会员与百万级订单", "游标分页、索引与冷热分层"),
|
||||
("并发", "自动刷新与多运营并发安全", "乐观锁、版本号、冲突提示"),
|
||||
("可观测", "定位每次请求与资金变更", "traceId、指标、日志、告警、审计关联"),
|
||||
("兼容", "主流桌面浏览器", "Chrome/Edge 最近两个稳定版;1366×768 可用"),
|
||||
("可访问", "键盘与读屏可操作", "焦点、标签、颜色对比、44px 点击区"),
|
||||
("灾备", "配置与交易可恢复", "RPO≤5 分钟,RTO≤30 分钟;季度演练"),
|
||||
], [1.0, 3.0, 2.65])
|
||||
heading(doc, "15.1 列表与弹窗通用验收", 2)
|
||||
for item in [
|
||||
"查询条件、分页、排序、刷新后可恢复;空数据、加载中、失败、无权限均有独立状态。",
|
||||
"横向宽表固定关键列和操作列;敏感字段不因横向滚动或 tooltip 意外暴露。",
|
||||
"弹窗关闭与取消不产生写操作;表单脏数据关闭前提示;成功后只刷新受影响区域。",
|
||||
"重复点击、网络重试和浏览器回退不会重复创建、重复审核或重复入账。",
|
||||
"所有时间显示时区,金额显示币种和精度,百分比与小数含义统一。",
|
||||
]:
|
||||
bullet(doc, item)
|
||||
|
||||
|
||||
def add_acceptance(doc):
|
||||
page_break(doc)
|
||||
heading(doc, "16 端到端验收场景", 1)
|
||||
scenarios = [
|
||||
("A01 内容发布", "运营新建公告→预览→提交审核→审批→定时发布→C 端显示相同版本→回滚"),
|
||||
("A02 会员查询", "客服按掩码手机号查询→查看脱敏资料→申请查看完整值→审计留痕→超时隐藏"),
|
||||
("A03 任务闭环", "配置任务→C 端领取→生成订单→提交→后台审核→佣金入账→台账核对"),
|
||||
("A04 充值审核", "C 端提交→后台待审核→重复回调→单次入账→通知→对账一致"),
|
||||
("A05 提现复核", "C 端申请→风控命中→财务初审→复核→通道付款→状态与到账金额一致"),
|
||||
("A06 禁用会员", "客服发起→填写理由和时长→风控复核→会员受限→通知与申诉→解禁"),
|
||||
("A07 账变", "运营申请奖励→财务复核→台账入账→重复提交被幂等拦截→审计可追溯"),
|
||||
("A08 权限变更", "管理员申请新增提现审核权限→展示差异→复核→即时生效→历史会话重新鉴权"),
|
||||
("A09 配置错误", "输入反向时间/非法比例→前后端阻止→错误定位到字段→不生成草稿版本"),
|
||||
("A10 服务降级", "任务大厅 API 失败/客服 iframe 失败→清晰错误→重试/备用入口→告警产生"),
|
||||
]
|
||||
add_table(doc, ["编号", "场景与验收路径"], scenarios, [0.75, 5.9])
|
||||
heading(doc, "16.1 发布门槛", 2)
|
||||
for item in [
|
||||
"P0 风险全部关闭;资金、权限、隐私与上传通过专项安全测试。",
|
||||
"C 端和后台使用同一配置版本、状态枚举、金额口径和错误码。",
|
||||
"关键写操作完成幂等、并发、回滚、重试、超时与断网测试。",
|
||||
"审计日志可以从一次用户投诉还原完整操作者、审批链和资金流水。",
|
||||
"备份恢复、支付对账、权限复核和事故响应完成演练。",
|
||||
]:
|
||||
bullet(doc, item)
|
||||
|
||||
|
||||
def add_roadmap(doc):
|
||||
page_break(doc)
|
||||
heading(doc, "17 研发里程碑", 1)
|
||||
add_table(doc, ["阶段", "范围", "退出条件"], [
|
||||
("M0 设计基线", "信息架构、组件、状态机、RBAC、字段字典", "产品/设计/技术/安全评审通过"),
|
||||
("M1 基础后台", "登录、管理员、角色、权限、审计、内容、Banner", "MFA、审计、发布流可用"),
|
||||
("M2 会员与配置", "会员、等级、团队、配置中心", "脱敏、临时授权、版本发布完成"),
|
||||
("M3 商品与任务", "商品、任务、任务大厅、审核", "C 端联调与佣金快照通过"),
|
||||
("M4 交易与资金", "订单、充值、提现、对账、复核", "支付沙箱、幂等、双人复核通过"),
|
||||
("M5 运营与稳定性", "看板、客服、监控、灾备、性能", "SLO、告警、恢复演练通过"),
|
||||
], [1.1, 3.4, 2.25])
|
||||
heading(doc, "17.1 建议开发顺序", 2)
|
||||
for item in [
|
||||
"先建设统一认证、RBAC、审计、脱敏和配置发布,再开发具体业务页。",
|
||||
"会员、商品、任务可先使用模拟资金;充值和提现必须最后接真实通道。",
|
||||
"资金域上线前实行沙箱、小流量、额度限制和人工对账。",
|
||||
"上线后第一周每日权限复核、账务核对和错误率复盘。",
|
||||
]:
|
||||
numbered(doc, item)
|
||||
|
||||
|
||||
def add_prototypes(doc, imgs):
|
||||
page_break(doc)
|
||||
heading(doc, "18 原型图说明", 1)
|
||||
p(doc, "文档中的界面图按实测后台的布局、菜单、表格、弹窗和业务字段脱敏重绘,可直接用于讨论信息层级和交互。它们不是视觉稿:品牌、颜色、图标和真实数据应在 UI 设计阶段重新建立。")
|
||||
for idx, (img, caption) in enumerate([
|
||||
(imgs[0], "首页看板"), (imgs[1], "会员列表"), (imgs[2], "会员弹窗"),
|
||||
(imgs[3], "提现审核"), (imgs[4], "角色权限"), (imgs[5], "业务闭环"),
|
||||
], 1):
|
||||
if idx > 1:
|
||||
page_break(doc)
|
||||
heading(doc, f"18.{idx} {caption}(脱敏重绘)", 2)
|
||||
pic = doc.add_paragraph()
|
||||
pic.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
pic.add_run().add_picture(str(img), width=Inches(5.8))
|
||||
|
||||
|
||||
def add_open_questions(doc):
|
||||
page_break(doc)
|
||||
heading(doc, "19 待确认事项", 1)
|
||||
for item in [
|
||||
"目标系统是否处理真实资金,适用司法辖区、支付牌照与实名要求是什么。",
|
||||
"“设为假人、弹幕、奖励金、层级返佣”是否保留;若保留,业务合法性和真实性如何证明。",
|
||||
"充值、提现、账变和会员禁用的初审/复核角色与金额阈值。",
|
||||
"C 端现有接口能否修改,还是需要后台兼容旧字段和状态枚举。",
|
||||
"客服使用自建工单还是第三方服务,允许向第三方传输哪些数据。",
|
||||
"运营内容是否需要多语言、定时发布、区域差异与灰度发布。",
|
||||
"历史用户、订单、银行卡和台账的迁移范围、清洗规则和保留年限。",
|
||||
"生产环境数据导出、敏感值查看和紧急权限的审批人。",
|
||||
]:
|
||||
bullet(doc, item)
|
||||
p(doc, "在上述事项确认前,可以完成认证、RBAC、审计、内容、会员脱敏查询、商品任务配置和沙箱交易;不得接入真实收款、放款或自动资金调整。")
|
||||
|
||||
|
||||
def build_doc():
|
||||
OUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
imgs = save_assets()
|
||||
doc = Document()
|
||||
configure_doc(doc)
|
||||
add_cover(doc)
|
||||
add_scope(doc)
|
||||
add_architecture(doc)
|
||||
add_c_mapping(doc, imgs[5])
|
||||
add_dashboard(doc, imgs[0])
|
||||
add_content_modules(doc)
|
||||
add_member(doc, imgs[1], imgs[2])
|
||||
add_member_level_taskhall(doc)
|
||||
add_transactions(doc, imgs[3])
|
||||
add_products_tasks(doc)
|
||||
add_rbac(doc, imgs[4])
|
||||
add_dictionary_settings(doc)
|
||||
add_state_models(doc)
|
||||
add_api(doc)
|
||||
add_security(doc)
|
||||
add_nonfunctional(doc)
|
||||
add_acceptance(doc)
|
||||
add_roadmap(doc)
|
||||
add_prototypes(doc, imgs)
|
||||
add_open_questions(doc)
|
||||
doc.core_properties.title = "后台管理系统需求文档"
|
||||
doc.core_properties.subject = "参考管理系统拆解、C 端映射与安全重构方案"
|
||||
doc.core_properties.author = "Product Requirement Analysis"
|
||||
doc.core_properties.keywords = "后台, 需求文档, 会员, 交易, RBAC, 审计, 原型"
|
||||
doc.save(OUT_DOCX)
|
||||
print(OUT_DOCX)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
build_doc()
|
||||
@@ -0,0 +1,794 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from datetime import date
|
||||
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
from docx import Document
|
||||
from docx.enum.section import WD_SECTION
|
||||
from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_BREAK, WD_LINE_SPACING
|
||||
from docx.enum.table import WD_TABLE_ALIGNMENT, WD_CELL_VERTICAL_ALIGNMENT
|
||||
from docx.oxml import OxmlElement
|
||||
from docx.oxml.ns import qn
|
||||
from docx.shared import Inches, Pt, RGBColor
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
OUT_DIR = ROOT / "docs" / "reference_site_prd"
|
||||
ASSET_DIR = OUT_DIR / "assets"
|
||||
OUT_DOCX = OUT_DIR / "移动端任务与账户平台需求文档.docx"
|
||||
FONT_PATH = "/System/Library/Fonts/STHeiti Light.ttc"
|
||||
|
||||
INK = "18202F"
|
||||
BLUE = "4F79E8"
|
||||
PALE_BLUE = "EAF1FF"
|
||||
PALE = "F6F8FC"
|
||||
GOLD = "D8AD58"
|
||||
RED = "C64F48"
|
||||
GRID = "D9D9D9"
|
||||
MUTED = "667085"
|
||||
|
||||
|
||||
def font(size: int, bold: bool = False):
|
||||
try:
|
||||
return ImageFont.truetype(FONT_PATH, size=size)
|
||||
except Exception:
|
||||
return ImageFont.load_default()
|
||||
|
||||
|
||||
def rounded(draw, box, radius=18, fill="white", outline=None, width=1):
|
||||
draw.rounded_rectangle(box, radius=radius, fill=fill, outline=outline, width=width)
|
||||
|
||||
|
||||
def text(draw, xy, value, size=24, fill="#18202F", bold=False, anchor=None):
|
||||
draw.text(xy, value, font=font(size, bold), fill=fill, anchor=anchor)
|
||||
|
||||
|
||||
def phone_base(title: str):
|
||||
im = Image.new("RGB", (460, 860), "#F3F5FA")
|
||||
d = ImageDraw.Draw(im)
|
||||
rounded(d, (8, 8, 452, 852), 34, "#F3F5FA", "#D8DEE9", 2)
|
||||
text(d, (230, 42), title, 28, anchor="mm")
|
||||
return im, d
|
||||
|
||||
|
||||
def nav(d, active: str):
|
||||
rounded(d, (14, 790, 446, 842), 24, "white")
|
||||
items = ["首页", "记录", "领取", "客服", "我的"]
|
||||
for i, item in enumerate(items):
|
||||
x = 52 + i * 89
|
||||
color = "#4F79E8" if item == active else "#4B5565"
|
||||
d.ellipse((x - 9, 802, x + 9, 820), fill=color)
|
||||
text(d, (x, 831), item, 15, color, anchor="mm")
|
||||
|
||||
|
||||
def home_proto():
|
||||
im, d = phone_base("首页")
|
||||
rounded(d, (20, 78, 440, 220), 18, "#2459D9")
|
||||
text(d, (230, 145), "运营活动横幅", 34, "white", anchor="mm")
|
||||
for i, label in enumerate(["领取", "储入", "取出", "手工"]):
|
||||
x = 65 + i * 110
|
||||
d.ellipse((x - 22, 238, x + 22, 282), fill=["#6C8FF1", "#6DD0B5", "#D77B6C", "#E2B362"][i])
|
||||
text(d, (x, 301), label, 19, anchor="mm")
|
||||
rounded(d, (20, 322, 440, 422), 28, "#DCEBFF")
|
||||
text(d, (42, 347), "账户余额", 18, "#667085")
|
||||
text(d, (42, 395), "Rmb$ 300,019.13", 32)
|
||||
text(d, (20, 455), "荣誉榜", 23)
|
||||
for i in range(3):
|
||||
y = 486 + i * 68
|
||||
rounded(d, (20, y, 440, y + 56), 20, "white")
|
||||
d.ellipse((34, y + 9, 72, y + 47), fill="#DDE5F5")
|
||||
text(d, (88, y + 19), f"用户{i+1} 积分 {5200-i*430}.00", 16)
|
||||
rounded(d, (340, y + 12, 425, y + 44), 16, "#E3BB6F")
|
||||
text(d, (382, y + 29), "领取成功", 13, anchor="mm")
|
||||
text(d, (20, 705), "会员等级 · 公司简介", 20)
|
||||
nav(d, "首页")
|
||||
return im
|
||||
|
||||
|
||||
def records_proto():
|
||||
im, d = phone_base("记录")
|
||||
text(d, (28, 82), "剩余可用资产", 17, "#667085")
|
||||
text(d, (28, 126), "Rmb$ 300,019.13", 34)
|
||||
for i, tab in enumerate(["全部", "待处理", "已完成"]):
|
||||
text(d, (55 + i * 92, 184), tab, 18, "#18202F" if i == 0 else "#77808F", anchor="mm")
|
||||
for i, (name, price, comm) in enumerate([("商品订单 A", "¥298", "¥14.90"), ("商品订单 B", "¥235", "¥4.23")]):
|
||||
y = 220 + i * 190
|
||||
rounded(d, (18, y, 442, y + 168), 22, "white")
|
||||
text(d, (32, y + 25), "2026-10-07 16:25", 15, "#667085")
|
||||
rounded(d, (32, y + 50, 150, y + 142), 12, "#D8E4F8")
|
||||
text(d, (168, y + 64), name, 19)
|
||||
text(d, (168, y + 101), f"商品价格 {price}", 16)
|
||||
rounded(d, (168, y + 117, 422, y + 150), 10, "#F4D5A0")
|
||||
text(d, (185, y + 134), f"佣金 {comm}", 16, "#C64F48", anchor="lm")
|
||||
text(d, (384, y + 134), "已完成", 15, anchor="mm")
|
||||
nav(d, "记录")
|
||||
return im
|
||||
|
||||
|
||||
def receive_proto():
|
||||
im, d = phone_base("领取")
|
||||
rounded(d, (18, 72, 442, 320), 18, "#DCEEFF")
|
||||
text(d, (35, 102), "至尊会员", 30)
|
||||
text(d, (35, 138), "佣金费率 5%", 18)
|
||||
d.ellipse((165, 165, 295, 295), fill="#BED8FF")
|
||||
rounded(d, (148, 250, 312, 298), 24, "#4F79E8")
|
||||
text(d, (230, 275), "自动领取", 22, "white", anchor="mm")
|
||||
text(d, (230, 350), "全力领取中 结果将在下方发放", 16, "#667085", anchor="mm")
|
||||
text(d, (22, 398), "今日战果", 23)
|
||||
cards = [("总资产", "300019.13"), ("昨日佣金", "0"), ("今日佣金", "14.9"), ("今日领取数", "1")]
|
||||
for i, (label, val) in enumerate(cards):
|
||||
x = 20 + (i % 2) * 212
|
||||
y = 430 + (i // 2) * 104
|
||||
rounded(d, (x, y, x + 200, y + 88), 14, ["#E4E1FF", "#FFF0CF", "#FFE2E4", "#DDF6FF"][i])
|
||||
text(d, (x + 14, y + 25), label, 15, "#667085")
|
||||
text(d, (x + 14, y + 62), val, 22)
|
||||
text(d, (22, 655), "规则提示:营业时间、余额门槛、60 分钟时限", 15, "#667085")
|
||||
nav(d, "领取")
|
||||
return im
|
||||
|
||||
|
||||
def mine_proto():
|
||||
im, d = phone_base("我的")
|
||||
rounded(d, (20, 78, 440, 166), 22, "#CCE4FF")
|
||||
d.ellipse((38, 96, 92, 150), fill="#AFC8EA")
|
||||
text(d, (110, 108), "用户昵称", 22)
|
||||
text(d, (110, 139), "邀请码 ******", 15, "#667085")
|
||||
rounded(d, (20, 188, 440, 315), 25, "#F5D37D")
|
||||
text(d, (42, 226), "Rmb$ 300,019.13", 30)
|
||||
rounded(d, (48, 257, 177, 297), 19, "#B9A95F")
|
||||
rounded(d, (200, 257, 329, 297), 19, "white")
|
||||
text(d, (112, 278), "立即取出", 17, anchor="mm")
|
||||
text(d, (264, 278), "立即储入", 17, anchor="mm")
|
||||
text(d, (22, 355), "更多功能", 22)
|
||||
for i, label in enumerate(["个人信息", "储入记录", "取出记录", "账户明细", "弹窗记录", "退出登录"]):
|
||||
y = 390 + i * 58
|
||||
rounded(d, (20, y, 440, y + 48), 10, "white")
|
||||
text(d, (46, y + 25), label, 18, anchor="lm")
|
||||
text(d, (416, y + 25), ">", 18, "#667085", anchor="mm")
|
||||
nav(d, "我的")
|
||||
return im
|
||||
|
||||
|
||||
def recharge_proto():
|
||||
im, d = phone_base("储入")
|
||||
cols = ["等级", "金额", "赠送", "单数", "比例"]
|
||||
d.rounded_rectangle((14, 76, 446, 114), radius=10, fill="#4F79E8")
|
||||
for i, c in enumerate(cols):
|
||||
x = 24 + i * 82
|
||||
text(d, (x, 95), c, 14, "white", anchor="mm")
|
||||
rows = [("普通", "500", "20", "10", "1.8%"), ("白银", "1888", "88", "20", "2.0%"), ("黄金", "10000", "188", "30", "2.2%"), ("铂金", "30000", "388", "40", "3.0%"), ("钻石", "50000", "588", "50", "3.5%"), ("星耀", "88888", "1888", "80", "4.0%"), ("至尊", "188888", "2888", "1000", "5.0%")]
|
||||
for r, row in enumerate(rows):
|
||||
y = 126 + r * 42
|
||||
if r % 2:
|
||||
d.rectangle((14, y - 17, 446, y + 20), fill="#EEF3FC")
|
||||
for i, c in enumerate(row):
|
||||
text(d, (24 + i * 82, y), c, 13, anchor="mm")
|
||||
text(d, (22, 455), "输入金额", 16)
|
||||
rounded(d, (20, 478, 440, 530), 18, "white", "#D9D9D9")
|
||||
text(d, (22, 565), "姓名", 16)
|
||||
rounded(d, (20, 588, 440, 640), 18, "white", "#D9D9D9")
|
||||
rounded(d, (20, 690, 440, 746), 25, "#4F79E8")
|
||||
text(d, (230, 718), "提交", 21, "white", anchor="mm")
|
||||
return im
|
||||
|
||||
|
||||
def bind_proto():
|
||||
im, d = phone_base("绑定信息")
|
||||
labels = ["姓名", "银行名", "银行账户", "人民币编码"]
|
||||
for i, lab in enumerate(labels):
|
||||
y = 78 + i * 100
|
||||
text(d, (24, y), lab, 17)
|
||||
rounded(d, (20, y + 24, 440, y + 78), 18, "white", "#D9D9D9")
|
||||
text(d, (24, 490), "支付宝收款码", 17)
|
||||
rounded(d, (20, 520, 156, 650), 16, "white", "#C9CFDA")
|
||||
text(d, (88, 585), "+", 42, anchor="mm")
|
||||
text(d, (210, 490), "微信收款码", 17)
|
||||
rounded(d, (206, 520, 342, 650), 16, "white", "#C9CFDA")
|
||||
text(d, (274, 585), "+", 42, anchor="mm")
|
||||
rounded(d, (20, 700, 440, 756), 25, "#4F79E8")
|
||||
text(d, (230, 728), "提交", 21, "white", anchor="mm")
|
||||
return im
|
||||
|
||||
|
||||
def service_proto():
|
||||
im, d = phone_base("在线客服")
|
||||
text(d, (28, 92), "储入与问题处理请联系在线客服", 18)
|
||||
for i in range(3):
|
||||
x = 18 + i * 144
|
||||
rounded(d, (x, 148, x + 132, 330), 20, "white")
|
||||
d.ellipse((x + 42, 176, x + 90, 224), fill="#2C3440")
|
||||
text(d, (x + 66, 253), f"在线客服 {i+1}", 16, anchor="mm")
|
||||
text(d, (x + 66, 281), "9:00-22:00", 13, "#667085", anchor="mm")
|
||||
rounded(d, (x + 16, 295, x + 116, 327), 16, "#4F79E8")
|
||||
text(d, (x + 66, 312), "联系客服", 14, "white", anchor="mm")
|
||||
text(d, (20, 390), "进入后嵌入第三方即时聊天页", 17, "#667085")
|
||||
nav(d, "客服")
|
||||
return im
|
||||
|
||||
|
||||
def mall_proto():
|
||||
im, d = phone_base("手工商城")
|
||||
rounded(d, (18, 78, 442, 210), 18, "#285FD8")
|
||||
text(d, (230, 144), "手工任务横幅", 30, "white", anchor="mm")
|
||||
for i in range(6):
|
||||
x = 18 + (i % 2) * 214
|
||||
y = 228 + (i // 2) * 172
|
||||
rounded(d, (x, y, x + 204, y + 156), 16, "white")
|
||||
rounded(d, (x + 8, y + 8, x + 196, y + 88), 12, "#DDE6F4")
|
||||
text(d, (x + 13, y + 110), f"手工商品 {i+1} 工资说明", 15)
|
||||
text(d, (x + 13, y + 137), "已领取 0.8", 13, "#667085")
|
||||
return im
|
||||
|
||||
|
||||
def composite(path: Path, title: str, left, right, left_label: str, right_label: str):
|
||||
canvas = Image.new("RGB", (1200, 980), "white")
|
||||
d = ImageDraw.Draw(canvas)
|
||||
text(d, (600, 34), title, 28, anchor="mm")
|
||||
canvas.paste(left.resize((460, 860)), (105, 74))
|
||||
canvas.paste(right.resize((460, 860)), (635, 74))
|
||||
text(d, (335, 955), left_label, 17, "#667085", anchor="mm")
|
||||
text(d, (865, 955), right_label, 17, "#667085", anchor="mm")
|
||||
canvas.save(path, quality=92)
|
||||
|
||||
|
||||
def set_cell_shading(cell, fill):
|
||||
tc_pr = cell._tc.get_or_add_tcPr()
|
||||
shd = tc_pr.find(qn("w:shd"))
|
||||
if shd is None:
|
||||
shd = OxmlElement("w:shd")
|
||||
tc_pr.append(shd)
|
||||
shd.set(qn("w:fill"), fill)
|
||||
|
||||
|
||||
def set_cell_border(cell, color=GRID, size="6"):
|
||||
tc_pr = cell._tc.get_or_add_tcPr()
|
||||
borders = tc_pr.first_child_found_in("w:tcBorders")
|
||||
if borders is None:
|
||||
borders = OxmlElement("w:tcBorders")
|
||||
tc_pr.append(borders)
|
||||
for edge in ("top", "left", "bottom", "right", "insideH", "insideV"):
|
||||
tag = "w:" + edge
|
||||
node = borders.find(qn(tag))
|
||||
if node is None:
|
||||
node = OxmlElement(tag)
|
||||
borders.append(node)
|
||||
node.set(qn("w:val"), "single")
|
||||
node.set(qn("w:sz"), size)
|
||||
node.set(qn("w:color"), color)
|
||||
|
||||
|
||||
def set_cell_margins(cell, top=100, start=110, bottom=100, end=110):
|
||||
tc = cell._tc
|
||||
tc_pr = tc.get_or_add_tcPr()
|
||||
tc_mar = tc_pr.first_child_found_in("w:tcMar")
|
||||
if tc_mar is None:
|
||||
tc_mar = OxmlElement("w:tcMar")
|
||||
tc_pr.append(tc_mar)
|
||||
for m, v in (("top", top), ("start", start), ("bottom", bottom), ("end", end)):
|
||||
node = tc_mar.find(qn("w:" + m))
|
||||
if node is None:
|
||||
node = OxmlElement("w:" + m)
|
||||
tc_mar.append(node)
|
||||
node.set(qn("w:w"), str(v))
|
||||
node.set(qn("w:type"), "dxa")
|
||||
|
||||
|
||||
def add_table(doc, headers, rows, widths=None, font_size=8.8):
|
||||
table = doc.add_table(rows=1, cols=len(headers))
|
||||
table.alignment = WD_TABLE_ALIGNMENT.CENTER
|
||||
table.autofit = False
|
||||
hdr = table.rows[0].cells
|
||||
header_props = table.rows[0]._tr.get_or_add_trPr()
|
||||
repeat_header = OxmlElement("w:tblHeader")
|
||||
repeat_header.set(qn("w:val"), "true")
|
||||
header_props.append(repeat_header)
|
||||
header_props.append(OxmlElement("w:cantSplit"))
|
||||
for i, h in enumerate(headers):
|
||||
hdr[i].text = h
|
||||
set_cell_shading(hdr[i], "334E7D")
|
||||
set_cell_border(hdr[i])
|
||||
set_cell_margins(hdr[i])
|
||||
hdr[i].vertical_alignment = WD_CELL_VERTICAL_ALIGNMENT.CENTER
|
||||
p = hdr[i].paragraphs[0]
|
||||
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
for r in p.runs:
|
||||
r.font.color.rgb = RGBColor(255, 255, 255)
|
||||
r.font.bold = True
|
||||
r.font.size = Pt(font_size)
|
||||
for ridx, row in enumerate(rows):
|
||||
table_row = table.add_row()
|
||||
table_row._tr.get_or_add_trPr().append(OxmlElement("w:cantSplit"))
|
||||
cells = table_row.cells
|
||||
for i, value in enumerate(row):
|
||||
cells[i].text = str(value)
|
||||
set_cell_border(cells[i])
|
||||
set_cell_margins(cells[i])
|
||||
if ridx % 2:
|
||||
set_cell_shading(cells[i], "F3F6FB")
|
||||
cells[i].vertical_alignment = WD_CELL_VERTICAL_ALIGNMENT.CENTER
|
||||
p = cells[i].paragraphs[0]
|
||||
p.alignment = WD_ALIGN_PARAGRAPH.CENTER if i == 0 else WD_ALIGN_PARAGRAPH.LEFT
|
||||
for r in p.runs:
|
||||
r.font.size = Pt(font_size)
|
||||
r.font.color.rgb = RGBColor.from_string(INK)
|
||||
if widths:
|
||||
for row in table.rows:
|
||||
for i, w in enumerate(widths):
|
||||
row.cells[i].width = Inches(w)
|
||||
doc.add_paragraph()
|
||||
return table
|
||||
|
||||
|
||||
def set_run_font(run, name="Heiti SC", size=10.5, bold=None, color=INK):
|
||||
run.font.name = name
|
||||
run._element.get_or_add_rPr().rFonts.set(qn("w:eastAsia"), "Heiti SC")
|
||||
run.font.size = Pt(size)
|
||||
run.font.color.rgb = RGBColor.from_string(color)
|
||||
if bold is not None:
|
||||
run.font.bold = bold
|
||||
|
||||
|
||||
def p(doc, content="", style=None, bold_lead=None):
|
||||
par = doc.add_paragraph(style=style)
|
||||
par.paragraph_format.space_after = Pt(6)
|
||||
par.paragraph_format.line_spacing = 1.28
|
||||
if bold_lead and content.startswith(bold_lead):
|
||||
a = par.add_run(bold_lead)
|
||||
set_run_font(a, bold=True)
|
||||
b = par.add_run(content[len(bold_lead):])
|
||||
set_run_font(b)
|
||||
else:
|
||||
r = par.add_run(content)
|
||||
set_run_font(r)
|
||||
return par
|
||||
|
||||
|
||||
def bullet(doc, content, level=0):
|
||||
par = doc.add_paragraph(style="List Bullet" if level == 0 else "List Bullet 2")
|
||||
par.paragraph_format.space_after = Pt(3)
|
||||
par.paragraph_format.line_spacing = 1.2
|
||||
set_run_font(par.add_run(content), size=10.2)
|
||||
return par
|
||||
|
||||
|
||||
def numbered(doc, content):
|
||||
par = doc.add_paragraph(style="List Number")
|
||||
par.paragraph_format.space_after = Pt(4)
|
||||
set_run_font(par.add_run(content), size=10.2)
|
||||
return par
|
||||
|
||||
|
||||
def heading(doc, value, level=1):
|
||||
h = doc.add_heading(value, level=level)
|
||||
h.paragraph_format.space_before = Pt(10 if level == 1 else 7)
|
||||
h.paragraph_format.space_after = Pt(6)
|
||||
return h
|
||||
|
||||
|
||||
def page_break(doc):
|
||||
doc.add_page_break()
|
||||
|
||||
|
||||
def add_picture(doc, path, caption, width=6.6, page_break_before=False):
|
||||
par = doc.add_paragraph()
|
||||
par.paragraph_format.page_break_before = page_break_before
|
||||
if page_break_before:
|
||||
par.paragraph_format.space_before = Pt(18)
|
||||
par.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
par.add_run().add_picture(str(path), width=Inches(width))
|
||||
cap = doc.add_paragraph()
|
||||
cap.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
cap.paragraph_format.space_after = Pt(8)
|
||||
set_run_font(cap.add_run(caption), size=9, color=MUTED)
|
||||
|
||||
|
||||
def add_page_number(paragraph):
|
||||
paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
run = paragraph.add_run()
|
||||
fld_char1 = OxmlElement("w:fldChar")
|
||||
fld_char1.set(qn("w:fldCharType"), "begin")
|
||||
instr = OxmlElement("w:instrText")
|
||||
instr.set(qn("xml:space"), "preserve")
|
||||
instr.text = "PAGE"
|
||||
fld_char2 = OxmlElement("w:fldChar")
|
||||
fld_char2.set(qn("w:fldCharType"), "end")
|
||||
run._r.append(fld_char1)
|
||||
run._r.append(instr)
|
||||
run._r.append(fld_char2)
|
||||
|
||||
|
||||
def build_assets():
|
||||
ASSET_DIR.mkdir(parents=True, exist_ok=True)
|
||||
pairs = [
|
||||
("prototype_01.png", "核心导航与内容列表", home_proto(), records_proto(), "首页", "订单记录"),
|
||||
("prototype_02.png", "领取与个人中心", receive_proto(), mine_proto(), "领取中心", "我的"),
|
||||
("prototype_03.png", "资金入口与收款信息", recharge_proto(), bind_proto(), "储入申请", "收款信息绑定"),
|
||||
("prototype_04.png", "客服与手工任务", service_proto(), mall_proto(), "在线客服", "手工商城"),
|
||||
]
|
||||
out = []
|
||||
for name, title, left, right, ll, rl in pairs:
|
||||
path = ASSET_DIR / name
|
||||
composite(path, title, left, right, ll, rl)
|
||||
out.append(path)
|
||||
return out
|
||||
|
||||
|
||||
def build_doc():
|
||||
OUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
figures = build_assets()
|
||||
doc = Document()
|
||||
sec = doc.sections[0]
|
||||
sec.page_width = Inches(8.5)
|
||||
sec.page_height = Inches(11)
|
||||
sec.top_margin = Inches(0.68)
|
||||
sec.bottom_margin = Inches(0.65)
|
||||
sec.left_margin = Inches(0.72)
|
||||
sec.right_margin = Inches(0.72)
|
||||
|
||||
styles = doc.styles
|
||||
normal = styles["Normal"]
|
||||
normal.font.name = "Heiti SC"
|
||||
normal._element.rPr.rFonts.set(qn("w:eastAsia"), "Heiti SC")
|
||||
normal.font.size = Pt(10.5)
|
||||
normal.font.color.rgb = RGBColor.from_string(INK)
|
||||
for name, size in (("Title", 27), ("Heading 1", 18), ("Heading 2", 14), ("Heading 3", 11.5)):
|
||||
st = styles[name]
|
||||
st.font.name = "Heiti SC"
|
||||
st._element.rPr.rFonts.set(qn("w:eastAsia"), "Heiti SC")
|
||||
st.font.size = Pt(size)
|
||||
st.font.color.rgb = RGBColor(0, 0, 0)
|
||||
st.font.bold = True
|
||||
|
||||
header = sec.header.paragraphs[0]
|
||||
header.alignment = WD_ALIGN_PARAGRAPH.RIGHT
|
||||
set_run_font(header.add_run("移动端任务与账户平台需求文档"), size=8.5, color=MUTED)
|
||||
add_page_number(sec.footer.paragraphs[0])
|
||||
|
||||
title = doc.add_paragraph(style="Title")
|
||||
title_pr = title._p.get_or_add_pPr()
|
||||
border = title_pr.find(qn("w:pBdr"))
|
||||
if border is not None:
|
||||
title_pr.remove(border)
|
||||
border = OxmlElement("w:pBdr")
|
||||
for edge in ("top", "left", "bottom", "right", "between"):
|
||||
node = OxmlElement("w:" + edge)
|
||||
node.set(qn("w:val"), "nil")
|
||||
border.append(node)
|
||||
title_pr.append(border)
|
||||
title.alignment = WD_ALIGN_PARAGRAPH.LEFT
|
||||
title.paragraph_format.space_before = Pt(70)
|
||||
title.paragraph_format.space_after = Pt(18)
|
||||
set_run_font(title.add_run("移动端任务与账户平台需求文档"), size=27, bold=True, color="000000")
|
||||
sub = doc.add_paragraph()
|
||||
set_run_font(sub.add_run("参考站点功能拆解与重构建议"), size=16, bold=True, color="000000")
|
||||
sub.paragraph_format.space_after = Pt(30)
|
||||
p(doc, "目标:在不复制品牌素材、敏感数据与高风险业务表述的前提下,复刻参考站点的信息架构和交互骨架,并形成可开发、可验收、可审计的产品需求基线。")
|
||||
p(doc, "主要读者:产品负责人、交互设计师、前端开发、后端开发、测试、风控与运营。")
|
||||
p(doc, f"观察日期:{date(2026, 10, 7).isoformat()} 参考环境:移动端网页 文档版本:V1.0")
|
||||
p(doc, "结论:参考站点由账户、任务订单、余额台账、储入取出、会员等级、内容展示和在线客服七个域构成。建议复刻页面结构与业务闭环,但重写资金、隐私、安全和合规机制。")
|
||||
page_break(doc)
|
||||
|
||||
heading(doc, "1 文档范围与方法", 1)
|
||||
p(doc, "本需求基于登录后的页面逐项浏览、按钮与导航行为检查、页面状态观察,以及浏览器中可见网络请求和前端资源分析。本文把结论分为三类:实测行为、前端可见实现、建议需求。敏感手机号、邀请码、银行卡等信息已脱敏,不作为原型内容。")
|
||||
heading(doc, "1.1 已覆盖范围", 2)
|
||||
for item in [
|
||||
"登录页现状、注册页字段、会话与路由保护。",
|
||||
"首页、荣誉榜、会员等级、平台简介、规则说明、代理合作、公司资质。",
|
||||
"订单记录筛选、领取中心、订单资金变化、手工商城。",
|
||||
"储入、取出、收款信息绑定、储入记录、取出记录、账户明细。",
|
||||
"个人信息、头像上传、登录密码、交易密码、通知记录、在线客服。",
|
||||
"可观察接口、响应结构、关键数据字段与前端安全实现。",
|
||||
]:
|
||||
bullet(doc, item)
|
||||
heading(doc, "1.2 未执行操作", 2)
|
||||
p(doc, "为避免生成真实订单、变更资金、上传个人文件、修改密码或退出当前会话,本文未点击自动领取、订单提交、储入提交、取出确认、收款信息提交、头像上传、密码提交和退出登录。对应需求由页面字段、前端接口定义与上下游数据状态推导,并标记为待联调确认。")
|
||||
|
||||
heading(doc, "2 产品定位与角色", 1)
|
||||
p(doc, "产品可定位为移动端会员任务与佣金账户平台。用户通过注册、储入余额、领取任务订单、完成任务、获得本金返还与佣金,并可查看台账或发起取出。平台通过等级、任务数量和费率管理用户权益,通过客服处理储入、异常订单和账户问题。")
|
||||
add_table(doc, ["角色", "主要目标", "关键权限"], [
|
||||
("游客", "了解平台并注册", "登录、注册、查看隐私政策"),
|
||||
("会员", "领取任务并查看收益", "首页、任务、订单、余额、客服、个人设置"),
|
||||
("高级会员", "按等级获得更多任务和更高费率", "等级权益、额度与次数受配置控制"),
|
||||
("客服", "处理储入、取出、异常任务", "会话、工单、用户与交易查询"),
|
||||
("运营", "管理内容、等级、任务和公告", "配置、发布、审核、数据看板"),
|
||||
("财务与风控", "审核资金和异常行为", "充值审核、提现审核、台账、风控规则"),
|
||||
("管理员", "维护权限与系统参数", "RBAC、审计日志、系统配置"),
|
||||
], [0.8, 2.1, 3.5])
|
||||
|
||||
heading(doc, "3 信息架构与页面路由", 1)
|
||||
add_table(doc, ["一级模块", "页面或路由", "核心内容", "入口"], [
|
||||
("认证", "/login /register", "手机号登录、账户名、推荐码、验证码", "未登录访问"),
|
||||
("首页", "/home", "活动图、快捷入口、余额、荣誉榜、会员等级、公司介绍", "底部首页"),
|
||||
("任务", "/receive /orderDetail", "领取、等待、任务详情、提交完成", "底部领取"),
|
||||
("记录", "记录页", "全部、待处理、已完成订单", "底部记录"),
|
||||
("手工", "/mall", "手工任务卡片并引导客服", "首页快捷入口"),
|
||||
("资金", "/recharge /withdrawal", "储入、取出、余额、交易密码", "首页与个人中心"),
|
||||
("账户", "/mine /personalInfo", "用户资料、信用分、邀请码、设置", "底部我的"),
|
||||
("明细", "/depositRecord /withdrawalRecord /bill", "储入、取出、余额台账", "我的"),
|
||||
("客服", "/customerService /iframeCustom", "客服列表与第三方会话", "底部客服"),
|
||||
("内容", "/html/{id}", "平台简介、规则、代理、资质", "首页公司简介"),
|
||||
], [0.7, 1.45, 3.1, 1.0])
|
||||
heading(doc, "3.1 建议导航原则", 2)
|
||||
for item in [
|
||||
"底部五栏固定为首页、记录、领取、客服、我的;详情页隐藏底部栏并保留明确返回。",
|
||||
"资金页面必须显示币种、可用余额、冻结余额、预计到账和手续费,不使用含义不明的“人民币编码”。",
|
||||
"所有金额变动都能跳转至同一业务单据,并在台账中保持可解释、可追溯。",
|
||||
"客服 iframe 需明确第三方来源、隐私边界和会话状态,失败时提供备用联系方式。",
|
||||
]:
|
||||
bullet(doc, item)
|
||||
|
||||
heading(doc, "4 核心业务闭环", 1)
|
||||
heading(doc, "4.1 任务订单闭环", 2)
|
||||
for item in [
|
||||
"用户进入领取中心,系统校验登录态、营业时间、会员等级、最低可用余额、未完成任务与风控状态。",
|
||||
"用户点击领取后,系统创建或匹配一笔任务订单,返回任务编号、商品、金额、佣金、截止时间与状态。",
|
||||
"任务确认时,台账先记录交易扣款;完成后记录本金返还和佣金入账。参考数据中同一业务单号出现 -298、+298、+14.9 三条流水。",
|
||||
"订单必须支持待领取、匹配中、待处理、处理中、已完成、超时、冻结、取消与失败,并记录每次状态变化。",
|
||||
"任何补差额、冻结或信用分变化都必须有明确原因、复核入口和申诉渠道;不得依赖口头客服指令。",
|
||||
]:
|
||||
numbered(doc, item)
|
||||
heading(doc, "4.2 储入与取出闭环", 2)
|
||||
p(doc, "储入页面按等级展示金额、赠送额、每日单数和佣金比例,用户填写金额与姓名后提交。参考站点还要求联系客服获取收款方式。新系统应改为平台内生成可核验的支付订单,禁止客服通过聊天发送临时个人账户。")
|
||||
p(doc, "取出前需绑定姓名、银行名、银行账户与收款码,随后输入金额和交易密码。新系统应只保留与实际支付通道必要的字段,敏感数据加密存储,收款码上传须做病毒扫描、内容校验与访问控制。")
|
||||
heading(doc, "4.3 会员与代理闭环", 2)
|
||||
p(doc, "参考站点设置普通、白银、黄金、铂金、钻石、星耀、至尊七档,充值门槛、赠送额、每日单数和佣金比例逐级上升,并存在三级推荐奖励。建议将等级规则做成版本化配置,禁止前端写死;代理奖励必须在合规评估通过后再启用。")
|
||||
|
||||
page_break(doc)
|
||||
heading(doc, "5 页面级功能需求", 1)
|
||||
heading(doc, "5.1 登录与注册", 2)
|
||||
add_table(doc, ["页面", "字段与控件", "校验与行为", "验收要点"], [
|
||||
("登录", "手机号、密码、记住密码、登录、去注册、隐私政策", "手机号格式;密码非空;错误次数限制;登录成功进入首页", "不在本地明文保存密码;支持会话过期提示"),
|
||||
("注册", "账户名、11 位手机号、密码、推荐码、图片验证码、隐私政策", "账户名必填;手机号唯一;验证码校验;推荐码可选或按运营配置", "提交前明确同意协议;防批量注册;注册成功自动登录或返回登录"),
|
||||
("会话", "Authorization 令牌", "401 清除会话并返回登录;403 显示可解释错误", "支持刷新、撤销、设备管理与退出所有设备"),
|
||||
], [0.65, 2.1, 2.35, 1.55])
|
||||
|
||||
heading(doc, "5.2 首页", 2)
|
||||
for item in [
|
||||
"展示轮播图或单图运营位,支持后台排序、上下线、跳转类型和有效期。",
|
||||
"快捷入口为领取、储入、取出、手工;按后台开关显示,关闭时给出原因而不是静默失效。",
|
||||
"账户卡展示昵称、可用余额与币种;点击可进入账户明细。",
|
||||
"荣誉榜为脱敏昵称、积分和结果状态,要求数据真实性标识、刷新频率与隐私策略。",
|
||||
"会员等级展示当前等级、费率和下一等级条件;等级图不得只是一张不可访问的图片。",
|
||||
"公司简介包括平台简介、规则说明、代理合作和公司资质,内容由 CMS 管理并保留版本。",
|
||||
]:
|
||||
bullet(doc, item)
|
||||
|
||||
heading(doc, "5.3 记录与订单详情", 2)
|
||||
p(doc, "记录页显示可用资产,提供全部、待处理、已完成三种筛选。列表项包括创建时间、商品图、商品名、商品价格、佣金与状态。分页默认 10 条,支持下拉刷新、空状态、加载失败重试和详情跳转。")
|
||||
p(doc, "实测发现先切换待处理、再切换已完成时曾短暂出现空列表,而重新加载全部后可见已完成记录。验收时需覆盖筛选参数、缓存状态和并发请求覆盖问题。")
|
||||
add_table(doc, ["状态", "用户可见文案", "允许操作", "系统约束"], [
|
||||
("匹配中", "正在匹配任务", "取消等待", "只允许一个匹配请求"),
|
||||
("待处理", "请在截止时间前完成", "查看详情、提交", "倒计时与服务端时间一致"),
|
||||
("处理中", "已提交,等待确认", "查看详情、联系客服", "防重复提交"),
|
||||
("已完成", "本金与佣金已入账", "查看流水", "三笔或组合流水必须可核对"),
|
||||
("超时或冻结", "显示原因与处理路径", "申诉、联系客服", "需要人工复核和审计"),
|
||||
("失败或取消", "显示失败原因", "重新领取", "不得产生不一致余额"),
|
||||
], [0.8, 2.1, 1.4, 2.35])
|
||||
|
||||
heading(doc, "5.4 领取中心", 2)
|
||||
p(doc, "领取中心包含会员等级、佣金费率、主领取按钮、匹配进度、今日战果和规则说明。点击主按钮前需完成资格预检;按钮点击后立即进入不可重复提交的 loading 状态;匹配成功跳转订单详情,失败显示可恢复原因。")
|
||||
bullet(doc, "预检项:营业时间、账户状态、信用或风控状态、最低余额、当日次数、未完成订单、等级商品范围。")
|
||||
bullet(doc, "统计项:总资产、昨日佣金、今日佣金、今日领取数;所有统计口径需要后台统一定义。")
|
||||
bullet(doc, "规则文案需要 CMS 管理,禁止把风控与资金规则硬编码在页面。")
|
||||
|
||||
heading(doc, "5.5 手工商城", 2)
|
||||
p(doc, "页面以双列卡片展示图片、任务名称、份数、工资与已领取数;点击卡片实测直接进入客服页。建议增加任务详情、适用人群、交付方式、预计周期、库存、审核标准和风险提示,再由用户主动发起咨询。")
|
||||
p(doc, "参考站点一条商品名称中可见脚本标签文本,说明内容过滤不足。新系统必须对运营内容做 HTML 转义、富文本白名单、图片域名白名单和持久型 XSS 检测。")
|
||||
|
||||
heading(doc, "5.6 客服", 2)
|
||||
p(doc, "客服页展示多个客服入口与工作时间,点击后在站内 iframe 打开第三方聊天。聊天页含历史会话、系统消息、文本框、图片与视频上传。需求上应增加服务状态、排队人数、会话编号、隐私声明、附件大小与类型限制、敏感信息提醒和会话评价。")
|
||||
|
||||
page_break(doc)
|
||||
heading(doc, "5.7 我的与个人信息", 2)
|
||||
add_table(doc, ["功能", "需求", "边界或风险"], [
|
||||
("资料概览", "头像、昵称、邀请码、会员等级、信用状态、余额", "手机号等敏感字段默认脱敏"),
|
||||
("储入与取出", "提供余额入口与状态提示", "资金操作必须二次确认与风控"),
|
||||
("收款信息", "姓名、银行、账户、可选支付通道", "加密、最小化收集、修改留痕"),
|
||||
("头像", "上传、裁剪、预览、替换", "文件扫描、大小和格式校验"),
|
||||
("密码", "旧密码、新密码、确认密码", "强度、频控、成功后令牌失效"),
|
||||
("交易密码", "独立于登录密码", "不得用无盐 MD5;失败次数锁定"),
|
||||
("通知", "公告列表、未读、已读、详情", "重要规则变更需留存阅读证据"),
|
||||
("退出", "清除令牌和本地用户数据", "保留确认,避免误触"),
|
||||
], [1.0, 3.1, 2.45])
|
||||
|
||||
heading(doc, "5.8 储入", 2)
|
||||
p(doc, "页面展示等级规则表和金额、姓名输入框。参考规则为普通 500、白银 1888、黄金 10000、铂金 30000、钻石 50000、星耀 88888、至尊 188888;费率从 1.8% 到 5%。规则说明和表格存在个别口径差异,必须以后台版本化配置为唯一事实源。")
|
||||
bullet(doc, "提交前展示收款主体、订单号、有效期、金额、手续费和风险提示。")
|
||||
bullet(doc, "储入订单状态:待支付、待审核、成功、失败、过期、取消;失败必须提供原因。")
|
||||
bullet(doc, "若需要上传付款凭证,必须经过用户明确选择,并限制为图片或 PDF。")
|
||||
|
||||
heading(doc, "5.9 取出与收款绑定", 2)
|
||||
p(doc, "取出页面展示余额、金额、已绑定收款信息摘要和交易密码。若未绑定,弹窗提示并引导至绑定页。绑定页实测字段为姓名、银行名、银行账户、人民币编码、支付宝收款码和微信收款码。建议删除含义不清或非必要字段,并根据支付渠道动态展示。")
|
||||
bullet(doc, "金额校验:大于 0、不超过可用余额、符合单笔最小值和最大值,并显示手续费与到账金额。")
|
||||
bullet(doc, "状态:待审核、审核中、处理中、成功、失败、驳回、取消;用户可查看原因和预计时间。")
|
||||
bullet(doc, "交易密码只在提交时使用;服务端采用强哈希校验并配合频率限制、设备风控和二次验证。")
|
||||
|
||||
page_break(doc)
|
||||
heading(doc, "\u20605.10 账户明细与记录", 2)
|
||||
p(doc, "账户明细需按业务单号聚合展示变动类型、变动金额、变动前余额、变动后余额、时间、状态和关联单据。储入记录显示订单号、状态、金额、创建时间;取出记录字段同构,并增加手续费、到账金额与失败原因。")
|
||||
|
||||
heading(doc, "6 数据模型与状态机", 1)
|
||||
add_table(doc, ["实体", "关键字段", "说明"], [
|
||||
("User", "id, username, mobile, avatar, status, inviteCode", "用户主档,手机号单独加密与索引"),
|
||||
("Membership", "levelId, rate, dailyLimit, withdrawLimits, effectiveAt", "版本化等级权益"),
|
||||
("Wallet", "available, frozen, currency, version", "使用乐观锁或账户序列保证一致性"),
|
||||
("LedgerEntry", "bizNo, type, direction, amount, before, after", "不可变台账,支持幂等与冲正"),
|
||||
("TaskOrder", "orderNo, productId, amount, commission, status, deadline", "任务生命周期"),
|
||||
("DepositOrder", "depositNo, amount, channel, status, proofUrl", "储入生命周期"),
|
||||
("WithdrawalOrder", "withdrawNo, amount, fee, netAmount, status", "取出生命周期"),
|
||||
("PayoutProfile", "realName, bankName, maskedAccount, qrAssetIds", "敏感字段加密与脱敏"),
|
||||
("Content", "type, title, body, version, status, publishedAt", "公告、规则与公司资料"),
|
||||
("SupportSession", "sessionNo, provider, status, openedAt, closedAt", "第三方客服映射"),
|
||||
("AuditLog", "actor, action, target, before, after, ip, device", "资金与权限操作强制留痕"),
|
||||
], [1.05, 3.55, 2.0])
|
||||
heading(doc, "6.1 幂等与一致性", 2)
|
||||
for item in [
|
||||
"领取、提交订单、储入回调、取出申请和密码修改都必须使用幂等键。",
|
||||
"钱包余额只能由服务端台账事务修改;前端显示值不参与结算。",
|
||||
"订单状态转换由服务端状态机控制,不允许客户端任意设置。",
|
||||
"重复回调、网络重试和并发点击不得重复扣款、返款或发佣金。",
|
||||
"所有涉及金额的接口返回业务单号和最新余额版本。",
|
||||
]:
|
||||
bullet(doc, item)
|
||||
|
||||
heading(doc, "7 接口需求", 1)
|
||||
p(doc, "下表分为实测请求与前端资源中可见接口。实测接口在浏览器中返回 HTTP 200,响应外层为 Base64 字符串,解码后为 JSON,统一包含 success、code、data、msg。建议新系统直接使用标准 JSON,不把 Base64 当作安全措施。")
|
||||
add_table(doc, ["方法", "接口", "用途", "主要请求或返回字段", "证据"], [
|
||||
("POST", "/wap-api/doLogin", "登录", "username, password;token, users", "前端实现与已发生请求"),
|
||||
("POST", "/wap-api/doRegister", "注册", "nickName, username, password, inviteCode, code, uuid", "前端实现"),
|
||||
("GET", "/wap-api/captchaImage", "图片验证码", "uuid, img, captchaBol", "前端实现"),
|
||||
("GET", "/wap-api/profile", "用户概要", "tel, username, headpic, inviteCode, level, levelRate, balance, xyf", "实测"),
|
||||
("GET", "/wap-api/banner/list", "首页横幅", "图片地址数组", "实测"),
|
||||
("GET", "/wap-api/home", "首页开关与内容编号", "SHOP_STATUS, TASK, POP_MESSAGE, 内容 ID", "实测"),
|
||||
("GET", "/wap-api/user/data/home", "荣誉榜", "name, avatar, balance", "实测"),
|
||||
("GET", "/wap-api/ctrl/level-list", "等级配置", "name, orderNum, num, giveNum, bili, 提现限制", "实测"),
|
||||
("GET", "/wap-api/support", "客服列表", "qq, url, workTime, 分页", "实测"),
|
||||
("GET", "/wap-api/convey/page", "订单分页", "pageNo, pageSize, status;订单与商品字段", "实测"),
|
||||
("POST", "/wap-api/convey/order", "领取或匹配订单", "无显式参数;返回订单", "前端实现,未执行"),
|
||||
("POST", "/wap-api/convey/order/submit/{id}", "提交任务", "订单 id", "前端实现,未执行"),
|
||||
("GET", "/wap-api/convey/count", "领取统计", "balance, countToday, countNumToday, countNumYesterday, content", "实测"),
|
||||
("GET", "/wap-api/convey/unpay", "未完成订单检查", "djs 等状态", "实测"),
|
||||
("GET", "/wap-api/product/page", "手工商品分页", "分页条件;商品卡片", "前端实现"),
|
||||
("GET", "/wap-api/platform-config/trade-wait-time", "任务等待时间", "秒数或分钟数", "实测"),
|
||||
("GET", "/wap-api/bank/current/info", "当前收款信息", "收款资料摘要", "前端实现"),
|
||||
("POST", "/wap-api/bank/save", "保存收款信息", "姓名、银行、账户、收款码", "前端实现,未执行"),
|
||||
("POST", "/wap-api/recharge/recharge", "创建储入申请", "amount, name 等", "前端实现,未执行"),
|
||||
("GET", "/wap-api/recharge/page", "储入记录分页", "分页与状态", "前端实现"),
|
||||
("POST", "/wap-api/deposit/do_deposit", "创建取出申请", "amount, paypassword 等", "前端实现,未执行"),
|
||||
("GET", "/wap-api/deposit/page", "取出记录分页", "分页与状态", "前端实现"),
|
||||
("GET", "/wap-api/balance-log/page", "账户流水", "分页;类型、金额、业务单号", "前端实现与页面实测"),
|
||||
("POST", "/wap-api/users/setPwd", "修改登录密码", "oldPwd, newPwd", "前端实现,未执行"),
|
||||
("POST", "/wap-api/users/setPwd2", "修改交易密码", "oldPwd, newPwd", "前端实现,未执行"),
|
||||
("POST", "/wap-api/local/upload", "文件上传", "multipart file", "前端实现,未执行"),
|
||||
("GET/POST", "/wap-api/user-tc/select /read/{id} /page", "弹窗通知", "公告、已读、分页", "实测与前端实现"),
|
||||
], [0.55, 1.65, 1.25, 2.45, 0.85], 7.2)
|
||||
|
||||
heading(doc, "7.1 新系统接口规范", 2)
|
||||
for item in [
|
||||
"统一使用 HTTPS、JSON、明确的 HTTP 状态码、traceId、错误码和可本地化 message。",
|
||||
"Authorization 使用短期访问令牌与可撤销刷新令牌;敏感操作增加一次性挑战或二次验证。",
|
||||
"列表接口统一 page、pageSize、sort、status,返回 items、total、hasNext。",
|
||||
"金额使用最小货币单位整数或高精度 Decimal,禁止浮点结算。",
|
||||
"上传接口先申请受限上传凭证,服务端扫描后再绑定业务。",
|
||||
"所有写接口要求 Idempotency-Key、设备信息与审计上下文。",
|
||||
]:
|
||||
bullet(doc, item)
|
||||
|
||||
heading(doc, "8 管理后台需求", 1)
|
||||
add_table(doc, ["模块", "主要能力", "审批与审计"], [
|
||||
("用户管理", "查询、状态、等级、信用、设备、邀请关系", "冻结与解冻需理由和双人复核"),
|
||||
("等级配置", "门槛、赠送、次数、费率、提现限制、商品范围", "版本化发布,可回滚"),
|
||||
("任务商品", "商品、图片、金额、佣金、库存、上下线", "内容过滤和审核流"),
|
||||
("任务订单", "状态、倒计时、资金流水、异常处理", "人工改状态必须双人审批"),
|
||||
("储入管理", "支付订单、回调、凭证、审核、对账", "每日对账与差错处理"),
|
||||
("取出管理", "审核、通道、手续费、失败重试", "大额与异常规则"),
|
||||
("内容管理", "横幅、规则、简介、资质、公告", "预览、定时、版本与发布人"),
|
||||
("客服管理", "客服入口、工作时间、会话映射、工单", "第三方访问最小权限"),
|
||||
("风控", "设备、IP、频次、关联账户、异常金额", "命中规则可解释并可申诉"),
|
||||
("权限审计", "RBAC、操作日志、导出日志", "关键权限定期复核"),
|
||||
], [1.0, 3.65, 2.1])
|
||||
|
||||
heading(doc, "9 非功能需求", 1)
|
||||
heading(doc, "9.1 安全", 2)
|
||||
for item in [
|
||||
"密码使用 Argon2id 或 bcrypt 加盐存储。参考站点在浏览器端对密码做 MD5,这不能替代服务端强哈希。",
|
||||
"输入与富文本做上下文相关转义;运营内容采用白名单;CSP 禁止任意外部脚本。",
|
||||
"银行卡和手机号加密存储,日志与埋点禁止记录完整值;展示统一脱敏。",
|
||||
"接口启用 CSRF 或同站策略、速率限制、风控挑战、签名回调和重放保护。",
|
||||
"第三方客服 iframe 设置最小 sandbox 权限、allowlist 域名和清晰的数据传输告知。",
|
||||
"上传文件限制 MIME、扩展名、体积与像素,执行恶意内容扫描,使用隔离存储和短期访问 URL。",
|
||||
]:
|
||||
bullet(doc, item)
|
||||
heading(doc, "9.2 性能与可用性", 2)
|
||||
for item in [
|
||||
"首屏 LCP 目标小于 2.5 秒,核心 API P95 小于 800 毫秒,写操作 P95 小于 1.5 秒。",
|
||||
"按钮防抖、请求取消、重试与离线提示;长列表分页和图片懒加载。",
|
||||
"资金写入接口可用性目标 99.95%,客服与内容接口目标 99.9%。",
|
||||
"客户端时间仅用于展示,截止时间与状态以服务端为准。",
|
||||
]:
|
||||
bullet(doc, item)
|
||||
heading(doc, "9.3 可访问性与国际化", 2)
|
||||
p(doc, "按钮和导航必须有语义标签与至少 44×44 像素触控区域;颜色不作为唯一状态标识;支持字体放大。简体、繁体和术语需统一,避免同一页面混用“储入/充值”“取出/提现”“领取/抢单”。")
|
||||
|
||||
heading(doc, "10 合规与业务风险", 1)
|
||||
p(doc, "参考站点涉及先储入、任务扣款、返本付佣、推荐奖励、信用分和提现限制。此类模式在不同司法辖区可能触及支付、消费者保护、反洗钱、网络营销或其他监管要求。上线前需由法务和持牌支付机构评估,不应直接复制其资金规则、免责条款或客服收款模式。")
|
||||
add_table(doc, ["风险", "参考站点表现", "新系统要求", "优先级"], [
|
||||
("资金真实性", "客服提供收款方式,失败责任不清", "使用合规支付通道、订单化收款与自动对账", "P0"),
|
||||
("诱导与不透明", "高等级对应更高储入和收益", "明确费用、风险、无保证收益,禁止误导", "P0"),
|
||||
("推荐奖励", "三级佣金 16% 8% 4%", "法务审查后决定是否启用", "P0"),
|
||||
("账户冻结", "规则含冻结、补差额、信用分", "冻结需合法依据、通知、申诉和复核", "P0"),
|
||||
("内容安全", "商品名出现脚本标签文本", "CMS 白名单与持久型 XSS 防护", "P0"),
|
||||
("密码安全", "前端无盐 MD5", "服务端强哈希、MFA、频控", "P0"),
|
||||
("隐私", "收集银行账户和收款码", "最小化、加密、授权、删除与访问审计", "P0"),
|
||||
("数据一致性", "筛选短暂空列表、规则口径差异", "单一配置源、请求竞态处理、自动化测试", "P1"),
|
||||
], [1.0, 2.0, 2.9, 0.65])
|
||||
|
||||
heading(doc, "11 埋点与运营指标", 1)
|
||||
add_table(doc, ["事件", "触发时机", "关键属性"], [
|
||||
("auth_login_result", "登录成功或失败", "result, errorCode, deviceRisk"),
|
||||
("home_shortcut_click", "点击首页快捷入口", "target, userLevel"),
|
||||
("order_claim_click", "点击领取", "eligibilityResult, balanceBucket, level"),
|
||||
("order_state_change", "订单状态变化", "orderNo, from, to, reason"),
|
||||
("deposit_submit", "提交储入申请", "amountBucket, channel, result"),
|
||||
("withdraw_submit", "提交取出申请", "amountBucket, fee, result"),
|
||||
("support_open", "打开客服会话", "provider, entry, workStatus"),
|
||||
("content_view", "查看规则或公告", "contentId, version, readDuration"),
|
||||
("security_challenge", "触发安全验证", "scene, result, ruleId"),
|
||||
], [1.35, 2.2, 3.0])
|
||||
p(doc, "核心指标包括注册转化、首个任务完成率、领取成功率、订单超时率、储入成功率、取出成功率与处理时长、客服转人工率、异常账户率、账务差错率和投诉率。涉及金额与身份的数据只做区间化或匿名化统计。")
|
||||
|
||||
heading(doc, "12 验收标准", 1)
|
||||
for idx, item in enumerate([
|
||||
"未登录用户只能访问登录、注册和公开协议;受保护路由统一拦截。",
|
||||
"同一用户重复点击领取或提交订单,不产生重复业务单和重复资金流水。",
|
||||
"订单完成后,订单状态、钱包余额、三类流水和统计数据在一次事务或可验证最终一致性中完成。",
|
||||
"储入与取出所有状态均可在记录页查询,失败和驳回有明确原因。",
|
||||
"等级规则由后台发布后前台一致生效,表格、领取校验与提现限制不存在口径差异。",
|
||||
"手机号、银行卡、邀请码和交易密码不出现在客户端日志、服务端普通日志或分析平台。",
|
||||
"富文本、商品名、公告和客服消息中的脚本或危险 HTML 不能执行。",
|
||||
"接口 401、403、429、5xx、超时和断网场景均有清晰恢复路径。",
|
||||
"移动端 320 至 430 像素宽度可用,底部导航不遮挡内容,键盘弹出时表单按钮可达。",
|
||||
"关键资金和权限操作在后台可按用户、业务单号和时间完整审计。",
|
||||
], 1):
|
||||
p(doc, f"{idx}. {item}")
|
||||
|
||||
heading(doc, "13 交付优先级与里程碑", 1)
|
||||
add_table(doc, ["阶段", "范围", "退出条件"], [
|
||||
("MVP 认证与账户", "登录注册、资料、钱包、台账、内容、客服", "权限、安全、审计基线通过"),
|
||||
("MVP 任务闭环", "任务列表、领取、详情、提交、状态机、佣金", "幂等和账务一致性测试通过"),
|
||||
("资金闭环", "储入、取出、收款绑定、支付通道、对账", "支付与法务合规评审通过"),
|
||||
("运营能力", "等级、公告、横幅、商品、通知、后台", "配置版本和发布审核可用"),
|
||||
("增长与优化", "邀请关系、数据看板、体验优化", "合规批准且核心指标稳定"),
|
||||
], [1.1, 3.55, 2.0])
|
||||
|
||||
page_break(doc)
|
||||
heading(doc, "14 低保真原型", 1)
|
||||
p(doc, "以下原型根据实测页面重新绘制,用于确认页面层级、组件位置和交互入口,不复制原站品牌素材、头像或敏感数据。视觉设计阶段应重新建立色彩、字体、图标和组件规范。")
|
||||
for idx, fig in enumerate(figures, 1):
|
||||
add_picture(doc, fig, f"图 {idx} 参考站点功能复刻原型", width=6.2, page_break_before=(idx > 1))
|
||||
|
||||
final_heading = heading(doc, "15 待确认事项", 1)
|
||||
final_heading.paragraph_format.page_break_before = True
|
||||
for item in [
|
||||
"目标产品是否真实涉及用户资金储入与取出,还是仅做演示积分。",
|
||||
"任务来源、商品真实性、结算主体和佣金承担方。",
|
||||
"目标市场与适用法律、支付通道和实名要求。",
|
||||
"是否保留会员等级、推荐奖励、信用分和冻结机制。",
|
||||
"客服使用自建系统还是第三方服务,允许传输哪些用户数据。",
|
||||
"管理后台的组织结构、审批层级和财务对账责任人。",
|
||||
"品牌名、语言版本、视觉方向与是否需要 App 壳或 PWA。",
|
||||
]:
|
||||
bullet(doc, item)
|
||||
p(doc, "在上述事项明确前,可以先完成无资金风险的认证、内容、任务展示、客服、账户明细和后台配置原型;资金接口只做沙箱与模拟数据。")
|
||||
|
||||
doc.core_properties.title = "移动端任务与账户平台需求文档"
|
||||
doc.core_properties.subject = "参考站点功能拆解与重构建议"
|
||||
doc.core_properties.author = "Product Requirement Analysis"
|
||||
doc.core_properties.keywords = "需求文档, 移动端, 任务订单, 账户, 接口, 原型"
|
||||
doc.save(OUT_DOCX)
|
||||
print(OUT_DOCX)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
build_doc()
|
||||
@@ -0,0 +1,146 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
from docx import Document
|
||||
from docx.oxml.ns import qn
|
||||
from docx.table import Table
|
||||
from docx.text.paragraph import Paragraph
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
DOC_DIR = ROOT / "docs" / "admin_reference_prd"
|
||||
SOURCE = DOC_DIR / "后台管理系统需求文档.docx"
|
||||
TARGET = DOC_DIR / "后台管理系统需求文档.md"
|
||||
ASSET_DIR = DOC_DIR / "assets"
|
||||
|
||||
IMAGE_ALTS = {
|
||||
"prototype_01_dashboard.png": "后台首页看板脱敏原型",
|
||||
"prototype_02_member_list.png": "会员列表脱敏原型",
|
||||
"prototype_03_member_modal.png": "会员编辑弹窗原型",
|
||||
"prototype_04_withdrawal.png": "提现审核脱敏原型",
|
||||
"prototype_05_rbac.png": "RBAC 角色权限原型",
|
||||
"prototype_06_flow.png": "C 端与后台业务闭环原型",
|
||||
}
|
||||
|
||||
|
||||
def iter_blocks(document: Document):
|
||||
for child in document.element.body.iterchildren():
|
||||
if child.tag == qn("w:p"):
|
||||
yield Paragraph(child, document)
|
||||
elif child.tag == qn("w:tbl"):
|
||||
yield Table(child, document)
|
||||
|
||||
|
||||
def media_map(document: Document) -> dict[str, str]:
|
||||
assets_by_hash = {
|
||||
hashlib.sha256(path.read_bytes()).hexdigest(): path.name
|
||||
for path in ASSET_DIR.glob("*.png")
|
||||
}
|
||||
mapping: dict[str, str] = {}
|
||||
for rel_id, rel in document.part.rels.items():
|
||||
if "image" not in rel.reltype:
|
||||
continue
|
||||
digest = hashlib.sha256(rel.target_part.blob).hexdigest()
|
||||
name = assets_by_hash.get(digest)
|
||||
if name:
|
||||
mapping[rel_id] = f"assets/{name}"
|
||||
return mapping
|
||||
|
||||
|
||||
def paragraph_images(paragraph: Paragraph, images: dict[str, str]) -> list[str]:
|
||||
result: list[str] = []
|
||||
for blip in paragraph._p.xpath(".//a:blip"):
|
||||
rel_id = blip.get(qn("r:embed"))
|
||||
if rel_id in images:
|
||||
result.append(images[rel_id])
|
||||
return result
|
||||
|
||||
|
||||
def escape_cell(value: str) -> str:
|
||||
return value.strip().replace("|", "\\|").replace("\n", "<br>") or "—"
|
||||
|
||||
|
||||
def table_markdown(table: Table) -> list[str]:
|
||||
rows = [[escape_cell(cell.text) for cell in row.cells] for row in table.rows]
|
||||
if not rows:
|
||||
return []
|
||||
width = max(len(row) for row in rows)
|
||||
rows = [row + ["—"] * (width - len(row)) for row in rows]
|
||||
lines = [
|
||||
"| " + " | ".join(rows[0]) + " |",
|
||||
"| " + " | ".join(["---"] * width) + " |",
|
||||
]
|
||||
lines.extend("| " + " | ".join(row) + " |" for row in rows[1:])
|
||||
return lines
|
||||
|
||||
|
||||
def github_slug(title: str) -> str:
|
||||
slug = title.strip().lower()
|
||||
slug = re.sub(r"[^\w\-\u4e00-\u9fff ]", "", slug)
|
||||
return re.sub(r"\s+", "-", slug)
|
||||
|
||||
|
||||
def convert() -> None:
|
||||
document = Document(SOURCE)
|
||||
images = media_map(document)
|
||||
body: list[str] = []
|
||||
top_level: list[str] = []
|
||||
image_number = 0
|
||||
|
||||
for block in iter_blocks(document):
|
||||
if isinstance(block, Table):
|
||||
body.extend(table_markdown(block))
|
||||
body.append("")
|
||||
continue
|
||||
|
||||
text = block.text.strip()
|
||||
style = block.style.name if block.style else "Normal"
|
||||
embedded = paragraph_images(block, images)
|
||||
|
||||
if style == "Title" and text:
|
||||
body.extend([f"# {text}", ""])
|
||||
elif style == "Heading 1" and text:
|
||||
top_level.append(text)
|
||||
body.extend([f"## {text}", ""])
|
||||
elif style == "Heading 2" and text:
|
||||
body.extend([f"### {text}", ""])
|
||||
elif style == "List Bullet" and text:
|
||||
body.extend([f"- {text}", ""])
|
||||
elif style == "List Number" and text:
|
||||
body.extend([f"1. {text}", ""])
|
||||
elif text:
|
||||
if text.startswith("结论:"):
|
||||
body.extend([f"> **{text[:3]}** {text[3:]}", ""])
|
||||
elif text.startswith("P0:"):
|
||||
body.extend([f"> **P0 风险:** {text[3:]}", ""])
|
||||
else:
|
||||
body.extend([text, ""])
|
||||
|
||||
for path in embedded:
|
||||
image_number += 1
|
||||
alt = IMAGE_ALTS.get(Path(path).name, f"需求原型图 {image_number}")
|
||||
body.extend([f"", ""])
|
||||
|
||||
# Word 封面之后放置 Markdown 可点击目录,去掉 Word 中无意义的空段落。
|
||||
first_section = next(i for i, line in enumerate(body) if line.startswith("## 1 "))
|
||||
toc = ["## 目录", ""]
|
||||
toc.extend(f"- [{title}](#{github_slug(title)})" for title in top_level)
|
||||
toc.extend(["", "---", ""])
|
||||
output = body[:first_section] + toc + body[first_section:]
|
||||
|
||||
compact: list[str] = []
|
||||
for line in output:
|
||||
if line == "" and compact and compact[-1] == "":
|
||||
continue
|
||||
compact.append(line)
|
||||
rendered = "\n".join(compact).rstrip() + "\n"
|
||||
rendered = re.sub(r"(^- .+)\n\n(?=- )", r"\1\n", rendered, flags=re.MULTILINE)
|
||||
rendered = re.sub(r"(^1\. .+)\n\n(?=1\. )", r"\1\n", rendered, flags=re.MULTILINE)
|
||||
TARGET.write_text(rendered, encoding="utf-8")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
convert()
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0"?>
|
||||
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
|
||||
<fontconfig>
|
||||
<dir>/System/Library/Fonts</dir>
|
||||
<dir>/System/Library/Fonts/Supplemental</dir>
|
||||
<dir>/Library/Fonts</dir>
|
||||
<cachedir>/private/tmp/codex-font-cache</cachedir>
|
||||
</fontconfig>
|
||||
@@ -0,0 +1,302 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from docx import Document
|
||||
from docx.enum.table import WD_CELL_VERTICAL_ALIGNMENT, WD_TABLE_ALIGNMENT
|
||||
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
||||
from docx.oxml import OxmlElement
|
||||
from docx.oxml.ns import qn
|
||||
from docx.shared import Inches, Pt, RGBColor
|
||||
from docx.text.paragraph import Paragraph
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SOURCE = ROOT / "docs" / "reference_site_prd" / "移动端任务与账户平台需求文档.docx"
|
||||
OUT_DIR = ROOT / "docs" / "reference_site_prd_v2"
|
||||
OUTPUT = OUT_DIR / "移动端任务与账户平台需求文档 V2.docx"
|
||||
|
||||
INK = "18202F"
|
||||
MUTED = "667085"
|
||||
GRID = "D9D9D9"
|
||||
|
||||
|
||||
def set_font(run, size=10.5, bold=False, color=INK):
|
||||
run.font.name = "Heiti SC"
|
||||
run._element.get_or_add_rPr().rFonts.set(qn("w:eastAsia"), "Heiti SC")
|
||||
run.font.size = Pt(size)
|
||||
run.font.bold = bold
|
||||
run.font.color.rgb = RGBColor.from_string(color)
|
||||
|
||||
|
||||
def replace_all(doc, old: str, new: str):
|
||||
for paragraph in list(doc.paragraphs) + list(doc.sections[0].header.paragraphs):
|
||||
if old not in paragraph.text:
|
||||
continue
|
||||
full = paragraph.text.replace(old, new)
|
||||
for run in paragraph.runs:
|
||||
run.text = ""
|
||||
run = paragraph.runs[0] if paragraph.runs else paragraph.add_run()
|
||||
run.text = full
|
||||
style = paragraph.style.name if paragraph.style else ""
|
||||
if style == "Title":
|
||||
set_font(run, 25, True, "000000")
|
||||
elif style.startswith("Heading"):
|
||||
set_font(run, 14, True, "000000")
|
||||
else:
|
||||
set_font(run, 10.5)
|
||||
|
||||
|
||||
def find_paragraph(doc, exact: str):
|
||||
for p in doc.paragraphs:
|
||||
if p.text.strip() == exact:
|
||||
return p
|
||||
raise ValueError(f"Paragraph not found: {exact}")
|
||||
|
||||
|
||||
def add_before(anchor, text: str, style: str | None = None, size=10.5, bold=False, color=INK,
|
||||
space_before=0, space_after=6):
|
||||
p = OxmlElement("w:p")
|
||||
anchor._p.addprevious(p)
|
||||
paragraph = Paragraph(p, anchor._parent)
|
||||
if style:
|
||||
paragraph.style = style
|
||||
paragraph.paragraph_format.space_before = Pt(space_before)
|
||||
paragraph.paragraph_format.space_after = Pt(space_after)
|
||||
paragraph.paragraph_format.line_spacing = 1.25
|
||||
set_font(paragraph.add_run(text), size, bold, color)
|
||||
return paragraph
|
||||
|
||||
|
||||
def add_heading_before(anchor, text: str, level=2):
|
||||
sizes = {1: 18, 2: 14, 3: 11.5}
|
||||
return add_before(anchor, text, f"Heading {level}", sizes[level], True, "000000", 10, 6)
|
||||
|
||||
|
||||
def add_bullet_before(anchor, text: str):
|
||||
return add_before(anchor, text, "List Bullet", 10.2, False, INK, 0, 3)
|
||||
|
||||
|
||||
def set_cell(cell, text, header=False, shade=None, align=WD_ALIGN_PARAGRAPH.LEFT, size=8.6):
|
||||
cell.text = str(text)
|
||||
cell.vertical_alignment = WD_CELL_VERTICAL_ALIGNMENT.CENTER
|
||||
tc_pr = cell._tc.get_or_add_tcPr()
|
||||
mar = OxmlElement("w:tcMar")
|
||||
for side in ("top", "start", "bottom", "end"):
|
||||
node = OxmlElement(f"w:{side}")
|
||||
node.set(qn("w:w"), "100")
|
||||
node.set(qn("w:type"), "dxa")
|
||||
mar.append(node)
|
||||
tc_pr.append(mar)
|
||||
borders = OxmlElement("w:tcBorders")
|
||||
for edge in ("top", "left", "bottom", "right"):
|
||||
node = OxmlElement(f"w:{edge}")
|
||||
node.set(qn("w:val"), "single")
|
||||
node.set(qn("w:sz"), "6")
|
||||
node.set(qn("w:color"), GRID)
|
||||
borders.append(node)
|
||||
tc_pr.append(borders)
|
||||
if shade:
|
||||
shd = OxmlElement("w:shd")
|
||||
shd.set(qn("w:fill"), shade)
|
||||
tc_pr.append(shd)
|
||||
p = cell.paragraphs[0]
|
||||
p.alignment = align
|
||||
p.paragraph_format.space_after = Pt(0)
|
||||
for run in p.runs:
|
||||
set_font(run, size, header, "FFFFFF" if header else INK)
|
||||
|
||||
|
||||
def table_before(doc, anchor, headers, rows, widths):
|
||||
table = doc.add_table(rows=1, cols=len(headers))
|
||||
table.alignment = WD_TABLE_ALIGNMENT.CENTER
|
||||
table.autofit = False
|
||||
table.rows[0]._tr.get_or_add_trPr().append(OxmlElement("w:cantSplit"))
|
||||
repeat = OxmlElement("w:tblHeader")
|
||||
repeat.set(qn("w:val"), "true")
|
||||
table.rows[0]._tr.get_or_add_trPr().append(repeat)
|
||||
for i, h in enumerate(headers):
|
||||
set_cell(table.rows[0].cells[i], h, True, "334E7D", WD_ALIGN_PARAGRAPH.CENTER)
|
||||
for ridx, row in enumerate(rows):
|
||||
tr = table.add_row()
|
||||
tr._tr.get_or_add_trPr().append(OxmlElement("w:cantSplit"))
|
||||
for i, value in enumerate(row):
|
||||
set_cell(tr.cells[i], value, False, "F3F6FB" if ridx % 2 else None,
|
||||
WD_ALIGN_PARAGRAPH.CENTER if i == 0 else WD_ALIGN_PARAGRAPH.LEFT)
|
||||
for row in table.rows:
|
||||
for i, width in enumerate(widths):
|
||||
row.cells[i].width = Inches(width)
|
||||
anchor._p.addprevious(table._tbl)
|
||||
return table
|
||||
|
||||
|
||||
def add_revision_summary(doc):
|
||||
anchor = find_paragraph(doc, "1 文档范围与方法")
|
||||
add_heading_before(anchor, "0 后台反向校验结论", 1)
|
||||
add_before(anchor,
|
||||
"后台走查表明,原 V1 已覆盖账户、任务订单、储入取出和客服等主链路,但对后台可配置项、人工审核、任务大厅、会员限制和运营动作的用户端反馈描述不足。V2 将后台视为业务规则的管理端,并补齐移动端必须展示的状态、原因、时效和恢复路径。")
|
||||
table_before(doc, anchor,
|
||||
["校验主题", "后台证据", "V1 缺口", "V2 调整"],
|
||||
[
|
||||
("任务模型", "商品订单、任务大厅、任务列表、会员任务审核并存", "容易把所有任务理解为自动领取订单", "拆分自动订单与人工任务两条链路"),
|
||||
("资金审核", "充值与提现支持人工通过或驳回", "状态虽有定义,但用户通知与补件不够明确", "增加审核进度、驳回原因、补件和到账凭证"),
|
||||
("会员治理", "后台可改等级、禁用、账变和奖励", "移动端缺少被限制后的统一反馈", "增加受限状态页、原因、期限、申诉和台账说明"),
|
||||
("运营配置", "营业时间、金额门槛、页面开关、Banner 和文本可配置", "前台规则散落在页面描述中", "建立配置版本、发布时间和客户端降级契约"),
|
||||
("内容真实性", "存在假人和弹幕入口", "只提出风险,未明确产品取舍", "生产环境不展示伪造用户行为;演示数据必须标识"),
|
||||
("异常恢复", "任务大厅接口异常,客服页可能空白", "通用错误处理不够落地", "每个核心入口增加错误码、重试和备用路径"),
|
||||
], [1.0, 2.0, 1.9, 2.0])
|
||||
add_before(anchor, "V2 的核心原则是:后台每一次影响用户权益的操作,都必须在移动端形成可解释、可追踪、可申诉的结果。", size=10.5, bold=True)
|
||||
|
||||
|
||||
def add_business_closure(doc):
|
||||
anchor = find_paragraph(doc, "5 页面级功能需求")
|
||||
add_heading_before(anchor, "4.4 后台操作与移动端反馈闭环", 2)
|
||||
for text in [
|
||||
"后台修改等级、佣金、每日次数或提现限制时,移动端必须显示规则版本和生效时间。进行中的订单继续使用创建时的规则快照。",
|
||||
"后台审核充值、提现或会员任务后,移动端记录页同步显示审核节点、处理时间、结果和原因,并通过站内通知提醒用户。",
|
||||
"后台禁用、冻结或限制会员时,移动端不得只表现为接口失败。系统应进入受限状态页,说明受影响功能、期限、原因类别和申诉入口。",
|
||||
"后台执行余额调整或奖励发放时,移动端账户明细必须产生独立流水,展示业务单号、变动方向、原因摘要和处理主体类型。",
|
||||
"后台下线 Banner、内容、商品、任务或客服入口后,移动端按配置版本刷新。已打开的页面应显示已下线状态,不得继续提交。",
|
||||
]:
|
||||
add_bullet_before(anchor, text)
|
||||
|
||||
# The V1 builder placed an explicit page break between sections 4 and 5.
|
||||
# The new 4.4 content now occupies that boundary, so remove the old break to
|
||||
# avoid leaving section 4.3 alone on a mostly empty page.
|
||||
paragraphs = doc.paragraphs
|
||||
closure = find_paragraph(doc, "4.4 后台操作与移动端反馈闭环")
|
||||
closure_index = next(i for i, paragraph in enumerate(paragraphs) if paragraph._p is closure._p)
|
||||
for candidate in reversed(paragraphs[:closure_index]):
|
||||
if candidate._p.xpath('.//w:br[@w:type="page"]'):
|
||||
candidate._element.getparent().remove(candidate._element)
|
||||
break
|
||||
|
||||
|
||||
def add_mobile_pages(doc):
|
||||
anchor = find_paragraph(doc, "6 数据模型与状态机")
|
||||
add_heading_before(anchor, "5.11 任务大厅与会员任务", 2)
|
||||
add_before(anchor, "后台同时存在任务大厅、任务列表和会员任务审核,因此移动端需要把人工任务与自动领取订单分开设计,避免共用状态和文案。")
|
||||
table_before(doc, anchor,
|
||||
["环节", "移动端需求", "后台对应", "关键状态"],
|
||||
[
|
||||
("任务大厅", "展示任务名称、图标、奖励、适用等级、剩余名额和截止时间", "任务大厅配置", "未开始、可申请、已满、已结束、已下线"),
|
||||
("任务详情", "展示说明、材料要求、审核标准、预计时长和风险提示", "任务列表", "可申请、不可申请、已申请"),
|
||||
("提交材料", "支持文本、图片或文件,提交前预览并可撤回", "会员任务审核", "草稿、已提交、审核中"),
|
||||
("审核结果", "显示通过、驳回或补件原因,允许查看历史版本", "会员任务审核", "通过、驳回、待补件、已取消"),
|
||||
("奖励入账", "审核通过后生成奖励流水和通知", "奖励及账变", "待入账、已入账、入账失败"),
|
||||
], [0.9, 2.55, 1.45, 1.85])
|
||||
add_heading_before(anchor, "5.12 会员受限与运营通知", 2)
|
||||
for text in [
|
||||
"会员状态至少包括正常、临时限制、冻结、禁用和注销中;登录后优先展示状态说明,而不是让各页面分别报错。",
|
||||
"站内通知按系统公告、审核结果、资金结果、安全提醒和规则变更分类,重要通知保留已读时间和内容版本。",
|
||||
"荣誉榜、滚动消息和弹幕只能使用可证明的真实业务数据。测试或演示数据必须与生产数据隔离并明确标识。",
|
||||
"客服入口不可用时,显示服务时间、错误编号、重试按钮和经审核的备用渠道;不得把空白 iframe 作为最终状态。",
|
||||
]:
|
||||
add_bullet_before(anchor, text)
|
||||
|
||||
|
||||
def add_contracts(doc):
|
||||
anchor = find_paragraph(doc, "8 管理后台需求")
|
||||
add_heading_before(anchor, "7.2 后台配置与移动端契约", 2)
|
||||
table_before(doc, anchor,
|
||||
["后台配置", "移动端消费方式", "校验与降级"],
|
||||
[
|
||||
("营业与交易时间", "展示当前可用状态和下一开放时间", "结束时间必须晚于开始时间;配置无效时关闭提交并告警"),
|
||||
("金额与次数门槛", "表单提示、按钮状态和服务端校验使用同一版本", "客户端仅展示,服务端最终裁决"),
|
||||
("会员等级", "等级页、领取资格、佣金和提现限制统一读取", "订单创建时保存规则快照"),
|
||||
("Banner 与文本", "按排序、有效期、语言和跳转类型渲染", "危险链接拦截;富文本白名单"),
|
||||
("商品与任务", "仅展示已发布且适用于当前用户的条目", "已下线条目不可新建申请,历史记录仍可查看"),
|
||||
("客服入口", "按在线状态、时间和排序展示", "第三方失败时回退到备用联系方式"),
|
||||
], [1.35, 3.0, 2.4])
|
||||
add_before(anchor, "所有配置响应应包含 configVersion、effectiveAt 和 updatedAt。客户端缓存必须按版本失效,关键交易提交时还要携带规则版本,由服务端拒绝过期规则并返回可读提示。")
|
||||
|
||||
|
||||
def add_acceptance(doc):
|
||||
anchor = find_paragraph(doc, "13 交付优先级与里程碑")
|
||||
add_heading_before(anchor, "12.1 后台联动补充验收", 2)
|
||||
for idx, text in enumerate([
|
||||
"后台发布新等级规则后,移动端在生效时间后展示新规则;存量订单仍按原规则结算。",
|
||||
"后台驳回充值、提现或任务申请后,移动端显示同一原因码、用户文案、处理时间和补救入口。",
|
||||
"后台冻结会员后,移动端所有受影响写操作均被服务端拒绝,并统一跳转受限状态页;解除后无需重新安装或清缓存。",
|
||||
"后台余额调整和奖励发放均生成不可变流水;移动端可从通知跳转至对应流水详情。",
|
||||
"任务大厅服务不可用时,移动端保留其他导航功能,显示错误编号并支持重试,不出现空白页或无限加载。",
|
||||
"客服第三方页面失败时,移动端展示备用渠道,且不泄露令牌、手机号、银行卡或完整会话参数。",
|
||||
"后台下线商品或任务后,新申请立即禁止;已申请记录仍可查看状态和审核结果。",
|
||||
"生产环境的榜单、弹幕和消息不得混入测试账号或伪造行为数据。",
|
||||
], 11):
|
||||
add_before(anchor, f"{idx}. {text}")
|
||||
|
||||
|
||||
def add_pending_questions(doc):
|
||||
anchor = find_paragraph(doc, "在上述事项明确前,可以先完成无资金风险的认证、内容、任务展示、客服、账户明细和后台配置原型;资金接口只做沙箱与模拟数据。")
|
||||
add_heading_before(anchor, "15.1 后台联动待确认", 2)
|
||||
for text in [
|
||||
"任务大厅与自动领取订单是否属于两个独立业务,还是同一任务的不同入口。",
|
||||
"充值、提现和会员任务的驳回原因由固定字典选择,还是允许运营自由输入。",
|
||||
"会员禁用、冻结、等级调整和余额调整分别由哪些角色发起、复核与申诉处理。",
|
||||
"规则配置采用立即生效、定时发布还是审批后发布;存量订单使用哪一版规则。",
|
||||
"荣誉榜与弹幕是否保留。若保留,只能展示真实、可审计且已脱敏的数据。",
|
||||
]:
|
||||
add_bullet_before(anchor, text)
|
||||
|
||||
|
||||
def remove_page_break_before(doc, heading_text: str):
|
||||
paragraphs = doc.paragraphs
|
||||
target = find_paragraph(doc, heading_text)
|
||||
target_index = next(i for i, paragraph in enumerate(paragraphs) if paragraph._p is target._p)
|
||||
for candidate in reversed(paragraphs[:target_index]):
|
||||
if candidate._p.xpath('.//w:br[@w:type="page"]'):
|
||||
candidate._element.getparent().remove(candidate._element)
|
||||
return
|
||||
|
||||
|
||||
def normalize_layout(doc):
|
||||
# Fill the otherwise nearly empty customer-service page and reset inherited
|
||||
# indentation that clipped the first digit of a top-of-page heading.
|
||||
remove_page_break_before(doc, "5.7 我的与个人信息")
|
||||
remove_page_break_before(doc, "\u20605.10 账户明细与记录")
|
||||
for text in ("5.7 我的与个人信息", "5.9 取出与收款绑定", "5.10 账户明细与记录"):
|
||||
paragraph = next(p for p in doc.paragraphs if p.text.strip().endswith(text))
|
||||
paragraph.paragraph_format.left_indent = Inches(0)
|
||||
paragraph.paragraph_format.first_line_indent = Inches(0)
|
||||
|
||||
# The source document predates the table-row split safeguard used by the
|
||||
# backend PRD. Apply it globally so milestone and interface rows do not break
|
||||
# across pages.
|
||||
for table in doc.tables:
|
||||
for row in table.rows:
|
||||
tr_pr = row._tr.get_or_add_trPr()
|
||||
if not tr_pr.xpath('./w:cantSplit'):
|
||||
tr_pr.append(OxmlElement("w:cantSplit"))
|
||||
|
||||
|
||||
def update_metadata(doc):
|
||||
doc.core_properties.title = "移动端任务与账户平台需求文档 V2"
|
||||
doc.core_properties.subject = "基于后台功能的移动端需求反向校验修订版"
|
||||
doc.core_properties.author = "Product Requirement Analysis"
|
||||
doc.core_properties.keywords = "需求文档, 移动端, 后台联动, 任务, 资金, 审核, 原型"
|
||||
|
||||
|
||||
def main():
|
||||
OUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
doc = Document(SOURCE)
|
||||
replace_all(doc, "移动端任务与账户平台需求文档", "移动端任务与账户平台需求文档 V2")
|
||||
replace_all(doc, "参考站点功能拆解与重构建议", "后台反向校验修订版")
|
||||
replace_all(doc, "文档版本:V1.0", "文档版本:V2.0")
|
||||
replace_all(doc,
|
||||
"结论:参考站点由账户、任务订单、余额台账、储入取出、会员等级、内容展示和在线客服七个域构成。建议复刻页面结构与业务闭环,但重写资金、隐私、安全和合规机制。",
|
||||
"结论:移动端主链路可以保留,但必须新增人工任务、审核结果、会员受限、配置版本和后台账变反馈。资金、权限、隐私和内容真实性仍按高风险能力重构。")
|
||||
add_revision_summary(doc)
|
||||
add_business_closure(doc)
|
||||
add_mobile_pages(doc)
|
||||
add_contracts(doc)
|
||||
add_acceptance(doc)
|
||||
add_pending_questions(doc)
|
||||
normalize_layout(doc)
|
||||
update_metadata(doc)
|
||||
doc.save(OUTPUT)
|
||||
print(OUTPUT)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,23 @@
|
||||
from docx import Document
|
||||
from docx.oxml.ns import qn
|
||||
|
||||
names = [
|
||||
"Heiti SC",
|
||||
"STHeiti",
|
||||
"Hiragino Sans GB",
|
||||
"Songti SC",
|
||||
"Arial Unicode MS",
|
||||
"PingFang SC",
|
||||
"Microsoft YaHei",
|
||||
"SimSun",
|
||||
"Noto Sans CJK SC",
|
||||
]
|
||||
doc = Document()
|
||||
for name in names:
|
||||
p = doc.add_paragraph()
|
||||
r = p.add_run(name + " 中文字体测试 移动端任务与账户平台")
|
||||
r.font.name = name
|
||||
r._element.get_or_add_rPr().rFonts.set(qn("w:ascii"), name)
|
||||
r._element.get_or_add_rPr().rFonts.set(qn("w:hAnsi"), name)
|
||||
r._element.get_or_add_rPr().rFonts.set(qn("w:eastAsia"), name)
|
||||
doc.save("docs/reference_site_prd/font_test.docx")
|
||||
Reference in New Issue
Block a user