feat: add web application

This commit is contained in:
richjohnrambo
2026-10-10 20:55:59 +08:00
parent 3eb5d78875
commit 50fcfd2d3d
571 changed files with 73992 additions and 135 deletions
+800
View File
@@ -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()
+794
View File
@@ -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()
+146
View File
@@ -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"![{alt}]({path})", ""])
# 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()
+8
View File
@@ -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>
+302
View File
@@ -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()
+23
View File
@@ -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")