feat: 联盟返佣系统规范化并打通技能广场(无 ICP 证合规变现主路径)

- 新增 AffiliateProgram / AffiliateLink / AffiliateClick 规范化 Prisma 模型
  取代原手写裸表 affiliate_stats / affiliate_clicks
- 新增迁移 prisma/migrations/20260711000000_add_affiliate_models
- 重构 affiliate.service.ts 改用 Prisma ORM,消除 $queryRawUnsafe SQL 注入
- 重构 affiliate.controller.ts 接口:programs / links(?skillId,?toolId) / stats / click
- 前端 affiliate 页接入真实接口,移除硬编码 demo 数据
- 技能详情页新增「学此技能推荐使用的工具」联盟链接区块
- 新增 affiliate.service.spec.ts(4 用例通过)与幂等种子 seed-affiliate.ts
- 更新 docs/progress/current.md,明确无 ICP 经营许可证下以联盟返佣为合规变现主路径
- 含此前工作区未提交改动(工具 slug 路由、支付/订单、SEO 等)

Co-Authored-By: opencode <opencode@anthropic.com>
This commit is contained in:
TradeMate Dev
2026-07-11 14:47:41 +08:00
parent 05bf267d09
commit 31cc1e02ce
46 changed files with 3565 additions and 1201 deletions
@@ -0,0 +1,62 @@
-- 联盟返佣系统:规范化数据模型
-- 替换原先手写的 affiliate_stats / affiliate_clicks 裸表,改用 ORM 管理
CREATE TABLE IF NOT EXISTS `affiliate_programs` (
`id` INT NOT NULL AUTO_INCREMENT,
`name` VARCHAR(191) NOT NULL,
`slug` VARCHAR(191) NOT NULL,
`provider` VARCHAR(191),
`description` TEXT,
`commission_rate` VARCHAR(191),
`cookie_days` INT,
`homepage_url` VARCHAR(191),
`logo` VARCHAR(191),
`active` TINYINT(1) NOT NULL DEFAULT 1,
`sort_order` INT NOT NULL DEFAULT 0,
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`updated_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (`id`),
UNIQUE INDEX `affiliate_programs_slug_key` (`slug`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `affiliate_links` (
`id` INT NOT NULL AUTO_INCREMENT,
`program_id` INT NOT NULL,
`title` VARCHAR(191) NOT NULL,
`description` TEXT,
`url` TEXT NOT NULL,
`thumbnail` VARCHAR(191),
`tool_id` INT,
`skill_id` VARCHAR(191),
`tags` TEXT,
`estimated_commission` DOUBLE NOT NULL DEFAULT 0,
`sort_order` INT NOT NULL DEFAULT 0,
`is_featured` TINYINT(1) NOT NULL DEFAULT 0,
`active` TINYINT(1) NOT NULL DEFAULT 1,
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`updated_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (`id`),
INDEX `affiliate_links_program_id_idx` (`program_id`),
INDEX `affiliate_links_tool_id_idx` (`tool_id`),
INDEX `affiliate_links_skill_id_idx` (`skill_id`),
CONSTRAINT `affiliate_links_program_id_fkey` FOREIGN KEY (`program_id`) REFERENCES `affiliate_programs` (`id`) ON DELETE CASCADE,
CONSTRAINT `affiliate_links_tool_id_fkey` FOREIGN KEY (`tool_id`) REFERENCES `tools` (`id`) ON DELETE SET NULL,
CONSTRAINT `affiliate_links_skill_id_fkey` FOREIGN KEY (`skill_id`) REFERENCES `skills` (`id`) ON DELETE SET NULL
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `affiliate_clicks` (
`id` INT NOT NULL AUTO_INCREMENT,
`link_id` INT NOT NULL,
`user_id` INT,
`ip` VARCHAR(191),
`user_agent` TEXT,
`ref` VARCHAR(191),
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
PRIMARY KEY (`id`),
INDEX `affiliate_clicks_link_id_idx` (`link_id`),
CONSTRAINT `affiliate_clicks_link_id_fkey` FOREIGN KEY (`link_id`) REFERENCES `affiliate_links` (`id`) ON DELETE CASCADE
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- 清理旧的手写裸表(被本迁移的规范化模型取代)
DROP TABLE IF EXISTS `affiliate_stats`;
DROP TABLE IF EXISTS `affiliate_clicks_old`;
File diff suppressed because it is too large Load Diff
+100
View File
@@ -0,0 +1,100 @@
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
/**
* 联盟返佣种子:创建「阿里云云大使」推广计划,并把技能/工具关联到推广链接。
* 幂等(upsert),可重复执行。部署时运行:npx ts-node prisma/seed-affiliate.ts
*/
async function main() {
const program = await prisma.affiliateProgram.upsert({
where: { slug: 'aliyun-promoter' },
update: { active: true },
create: {
name: '阿里云云大使',
slug: 'aliyun-promoter',
provider: '阿里云',
description: '阿里云推广大使联盟,用户通过推荐链接购买云产品,平台获得返佣。',
commissionRate: '最高 20%',
cookieDays: 30,
homepageUrl: 'https://promotion.aliyun.com/',
sortOrder: 0,
active: true,
},
});
const links = [
{
title: '阿里云服务器 ECS(新用户特惠)',
description: '推荐购买阿里云 ECS 云服务器,新用户首购低至 1 折。',
url: 'https://dashi.aliyun.com/activity/oss?userCode=yzrcloud',
skillSlug: 'title-craft',
toolName: '通义千问',
tags: '云计算,服务器,阿里云',
estimatedCommission: 15,
isFeatured: true,
},
{
title: '阿里云 AI 大模型 API(百炼平台)',
description: '通义千问/百炼大模型 API,按量计费,适合开发者接入。',
url: 'https://bailian.aliyun.com/?userCode=yzrcloud',
skillSlug: 'payment-chaser',
toolName: '通义千问',
tags: '大模型,API,AI',
estimatedCommission: 12,
isFeatured: true,
},
{
title: '阿里云对象存储 OSS',
description: '推荐企业/开发者使用 OSS 存储,长期稳定低成本。',
url: 'https://dashi.aliyun.com/activity/oss?userCode=yzrcloud',
skillSlug: null,
toolName: 'WPS AI',
tags: '存储,对象存储',
estimatedCommission: 8,
isFeatured: false,
},
];
for (const link of links) {
let skillId: string | null = null;
if (link.skillSlug) {
const skill = await prisma.skill.findUnique({ where: { id: link.skillSlug } });
skillId = skill?.id ?? null;
}
let toolId: number | null = null;
if (link.toolName) {
const tool = await prisma.tool.findFirst({ where: { name: link.toolName } });
toolId = tool?.id ?? null;
}
await prisma.affiliateLink.upsert({
where: { title: link.title },
update: { url: link.url, active: true, skillId, toolId },
create: {
programId: program.id,
title: link.title,
description: link.description,
url: link.url,
tags: link.tags,
estimatedCommission: link.estimatedCommission,
isFeatured: link.isFeatured,
skillId,
toolId,
active: true,
},
});
}
const count = await prisma.affiliateLink.count();
console.log(`✅ 联盟返佣种子完成,当前推广链接 ${count}`);
}
main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});
+1 -1
View File
@@ -26,7 +26,7 @@ async function main() {
description: '阿里云推出的 AI 大模型,中文理解和生成能力领先。深度整合阿里云生态(钉钉、淘宝、阿里云控制台),提供企业级 API 服务。',
url: 'https://tongyi.aliyun.com',
tags: '中文,阿里云,企业级,API',
affiliateLink: 'https://www.aliyun.com/minisite/goods?userCode=yuzhiran', // 阿里云云大使
affiliateLink: 'https://dashi.aliyun.com/activity/oss?userCode=kap6n5tc', // 阿里云云大使
isFeatured: true,
},
{