Files
TradeMate Dev 31cc1e02ce 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>
2026-07-11 14:47:41 +08:00

101 lines
3.3 KiB
JavaScript

const fs = require('fs');
const path = require('path');
const SITE_URL = 'https://yuzhiran.com';
const OUT_DIR = path.join(__dirname, '..', 'out');
const PRIORITIES = {
'/': 1.0,
'/tools': 0.9,
'/courses': 0.9,
'/skills': 0.8,
'/marketplace': 0.8,
'/practices': 0.8,
'/contents': 0.8,
'/prompts': 0.8,
'/prompts/workshop': 0.7,
'/models': 0.7,
'/sandbox': 0.7,
'/sandbox/compare': 0.6,
'/sandbox/code': 0.6,
'/sandbox/share': 0.6,
'/discover': 0.7,
'/discover/hot': 0.6,
'/discover/tools': 0.6,
'/discover/daily': 0.6,
'/community': 0.6,
'/circles': 0.6,
'/resources': 0.7,
'/about': 0.5,
'/privacy': 0.3,
'/terms': 0.3,
'/ai-agreement': 0.3,
'/search': 0.5,
};
function walkDir(dir, basePath = '') {
const entries = fs.readdirSync(dir, { withFileTypes: true });
const urls = [];
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
const relativeName = path.join(basePath, entry.name);
if (entry.isDirectory()) {
if (entry.name.startsWith('_') || entry.name.startsWith('.')) continue;
urls.push(...walkDir(fullPath, relativeName));
} else if (entry.name === 'page.html' || entry.name.endsWith('.html')) {
if (relativeName === 'index.html' || relativeName === 'page.html') {
urls.push({ path: '/', file: fullPath });
} else {
let urlPath = '/' + relativeName.replace(/\.html$/, '').replace(/\/index$/, '').replace(/\/page$/, '');
urls.push({ path: urlPath, file: fullPath });
}
}
}
return urls;
}
function generateSitemap(urls) {
const now = new Date().toISOString().split('T')[0];
let xml = '<?xml version="1.0" encoding="UTF-8"?>\n';
xml += '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n';
const seen = new Set();
for (const url of urls) {
if (seen.has(url.path)) continue;
seen.add(url.path);
if (url.path.startsWith('/admin') || url.path.startsWith('/_next') || url.path.startsWith('/api') || url.path === '/404') continue;
if (url.path.includes('/[') || url.path.includes(']')) continue;
const depth = url.path.split('/').filter(Boolean).length;
const changefreq = depth <= 1 ? 'weekly' : 'monthly';
const priority = PRIORITIES[url.path];
const computedPriority = priority !== undefined ? priority : Math.max(0.1, 0.7 - depth * 0.1);
xml += ' <url>\n';
xml += ` <loc>${SITE_URL}${url.path}</loc>\n`;
xml += ` <lastmod>${now}</lastmod>\n`;
xml += ` <changefreq>${changefreq}</changefreq>\n`;
xml += ` <priority>${computedPriority.toFixed(1)}</priority>\n`;
xml += ' </url>\n';
}
xml += '</urlset>';
return xml;
}
if (!fs.existsSync(OUT_DIR)) {
console.error('out/ directory not found. Run next build first.');
process.exit(1);
}
const urls = walkDir(OUT_DIR);
console.log(`Found ${urls.length} HTML pages in output.`);
const sitemap = generateSitemap(urls);
const outputPath = path.join(OUT_DIR, 'sitemap.xml');
fs.writeFileSync(outputPath, sitemap, 'utf-8');
console.log(`Generated sitemap.xml with ${sitemap.split('<url>').length - 1} URLs → ${outputPath}`);
// Copy robots.txt from public/ to out/
const robotsSrc = path.join(__dirname, '..', 'public', 'robots.txt');
const robotsDst = path.join(OUT_DIR, 'robots.txt');
if (fs.existsSync(robotsSrc)) {
fs.copyFileSync(robotsSrc, robotsDst);
console.log(`Copied robots.txt → ${robotsDst}`);
}