AI智能结算助手 Pro v2.1.1
融合AI规则解析与高效数据处理的专业结算工具,让复杂活动结算变得简单智能。
⚠️ 执行规则(最高优先级,必须遵守)
核心原则:本Skill在文档末尾内嵌了完整可执行的结算引擎代码,AI必须直接复制该代码块到执行环境中运行,严禁自行手写替代代码。
强制要求
- 必须使用内嵌的结算引擎代码:文档末尾"结算引擎代码"章节包含完整实现,所有结算逻辑(数据加载、条件过滤、金额计算、CSV导出)均已就绪,不得另行实现。
- 禁止手动计算奖金:不得用
round()、手写除法或任何自定义方式计算每人奖金,必须调用内嵌代码中的_distribute_equal()或run_settlement()。 - 禁止自定义精度处理:内嵌代码已内置
Decimal高精度算法,直接使用即可,不得重复实现。 - AI只需构造奖池配置:读懂规则后,构造
AwardPool列表传入run_settlement(),其他全部由引擎处理。
# 正确执行方式:
# 1. 复制文档末尾"完整结算引擎代码"代码块,全部执行
# 2. 根据活动规则构造 pools 列表
# 3. 调用 run_settlement(file_path, pools, ...)
# 4. 调用 export_to_csv() 导出结果
AI行为检查清单(每次结算前自查)
- 已将文档末尾的完整引擎代码块复制到执行环境并运行?
- 未自行实现任何结算逻辑(包括数据加载、金额计算)?
- 未使用
round()计算奖金? - 金额计算通过内嵌代码的
_distribute_equal()函数完成? - 已验证
每人金额 × 人数 == 奖池金额?
🚫 AI执行约束(所有操作均受此章节约束)
以下约束适用于 AI 在整个结算过程中的所有行为,包括代码生成、计算、输出。违反任何一条均视为执行错误。
0. 开始结算前:必须完整列出所有奖池
在构造任何代码之前,AI必须先从用户描述中提取并列出全部奖池,格式如下:
📋 我理解本次活动共有 X 个奖池:
奖池1: [名称] - [金额] - [类型:达标瓜分/排名奖/权重分配] - [达标条件]
奖池2: [名称] - [金额] - [类型] - [条件]
...
总奖金合计: [所有奖池金额之和]
请确认是否遗漏?确认后我将开始构造结算代码。
- ✅ 必须逐一列出,等待用户确认后再执行
- ❌ 禁止跳过此步骤直接写代码
- ❌ 禁止只构造部分奖池(如只写瓜分,遗漏榜单奖)
- ❌ 如果所有奖池金额之和与用户说的总奖金不符,必须询问用户
1. 严格按文档算法执行
- ✅ 仅使用本文档明确描述的算法和逻辑
- ❌ 禁止自行添加任何「优化」、「修正」、「调整」逻辑,即使 AI 认为这样更「合理」
- ❌ 禁止在未告知用户的情况下更改计算方式
2. 瓜分奖金额必须完全相等
- ✅ 同一奖池内,所有达标者奖金数值完全相同(字符串完全一致)
- ❌ 禁止以任何形式让某一个人的金额与其他人不同,包括:
- 给「第一个人」多分几分钱来消化差额
- 给「最后一个人」少分钱来凑整
- 以「精度误差修正」为由调整任何人的金额
- ❌ 禁止在代码中出现类似
results[0].award += remainder的逻辑
3. 不添加文档未提及的功能
- ✅ 代码实现与文档描述的逻辑保持完全一致
- ❌ 禁止添加文档未提及的过滤条件、排序规则、特殊处理逻辑
- ❌ 禁止「顺手」修复 AI 认为的潜在问题(如自动过滤异常数据行)
4. 有疑问时必须询问用户
- ✅ 规则存在歧义 → 列出歧义点并询问用户,等待明确答复后再执行
- ✅ 数据字段无法匹配 → 告知用户并询问对应关系,不得猜测
- ✅ 奖池金额无法整除 → 自动执行方案B(保留最少小数位使每人金额相同),并在输出时告知用户保留了几位小数
- ❌ 禁止在任何不确定的情况下自行假设并执行
🎯 核心能力
| 能力 | 说明 |
|---|---|
| 🧠 AI规则解析 | 用自然语言描述规则,AI自动转换为结算逻辑 |
| 🏷️ 话题词识别 | 精准匹配视频标题中的话题词,支持且/或逻辑 |
| ✅ 规则确认流程 | 返回规则理解供用户确认,可多轮修正 |
| 📊 多模式结算 | 达标瓜分、排名/榜单奖、混合多奖池并行、权重分配 |
| 📂 多格式数据源 | CSV 和 Excel (xlsx/xls),支持多 Sheet 合并 |
| ⚡ 高效处理 | 本地Python处理,万级数据秒级完成 |
| 🛡️ 数据安全 | 原始数据不上传,仅规则描述发送至AI |
| 🔧 健壮可靠 | 超时自动重试、分级错误提示、断点恢复 |
🚀 快速开始
方式一:自然语言直接结算(推荐)
请帮我结算这个活动:
总奖金2万元,发布作品≥5条且播放量≥3万,必须携带话题#春节活动的作者等额瓜分
快捷触发词:发送 「一组结算」 可直接进入结算流程,无需其他前缀。 同样有效的触发词:「帮我结算」「请结算」「结算活动」「一组结算」
话题词说明:
- 可以是任意话题,如:
#春节活动、#新品发布、#挑战赛、#品牌联名等 - 系统会自动从你的描述中提取
#开头的话题词 - 支持多个话题词组合(且/或关系)
方式二:分步操作
# 步骤1:打开结算助手
打开AI结算助手
# 步骤2:描述规则(包含话题词要求)
规则:播放量≥3万,同时携带话题 #春节活动 和 #新年优惠 的作者瓜分2万奖金
# 注意:话题词可以是任意内容,根据你的活动需求自定义
# 步骤3:确认规则
查看AI返回的规则理解,确认或修改
# 步骤4:上传数据
[上传Excel/CSV文件,必须包含视频标题字段]
# 步骤5:获取结果
下载结算结果
💬 自然语言规则示例
✅ 达标瓜分模式
总奖金10万元,完成量≥100的用户等额瓜分奖池
设置两个奖池:
- 基础奖池5万:完成量≥50的用户瓜分
- 进阶奖池5万:完成量≥100的用户瓜分
⚠️ 分档瓜分:必须询问是否互斥
当规则包含多档奖池(如1档/2档/3档)时,在规则确认阶段必须主动询问用户档位是否互斥,不得自行假设,不得跳过询问直接结算。
询问时机与格式
在返回规则理解供用户确认时,若检测到多档奖池结构,必须在确认信息末尾加入以下问题:
❓ 档位关系确认(必须回答后才能继续):
您的规则包含多档奖池,请确认档位关系:
A. 【累进叠加】满足高档条件的作者同时获得所有低档奖励
例:发7条 → 同时获得1档+2档+3档奖励
B. 【互斥独享】每位作者只能获得其满足的最高档奖励
例:发7条 → 只获得3档奖励
请回复 A 或 B,或直接说明。
根据用户回答处理
用户选A(累进叠加):各档独立判断资格,结算代码对每档独立循环,不使用 elif:
# ✅ 累进写法:各档独立判断
for tier in [tier1, tier2, tier3]:
qualified = [a for a in authors if a.valid_videos >= tier.min_videos]
per_person = tier.pool / len(qualified)
for a in qualified:
a.awards[tier.name] = per_person
| 作者有效视频数 | 参与1档(≥2条) | 参与2档(≥4条) | 参与3档(≥7条) | 总奖金 |
|---|---|---|---|---|
| 2条 | ✅ | ❌ | ❌ | 1档金额 |
| 4条 | ✅ | ✅ | ❌ | 1档+2档 |
| 7条 | ✅ | ✅ | ✅ | 1档+2档+3档 |
用户选B(互斥独享):每位作者只取满足的最高档,结算代码使用 elif 链从高到低判断:
# ✅ 互斥写法:从高档到低档判断
for a in authors:
if a.valid_videos >= tier3.min_videos:
a.awards['奖励'] = tier3.pool / qualified3_count
elif a.valid_videos >= tier2.min_videos:
a.awards['奖励'] = tier2.pool / qualified2_count
elif a.valid_videos >= tier1.min_videos:
a.awards['奖励'] = tier1.pool / qualified1_count
✅ 带话题词要求的瓜分模式
支持任意话题词,动态配置
总奖金2万元,发布作品≥5条且播放量≥3万,必须携带话题 #你的话题 的作者等额瓜分
奖池5万元,播放量≥10万,同时携带话题 #话题A 和 #话题B 的作者瓜分
总奖金3万,作品≥3条,携带话题 #话题1 或 #话题2 的用户瓜分
话题词规则说明:
- 🎯 动态配置:支持任意话题词,用户自定义(如 #春节活动、#新品发布、#品牌联名 等)
- ⚡ 精准匹配:话题词必须完全匹配,例如
#春节不会匹配#春节活动 - 🔗 且关系(AND):要求同时携带所有指定话题词(关键词:且/和/同时/都/AND)
- 🔗 或关系(OR):要求至少携带一个话题词(关键词:或/OR,或无明确关键词)
- 📝 自动识别:用
#开头标识话题词,AI自动提取和识别逻辑关系
话题词格式要求:
- ✅ 必须以
#开头 - ✅ 后面跟任意文字(中文、英文、数字均可)
- ✅ 遇到空格、逗号、或特殊符号自动截断
- ✅ 示例:
#春节活动、#NewYear、#2024挑战等
✅ 排名赛模式
按销售额降序排名:
- 第1名:10000元
- 第2-3名:各5000元
- 第4-10名:各2000元
✅ 混合不互斥模式
双奖池并行:
1. 达标奖池5万:播放量≥1万的作者平分
2. 排名奖池5万:按点赞数排名前20名,第1-5名5000元,第6-10名3000元,第11-20名1500元
✅ 权重分配模式
总奖金8万元,按播放量权重比例分配
🔢 精确瓜分算法(必须遵守)
核心要求:每人金额必须严格相等,不允许有人多有人少。奖池金额必须被彻底瓜分,不多不少。
算法约束(与上方「AI执行约束」共同生效)
| 约束 | 说明 |
|---|---|
| ✅ 所有达标者金额数值完全相同 | 同一奖池内,每人奖金字符串必须一致 |
| ❌ 禁止「差额补给第一人」 | 如 results[0].award += 0.01 绝对禁止 |
| ❌ 禁止自行截断后丢弃差额 | 截断后差额不能悄悄归零,必须报告用户 |
| ❌ 禁止以「修正精度误差」为由调整任何人金额 | 精度问题应通过方案选择解决,不是代码补丁 |
精度问题的唯一合法处理方案(方案B)
当奖池金额无法被参与人数整除时(如 30000 / 17),AI必须自动执行方案B,并在输出结果时告知用户:
⚠️ 精度提示:奖池 30,000 元 / 17 人 无法以2位小数等额整除
已自动执行方案B:保留最少小数位数使每人金额完全相同
每人 1764.705882 元(6位小数),17人合计精确等于 30,000 元
→ 所有人金额相同,奖池金额完全分完
❌ 禁止截断到2位小数后丢弃差额(即方案A不允许使用)。 ✅ 始终自动执行方案B:找到使
per × n == total成立的最小小数位数。
每人金额相等的正确实现
直接均分时保留足够小数位数,确保每人金额完全相同:
from decimal import Decimal, ROUND_HALF_UP, getcontext
def distribute_equal(total_yuan, n):
"""
等额瓜分:每人金额严格相同
- 先尝试精确整除
- 若能整除(如 5000 / 10 = 500.00),保留两位小数
- 若不能整除(如 5000 / 17),保留足够位数使每人金额完全相同
并验证 per_person * n == total_yuan
"""
getcontext().prec = 28
total = Decimal(str(total_yuan))
count = Decimal(n)
per_person = total / count # 精确除法
# 检查是否能整除到两位小数
per_2 = per_person.quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
if per_2 * count == total:
return float(per_2), 2 # 返回(每人金额, 小数位数)
# 否则找到能让 per * n == total 的最小小数位数(最多10位)
for digits in range(3, 11):
fmt = '0.' + '0' * digits
per_d = per_person.quantize(Decimal(fmt), rounding=ROUND_HALF_UP)
if per_d * count == total:
return float(per_d), digits
# 极端情况:直接返回高精度值,在表格中标注
return float(per_person), -1
# 示例
# distribute_equal(5000, 10) → (500.0, 2) 每人500.00元
# distribute_equal(5000, 17) → 需要找到能被整除的精度
# 5000/17 = 294.117647058823... → 若无法整除,按原始精确值记录,备注「无法等额整除」
若奖池无法等额整除
统一执行方案B:找到使 per × n == total 成立的最小小数位数,保持所有人金额完全相同,奖池金额精确分完。
- ❌ 不得截断到2位小数后丢弃差额
- ❌ 不得询问用户是否接受截断(方案A已废弃)
- ✅ 在输出结果时注明保留的小数位数,让用户知晓
验证规则
输出结果前必须验证:
每人金额 × 人数 == 奖池金额(精确验证,不允许浮点误差)- 验证失败则不允许输出,必须重新计算或报错
🔧 工作流程
┌─────────────────────────────────────────────────────┐
│ 用户输入自然语言规则 │
│ "总奖金2万,播放量≥3万,携带话题#春节活动的作者瓜分" │
└──────────────────────┬──────────────────────────────┘
↓
┌─────────────────────────────────────────────────────┐
│ AI自动提取话题词并解析规则 → 结构化配置 │
│ { │
│ mode: "guaranteed", │
│ pool: 20000, │
│ condition: {field: "播放量", op: ">=", value: 30000},│
│ topic_rule: {topics: ["#春节活动"], logic: "OR"} │
│ } │
└──────────────────────┬──────────────────────────────┘
↓
┌─────────────────────────────────────────────────────┐
│ 返回规则理解给用户确认 │
│ ═══════════════════════════════════════════ │
│ 📋 规则理解确认 │
│ 【奖池1】达标瓜分奖池 │
│ 💰 奖池金额: 20,000元 │
│ ✅ 达标条件: 播放量 >= 30,000 │
│ 🏷️ 话题词要求: #春节活动 │
│ ❓ 请确认以上规则理解是否正确? │
│ ═══════════════════════════════════════════ │
└──────────────────────┬──────────────────────────────┘
↓
┌─────────────────────────────────────────────────────┐
│ 用户确认/调整AI理解 │
│ • "对,但需要同时携带#春节活动和#新年优惠" │
│ → AI更新配置 → 再次返回确认 │
│ • "确认无误" → 进入下一步 │
└──────────────────────┬──────────────────────────────┘
↓
┌─────────────────────────────────────────────────────┐
│ 上传数据文件(Excel/CSV) │
│ 本地处理,数据不上传 │
└──────────────────────┬──────────────────────────────┘
↓
┌─────────────────────────────────────────────────────┐
│ 执行结算 │
│ - 数据聚合(按作者ID汇总) │
│ - 话题词检查(精准匹配) │
│ - 条件筛选 │
│ - 金额计算 │
│ - 结果生成 │
└──────────────────────┬──────────────────────────────┘
↓
┌─────────────────────────────────────────────────────┐
│ 输出结果 │
│ - 结算摘要(统计信息) │
│ - 详细名单(Excel下载) │
│ - 异常记录(如有) │
└─────────────────────────────────────────────────────┘
🏷️ 话题词识别详解
动态配置 - 支持任意话题词
话题词不是固定的,可以根据活动需求自定义配置。系统会自动从用户输入的规则中提取话题词。
支持的话题词示例:
#春节活动- 节日主题#新品发布- 产品活动#品牌联名- 营销活动#挑战赛2024- 竞赛活动#GameEvent- 英文话题#攻略- 简短话题- 任意以
#开头的文字
精准匹配规则
话题词必须完全匹配,部分匹配无效:
| 要求话题词 | 视频标题 | 是否匹配 | 说明 |
|---|---|---|---|
#春节 | "最强攻略 #春节 活动" | ✅ 匹配 | 完整包含#春节 |
#春节 | "最强攻略 #春节活动 指南" | ❌ 不匹配 | #春节活动 != #春节 |
#春节 | "活动 #春节 和 #春节活动" | ✅ 匹配 | 包含完整的#春节 |
#新品 + #发布(且) | "#新品 #发布 预告" | ✅ 匹配 | 两个都有 |
#新品 + #发布(且) | "#新品 预告视频" | ❌ 不匹配 | 缺少#发布 |
#新品 + #发布(或) | "#新品 预告视频" | ✅ 匹配 | 至少有一个 |
逻辑关系识别
且关系 (AND) - 关键词触发:
- "且"、"和"、"同时"、"都"、"AND"
- 示例:
同时携带#春节和#活动
或关系 (OR) - 关键词触发:
- "或"、"OR"
- 默认(无明确关键词时)
- 示例:
携带#话题A或#话题B
话题词提取规则
系统使用正则表达式自动提取:
# 匹配模式:# 后跟非特殊字符
topics = re.findall(r'#[^#\s,,和或]+', rule_text)
会被识别为话题词:
- ✅
#春节活动→ 提取#春节活动 - ✅
#NewYear2024→ 提取#NewYear2024 - ✅
#挑战→ 提取#挑战
不会被识别:
- ❌ 没有
#符号的文字 - ❌
#后直接跟空格、逗号等
多视频检查策略
作者只要有任意一个视频满足话题词要求即可:
作者A发布了3个视频:
视频1: "日常vlog" → 无话题词
视频2: "#春节活动 攻略" → 包含#春节活动 ✅
视频3: "游戏实况" → 无话题词
要求: 携带话题#春节活动
结果: ✅ 作者A符合要求(视频2包含)
常见应用场景
| 活动类型 | 话题词示例 | 规则示例 |
|---|---|---|
| 节日营销 | #春节、#中秋 | 携带话题#春节的作者瓜分 |
| 产品推广 | #新品发布、#限时特惠 | 同时携带#新品发布和#优惠的作者 |
| 品牌活动 | #品牌挑战赛、#品牌联名 | 携带#品牌挑战赛或#品牌联名 |
| 游戏活动 | #攻略、#教学 | 同时携带#攻略和#教学 |
| UGC征集 | #创作挑战、#用户故事 | 携带#创作挑战的用户 |
📋 数据格式要求
支持的文件格式
- ✅ Excel (.xlsx, .xls)
- ✅ CSV (.csv,推荐UTF-8编码)
必需字段
| 字段类型 | 说明 | 示例 |
|---|---|---|
| 用户ID | 唯一标识 | 作者ID、用户ID、账号 |
| 用户名称 | 显示名称 | 作者名称、用户昵称 |
| 视频标题 | 话题词匹配用 | 视频标题、标题 |
| 数据字段 | 结算依据 | 播放量、销售额、完成量 |
| 排序字段 | 排名赛使用 | 点赞数、销售额 |
示例数据结构
视频数据:
| 作者ID | 作者名称 | 视频ID | 视频标题 | 播放量 | 点赞数 |
|---|---|---|---|---|---|
| 123456 | 张三 | 789 | "#金铲铲 最强阵容" | 50000 | 1000 |
| 123456 | 张三 | 790 | "游戏攻略" | 30000 | 500 |
| 789012 | 李四 | 791 | "#金铲铲 #攻略" | 80000 | 2000 |
聚合后结算:
| 作者ID | 作者名称 | 作品数 | 累计播放量 | 累计点赞 | 符合话题词 |
|---|---|---|---|---|---|
| 123456 | 张三 | 2 | 80000 | 1500 | ✅ (视频789) |
| 789012 | 李四 | 1 | 80000 | 2000 | ✅ (视频791) |
📝 版本历史
v2.1.1 (2026-05-09)
- ✅ 新增「AI执行约束」章节:严格禁止 AI 自行添加「优化」逻辑、修改计算方式、添加文档未说明功能
- ✅ 强化瓜分算法约束:明确要求所有达标者奖金完全相同,明确禁止「差额补给某人」、「少给某人」、「自行处理差额」
- ✅ 规范精度无法整除时的处理:统一执行方案B(保留最少小数位使金额整除),废除方案A(截断后丢弃差额)
- ✅ 明确有疑问时必须询问用户:规则歧义、字段不匹配、奖池无法整除均必须等待用户决策
v2.1.0 (2026-05-08)
- ✅ 支持 Excel (xlsx/xls):新增
openpyxl读取,自动检测编码,支持多 Sheet 合并 - ✅ 多奖池并行:
SettlementEngine支持同时计算任意数量奖池(达标瓜分 + 榜单奖混合) - ✅ 榜单/排名奖:新增
SettlementMode.RANKING,支持分段档位(第1名/第2-3名/...)固定金额或瓜分 - ✅ 多条件达标:
conditions字段支持 AND 逻辑多条件(如作品数≥5 且 播放量≥3万) - ✅ 精度修复:
export_to_csv金额列不再用:.2f截断,完整保留精确小数 - ✅ 直播数据支持:新增
live_duration/live_count/live_sales字段聚合 - ✅ 命令行参数扩展:支持
sheet名称和--all-sheets参数
v2.0.2 (2026-05-08)
- ✅ 新增强制执行规则章节:明确要求AI必须调用
settlement_engine.py,禁止手写替代代码 - ✅ 禁止
round()精度陷阱:明确标注常见错误写法,防止AI第一次计算时忽略精度要求 - ✅ 完整执行流程示例:提供可直接复制执行的4步代码,含错误写法对比
- ✅ AI自查清单:结算前必须完成的5项检查,确保不遗漏关键步骤
- ✅ 命令行快速执行:补充命令行调用示例,AI可直接用
python settlement_engine.py完成结算
v2.0.1 (2026-05-07)
- ✅ 修复精确瓜分算法:彻底解决浮点舍入导致总额不等于奖池的问题
- ✅ 明确分档瓜分(累进档位)规则:各档独立判断,高档位作者可同时参与低档奖励
- ✅ 新增触发词「一组结算」,可直接启动结算流程
- 📝 补充分档瓜分示例和代码规范
v1.1.0 (2026-04-21)
- ✅ 新增话题词精准匹配功能
- 支持精准识别视频标题中的话题词(如#金铲铲)
- 支持且/或逻辑关系(AND/OR)
- 自动区分相似话题词(#金铲铲 vs #金铲铲之战)
- ✅ 新增规则确认流程
- AI解析后返回格式化的规则理解
- 用户可多轮修正直到确认
- 避免因理解偏差导致的结算错误
- 📊 优化数据加载逻辑,支持读取视频标题字段
v1.0.0 (2026-04-11)
- ✅ 初始版本发布
- ✅ 支持自然语言规则解析
- ✅ 四种结算模式:达标瓜分、排名赛、混合不互斥、权重分配
- ✅ 超时自动重试机制
- ✅ 数据本地处理,保障安全
- ✅ 专业Excel输出格式
💡 设计哲学
让结算变得简单,让数据变得安全,让结果变得专业。
核心原则
- AI优先:用自然语言替代复杂配置
- 本地处理:数据不出本地,安全保障
- 健壮可靠:超时重试,容错恢复
- 专业输出:格式规范,直接可用
🧪 结算引擎代码(AI直接使用,无需查找外部文件)
⚠️ AI执行结算时,必须将以下完整代码块全部复制到执行环境中运行,禁止自行手写任何替代实现。
完整结算引擎代码(直接复制执行)
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""AI智能结算引擎 v2.1.1 - 直接复制此代码块到执行环境"""
import csv, re, os
from decimal import Decimal, ROUND_HALF_UP, getcontext
from typing import Dict, List, Any, Optional, Tuple
from dataclasses import dataclass, field
from enum import Enum
getcontext().prec = 28
class SettlementMode(Enum):
GUARANTEED = "guaranteed"
RANKING = "ranking"
WEIGHTED = "weighted"
@dataclass
class TopicRule:
topics: List[str]
logic: str = "OR"
def check(self, title: str) -> bool:
if not self.topics: return True
matches = [bool(re.search(r'(?:^|[\s,,。!!??;;::、])' + re.escape(t) + r'(?:$|[\s,,。!!??;;::、])', title)) for t in self.topics]
return all(matches) if self.logic == "AND" else any(matches)
@dataclass
class RankingTier:
rank_start: int
rank_end: int
amount: float = 0.0
pool: float = 0.0
@dataclass
class AwardPool:
name: str
amount: float
mode: SettlementMode
condition: Optional[Dict] = None
ranking_field: Optional[str] = None
ranking_tiers: Optional[List[RankingTier]] = None
weight_field: Optional[str] = None
topic_rule: Optional[TopicRule] = None
conditions: Optional[List[Dict]] = None
@dataclass
class AuthorData:
author_id: str
author_name: str
videos: int = 0
total_plays: int = 0
total_likes: int = 0
video_titles: List[str] = field(default_factory=list)
live_duration: int = 0
live_count: int = 0
live_sales: int = 0
extra: Dict[str, Any] = field(default_factory=dict)
@dataclass
class SettlementResult:
author_id: str
author_name: str
videos: int
total_plays: int
total_likes: int
awards: Dict[str, str]
total_amount: str
def _distribute_equal(total_yuan: float, n: int) -> str:
"""
等额瓜分核心函数。保证所有人金额完全相同,奖池金额精确分完。
❌ 禁止用 round() 替代此函数。
"""
total = Decimal(str(total_yuan)); count = Decimal(n); per = total / count
for digits in range(2, 11):
per_r = per.quantize(Decimal('0.' + '0' * digits), rounding=ROUND_HALF_UP)
if per_r * count == total:
return str(per_r)
return str(per.quantize(Decimal('0.0000000001'), rounding=ROUND_HALF_UP))
FIELD_ALIASES = {
'author_id': ['作者ID', '用户ID', '账号ID', 'authorId', 'uid'],
'author_name': ['作者名称(最新)', '作者名称', '用户名称', '昵称', 'authorName'],
'title': ['视频标题', '标题', 'title'],
'plays': ['视频累计外显播放次数', '播放量', '播放次数', 'plays'],
'likes': ['视频累计外显点赞次数', '点赞量', '获赞数', 'likes'],
'live_duration': ['直播时长', '直播累计时长', 'liveDuration'],
'live_count': ['开播场次', '直播场次', 'liveCount'],
'live_sales': ['直播销售额', '销售额', 'liveSales'],
}
def _resolve(row, key):
for alias in FIELD_ALIASES.get(key, [key]):
if alias in row: return row[alias]
return None
def _num(val):
if val is None or val == '': return 0
try: return int(float(str(val).replace(',', '')))
except: return 0
def _load_csv(fp):
for enc in ('utf-8-sig', 'gbk', 'utf-8'):
try:
with open(fp, 'r', encoding=enc) as f:
return {os.path.splitext(os.path.basename(fp))[0]: list(csv.DictReader(f))}
except: continue
return {}
def _load_excel(fp, sheet_name, all_sheets):
try: import openpyxl
except ImportError: raise ImportError("读取Excel需要安装openpyxl:pip install openpyxl")
wb = openpyxl.load_workbook(fp, read_only=True, data_only=True)
target = wb.sheetnames if all_sheets else ([sheet_name] if sheet_name else [wb.sheetnames[0]])
result = {}
for sname in target:
ws = wb[sname]; rows_iter = ws.iter_rows(values_only=True)
header = [str(h).strip() if h is not None else '' for h in next(rows_iter, [])]
result[sname] = [{header[i]: (row[i] if i < len(row) else None) for i in range(len(header))} for row in rows_iter]
wb.close(); return result
def load_file(fp, sheet_name=None, all_sheets=False):
ext = os.path.splitext(fp)[1].lower()
if ext == '.csv': return _load_csv(fp)
elif ext in ('.xlsx', '.xls', '.xlsm'): return _load_excel(fp, sheet_name, all_sheets)
else: raise ValueError(f"不支持的格式: {ext}")
def _aggregate(rows):
authors = {}
for row in rows:
aid = str(_resolve(row, 'author_id') or '').strip()
if not aid: continue
if aid not in authors: authors[aid] = AuthorData(author_id=aid, author_name='')
a = authors[aid]
name = str(_resolve(row, 'author_name') or '').strip()
if name: a.author_name = name
a.videos += 1; a.total_plays += _num(_resolve(row, 'plays')); a.total_likes += _num(_resolve(row, 'likes'))
a.live_duration += _num(_resolve(row, 'live_duration')); a.live_count += _num(_resolve(row, 'live_count')); a.live_sales += _num(_resolve(row, 'live_sales'))
t = str(_resolve(row, 'title') or '').strip()
if t: a.video_titles.append(t)
for k, v in row.items():
if k not in a.extra: a.extra[k] = v
return authors
def _merge(base, new_authors):
for aid, data in new_authors.items():
if aid in base:
a = base[aid]
if data.author_name: a.author_name = data.author_name
a.videos += data.videos; a.total_plays += data.total_plays; a.total_likes += data.total_likes
a.live_duration += data.live_duration; a.live_count += data.live_count; a.live_sales += data.live_sales
a.video_titles += data.video_titles; a.extra.update(data.extra)
else: base[aid] = data
FMAP = {
'播放量': 'total_plays', '播放': 'total_plays', '点赞': 'total_likes', '获赞': 'total_likes',
'直播时长': 'live_duration', '开播场次': 'live_count', '销售额': 'live_sales', '作品数': 'videos',
}
def _topic_ok(a, rule): return not rule or any(rule.check(t) for t in a.video_titles)
def _cond_ok(a, pool):
for cond in (pool.conditions or ([pool.condition] if pool.condition else [])):
attr = FMAP.get(cond.get('field', ''), cond.get('field', ''))
actual = getattr(a, attr, None) or a.extra.get(attr, 0)
op = cond.get('op', '>='); val = cond.get('value', 0)
if op in ('>=', '≥'): ok = actual >= val
elif op == '>': ok = actual > val
elif op in ('<=', '≤'): ok = actual <= val
elif op == '<': ok = actual < val
elif op == '==': ok = actual == val
else: ok = False
if not ok: return False
return True
def run_settlement(file_path, pools, sheet_name=None, all_sheets=False, extra_file=None):
"""
主结算函数。pools 由 AI 根据活动规则构造。
- file_path: CSV 或 Excel
- all_sheets=True: 合并所有 Sheet
- extra_file: 第二个底表文件(数据合并后统一结算)
"""
authors = {}
for rows in load_file(file_path, sheet_name, all_sheets).values():
_merge(authors, _aggregate(rows))
if extra_file:
for rows in load_file(extra_file).values():
_merge(authors, _aggregate(rows))
results = {aid: SettlementResult(aid, a.author_name, a.videos, a.total_plays, a.total_likes, {}, '0')
for aid, a in authors.items()}
for pool in pools:
if pool.mode == SettlementMode.GUARANTEED:
q = [aid for aid, a in authors.items() if _topic_ok(a, pool.topic_rule) and _cond_ok(a, pool)]
if q:
per = _distribute_equal(pool.amount, len(q))
for aid in q: results[aid].awards[pool.name] = per
elif pool.mode == SettlementMode.RANKING and pool.ranking_field and pool.ranking_tiers:
cands = [a for a in authors.values() if _topic_ok(a, pool.topic_rule) and _cond_ok(a, pool)]
attr = FMAP.get(pool.ranking_field, pool.ranking_field)
cands.sort(key=lambda a: (getattr(a, attr, 0) or a.extra.get(attr, 0)), reverse=True)
for tier in pool.ranking_tiers:
tc = cands[tier.rank_start - 1: tier.rank_end]
if not tc: continue
per = _distribute_equal(tier.pool, len(tc)) if tier.pool > 0 else _distribute_equal(tier.amount, 1)
for a in tc: results[a.author_id].awards[pool.name] = per
elif pool.mode == SettlementMode.WEIGHTED and pool.weight_field:
cands = [a for a in authors.values() if _topic_ok(a, pool.topic_rule) and _cond_ok(a, pool)]
attr = FMAP.get(pool.weight_field, pool.weight_field)
tw = sum(getattr(a, attr, 0) or a.extra.get(attr, 0) for a in cands)
if tw > 0:
pd = Decimal(str(pool.amount)); twd = Decimal(str(tw))
for a in cands:
w = Decimal(str(getattr(a, attr, 0) or a.extra.get(attr, 0)))
award = (pd * w / twd).quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
if award > 0: results[a.author_id].awards[pool.name] = str(award)
for r in results.values():
tot = sum(Decimal(v) for v in r.awards.values())
r.total_amount = str(tot.normalize() if tot != 0 else Decimal('0'))
winners = sorted([r for r in results.values() if Decimal(r.total_amount) > 0],
key=lambda x: Decimal(x.total_amount), reverse=True)
stats = {
'total_authors': len(authors), 'qualified_authors': len(winners),
'total_videos': sum(r.videos for r in winners), 'total_plays': sum(r.total_plays for r in winners),
'total_likes': sum(r.total_likes for r in winners),
'total_award': str(sum(Decimal(r.total_amount) for r in winners).normalize()),
'pools': [{'name': p.name, 'amount': p.amount, 'mode': p.mode.value} for p in pools],
}
return winners, stats
def export_to_csv(results, output_path, stats):
"""导出结算结果,金额保留完整精度,不截断。"""
pool_names = []
for r in results:
for k in r.awards:
if k not in pool_names: pool_names.append(k)
header = ['序号', '作者ID', '作者名称', '发布作品数', '累计播放量', '累计获赞'] + [f'{p}(元)' for p in pool_names] + ['总奖金(元)']
with open(output_path, 'w', newline='', encoding='utf-8-sig') as f:
w = csv.writer(f); w.writerow(header)
for idx, r in enumerate(results, 1):
row = [idx, r.author_id, r.author_name, r.videos, r.total_plays, r.total_likes]
row += [r.awards.get(p, '') for p in pool_names] + [r.total_amount]
w.writerow(row)
summary = ['汇总', '', '', stats['total_videos'], stats['total_plays'], stats['total_likes']]
for p in pool_names:
summary.append(str(sum(Decimal(r.awards[p]) for r in results if p in r.awards).normalize()))
summary.append(stats['total_award']); w.writerow(summary)
def print_summary(results, stats):
print('=' * 80 + '\n结算结果摘要\n' + '=' * 80)
for p in stats.get('pools', []): print(f" - {p['name']}: {p['amount']:,.2f}元 ({p['mode']})")
print(f"\n 获奖: {stats['qualified_authors']}人 / 参与: {stats['total_authors']}人 总奖金: {stats['total_award']}元\n" + '-' * 80)
print(f"{'序号':<5}{'作者ID':<16}{'作者名称':<16}{'作品数':<8}{'播放量':<12}{'总奖金'}")
for i, r in enumerate(results[:10], 1):
print(f"{i:<5}{r.author_id:<16}{r.author_name:<16}{r.videos:<8}{r.total_plays:<12}{r.total_amount}")
print('=' * 80)
如何构造 pools(每次活动必须重新构造,示例仅供参考)
⚠️ 以下示例代码是模板,不是固定配置。AI每次结算前必须根据用户描述的实际规则重新构造
pools,奖池数量、类型、金额、条件均可任意组合。
pools 支持的奖池类型(可任意数量、任意组合):
| 类型 | SettlementMode | 适用场景 |
|---|---|---|
| 达标瓜分 | GUARANTEED | 满足条件的人等额平分奖池 |
| 排名/榜单奖 | RANKING | 按指定字段排名,不同名次固定金额或瓜分 |
| 权重分配 | WEIGHTED | 按某字段权重比例分配奖池 |
示例(仅参考,实际 pools 由 AI 根据当次活动规则构造):
# ============================================================
# 以下仅为示例,AI必须根据实际活动规则重新构造 pools
# 奖池数量不限,类型可任意混合
# ============================================================
# 场景示例A:单个达标瓜分
pools = [
AwardPool(
name="瓜分奖池",
amount=20000,
mode=SettlementMode.GUARANTEED,
conditions=[{'field': '播放量', 'op': '>=', 'value': 30000}],
),
]
# 场景示例B:多档瓜分(累进叠加,高档同时获得低档奖励)
pools = [
AwardPool(name="1档奖池", amount=10000, mode=SettlementMode.GUARANTEED,
conditions=[{'field': '作品数', 'op': '>=', 'value': 2}]),
AwardPool(name="2档奖池", amount=20000, mode=SettlementMode.GUARANTEED,
conditions=[{'field': '作品数', 'op': '>=', 'value': 5}]),
AwardPool(name="3档奖池", amount=30000, mode=SettlementMode.GUARANTEED,
conditions=[{'field': '作品数', 'op': '>=', 'value': 10}]),
]
# 场景示例C:瓜分 + 话题词过滤 + 榜单奖混合
pools = [
AwardPool(
name="基础瓜分",
amount=30000,
mode=SettlementMode.GUARANTEED,
conditions=[{'field': '播放量', 'op': '>=', 'value': 10000}],
topic_rule=TopicRule(topics=['#活动话题'], logic='OR'),
),
AwardPool(
name="大神榜",
amount=20000,
mode=SettlementMode.RANKING,
ranking_field='播放量',
ranking_tiers=[
RankingTier(rank_start=1, rank_end=1, amount=10000), # 第1名固定10000
RankingTier(rank_start=2, rank_end=3, amount=5000), # 第2-3名各5000
RankingTier(rank_start=4, rank_end=10, amount=0, pool=5000), # 第4-10名瓜分5000
],
),
]
# 场景示例D:按播放量权重比例分配
pools = [
AwardPool(
name="权重奖池",
amount=50000,
mode=SettlementMode.WEIGHTED,
weight_field='播放量',
conditions=[{'field': '播放量', 'op': '>=', 'value': 5000}],
),
]
# ============================================================
# 构造好 pools 后,调用结算函数(文件路径换成实际路径)
# ============================================================
results, stats = run_settlement(
file_path='data.xlsx', # 支持 .xlsx / .xls / .csv
pools=pools,
sheet_name=None, # 指定Sheet名,None则取第一个Sheet
all_sheets=False, # True则合并所有Sheet
# extra_file='live.xlsx' # 可选:第二个底表文件,数据合并后统一结算
)
print_summary(results, stats)
export_to_csv(results, 'data_结算结果.csv', stats)
print("✓ 完成")
开发团队:AI智能结算助手Pro
联系方式:通过OpenClaw技能市场反馈