feat: 新增多模块功能与服务实现

新增广告计划、用户资产、B2B交易、合规规则等核心模型
实现爬虫工作器、贸易服务、现金流预测等业务服务
添加RBAC权限测试、压力测试等测试用例
完善扩展程序的消息处理与内容脚本功能
重构应用入口与文档生成器
更新项目规则与业务闭环分析文档
This commit is contained in:
2026-03-18 09:38:09 +08:00
parent 72cd7f6f45
commit 037e412aad
158 changed files with 50217 additions and 1313 deletions

View File

@@ -0,0 +1,537 @@
import db from '../config/database';
import { logger } from '../utils/logger';
import { UserAssetService } from './UserAssetService';
import {
MemberLevel,
MemberLevelRule,
MemberLevelRuleCreateInput,
} from '../models/UserAsset';
/**
* MemberLevelService - 会员等级系统
*
* [BE-UA003] 会员等级系统
* @description 管理会员等级规则、等级升降、权益配置
*
* 功能定位:
* - 会员等级规则管理
* - 等级自动升降
* - 会员权益配置
* - 等级统计报表
*
* 安全约束:
* - 所有操作必须携带五元组追踪信息
* - 等级变更必须记录日志
*
* @author AI-Backend-7
* @taskId BE-UA003
*/
export class MemberLevelService {
private static readonly RULE_TABLE = 'cf_member_level_rule';
private static readonly LOG_TABLE = 'cf_member_level_log';
/**
* 初始化数据库表
* 遵循幂等性原则:使用 hasTable 前置校验
*/
static async initTable(): Promise<void> {
const hasRuleTable = await db.schema.hasTable(this.RULE_TABLE);
if (!hasRuleTable) {
logger.info(`[MemberLevelService] Creating ${this.RULE_TABLE} table...`);
await db.schema.createTable(this.RULE_TABLE, (table) => {
table.string('id', 36).primary();
table.string('tenant_id', 64).index();
table.enum('level', ['BRONZE', 'SILVER', 'GOLD', 'PLATINUM', 'DIAMOND']).notNullable();
table.integer('min_score').notNullable();
table.integer('max_score').notNullable();
table.decimal('discount', 5, 4).defaultTo(1.0).notNullable();
table.decimal('points_multiplier', 5, 4).defaultTo(1.0).notNullable();
table.json('benefits').notNullable();
table.boolean('is_active').defaultTo(true).notNullable();
table.timestamp('created_at').defaultTo(db.fn.now());
table.timestamp('updated_at').defaultTo(db.fn.now());
table.index(['tenant_id', 'level'], 'idx_rule_tenant_level');
});
logger.info(`[MemberLevelService] Table ${this.RULE_TABLE} created`);
}
const hasLogTable = await db.schema.hasTable(this.LOG_TABLE);
if (!hasLogTable) {
logger.info(`[MemberLevelService] Creating ${this.LOG_TABLE} table...`);
await db.schema.createTable(this.LOG_TABLE, (table) => {
table.string('id', 36).primary();
table.string('tenant_id', 64).notNullable().index();
table.string('shop_id', 64).notNullable().index();
table.string('user_id', 64).notNullable().index();
table.string('trace_id', 64).notNullable();
table.enum('old_level', ['BRONZE', 'SILVER', 'GOLD', 'PLATINUM', 'DIAMOND']);
table.enum('new_level', ['BRONZE', 'SILVER', 'GOLD', 'PLATINUM', 'DIAMOND']).notNullable();
table.integer('old_score');
table.integer('new_score').notNullable();
table.string('change_type', 32).notNullable();
table.text('reason');
table.timestamp('created_at').defaultTo(db.fn.now());
table.index(['tenant_id', 'user_id'], 'idx_log_user');
table.index(['created_at'], 'idx_log_time');
});
logger.info(`[MemberLevelService] Table ${this.LOG_TABLE} created`);
}
}
/**
* 创建或更新等级规则
* @param input 规则创建输入
* @returns 规则ID
*/
static async createOrUpdateRule(input: MemberLevelRuleCreateInput): Promise<string> {
const existing = await db(this.RULE_TABLE)
.where({
tenant_id: input.tenantId || null,
level: input.level,
})
.first();
if (existing) {
await db(this.RULE_TABLE)
.where({ id: existing.id })
.update({
min_score: input.minScore,
max_score: input.maxScore,
discount: input.discount,
points_multiplier: input.pointsMultiplier,
benefits: JSON.stringify(input.benefits),
is_active: input.isActive !== undefined ? input.isActive : true,
updated_at: new Date(),
});
logger.info(`[MemberLevelService] Rule updated: level=${input.level}, tenantId=${input.tenantId || 'global'}`);
return existing.id;
}
const id = this.generateId();
const now = new Date();
await db(this.RULE_TABLE).insert({
id,
tenant_id: input.tenantId || null,
level: input.level,
min_score: input.minScore,
max_score: input.maxScore,
discount: input.discount,
points_multiplier: input.pointsMultiplier,
benefits: JSON.stringify(input.benefits),
is_active: input.isActive !== undefined ? input.isActive : true,
created_at: now,
updated_at: now,
});
logger.info(`[MemberLevelService] Rule created: id=${id}, level=${input.level}, tenantId=${input.tenantId || 'global'}`);
return id;
}
/**
* 获取等级规则
* @param tenantId 租户ID可选为空则获取全局规则
* @param level 等级
* @returns 等级规则
*/
static async getRule(tenantId: string | undefined, level: MemberLevel): Promise<MemberLevelRule | null> {
let query = db(this.RULE_TABLE)
.where('level', level)
.where('is_active', true);
if (tenantId) {
query = query.where(function(this: any) {
this.whereNull('tenant_id').orWhere('tenant_id', tenantId);
});
} else {
query = query.whereNull('tenant_id');
}
const row = await query.orderBy('tenant_id', 'desc').first();
if (!row) return null;
return this.mapRowToRule(row);
}
/**
* 获取所有等级规则
* @param tenantId 租户ID可选
* @returns 等级规则列表
*/
static async getAllRules(tenantId?: string): Promise<MemberLevelRule[]> {
let query = db(this.RULE_TABLE)
.where('is_active', true);
if (tenantId) {
query = query.where(function(this: any) {
this.whereNull('tenant_id').orWhere('tenant_id', tenantId);
});
} else {
query = query.whereNull('tenant_id');
}
const rows = await query.orderBy('min_score', 'asc');
const rules: MemberLevelRule[] = [];
const seenLevels = new Set<string>();
for (const row of rows) {
if (!seenLevels.has(row.level)) {
rules.push(this.mapRowToRule(row));
seenLevels.add(row.level);
}
}
return rules;
}
/**
* 根据积分计算等级
* @param tenantId 租户ID
* @param score 积分
* @returns 等级
*/
static async calculateLevel(tenantId: string, score: number): Promise<MemberLevel> {
const rules = await this.getAllRules(tenantId);
if (rules.length === 0) {
return UserAssetService.calculateLevel(score);
}
for (const rule of rules) {
if (score >= rule.minScore && score <= rule.maxScore) {
return rule.level;
}
}
const sortedRules = [...rules].sort((a, b) => b.minScore - a.minScore);
for (const rule of sortedRules) {
if (score >= rule.minScore) {
return rule.level;
}
}
return 'BRONZE';
}
/**
* 获取等级权益
* @param tenantId 租户ID
* @param level 等级
* @returns 权益列表
*/
static async getLevelBenefits(tenantId: string, level: MemberLevel): Promise<string[]> {
const rule = await this.getRule(tenantId, level);
return rule?.benefits || [];
}
/**
* 获取等级折扣
* @param tenantId 租户ID
* @param level 等级
* @returns 折扣率0-1
*/
static async getLevelDiscount(tenantId: string, level: MemberLevel): Promise<number> {
const rule = await this.getRule(tenantId, level);
return rule?.discount || 1.0;
}
/**
* 获取等级积分倍率
* @param tenantId 租户ID
* @param level 等级
* @returns 积分倍率
*/
static async getLevelPointsMultiplier(tenantId: string, level: MemberLevel): Promise<number> {
const rule = await this.getRule(tenantId, level);
return rule?.pointsMultiplier || 1.0;
}
/**
* 升级用户等级
* @param tenantId 租户ID
* @param shopId 店铺ID
* @param userId 用户ID
* @param newLevel 新等级
* @param reason 原因
* @param traceId 追踪ID
*/
static async upgradeLevel(
tenantId: string,
shopId: string,
userId: string,
newLevel: MemberLevel,
reason: string,
traceId: string
): Promise<void> {
const asset = await UserAssetService.getByUserId(tenantId, shopId, userId);
if (!asset) {
throw new Error(`User asset not found: userId=${userId}`);
}
const oldLevel = asset.memberLevel;
if (oldLevel === newLevel) {
return;
}
await db('cf_user_asset')
.where({ tenant_id: tenantId, shop_id: shopId, user_id: userId })
.update({
member_level: newLevel,
updated_at: new Date(),
});
await this.logLevelChange({
tenantId,
shopId,
userId,
oldLevel,
newLevel,
oldScore: asset.memberScore,
newScore: asset.memberScore,
changeType: this.getLevelOrder(newLevel) > this.getLevelOrder(oldLevel) ? 'UPGRADE' : 'DOWNGRADE',
reason,
traceId,
});
logger.info(`[MemberLevelService] Level changed: userId=${userId}, oldLevel=${oldLevel}, newLevel=${newLevel}, traceId=${traceId}`);
}
/**
* 批量重新计算用户等级
* @param tenantId 租户ID
* @param shopId 店铺ID
* @returns 更新的用户数
*/
static async batchRecalculateLevels(
tenantId: string,
shopId?: string
): Promise<number> {
let query = db('cf_user_asset').where('tenant_id', tenantId);
if (shopId) {
query = query.where('shop_id', shopId);
}
const users = await query;
let updatedCount = 0;
for (const user of users) {
try {
const newLevel = await this.calculateLevel(tenantId, user.member_score);
if (newLevel !== user.member_level) {
await this.upgradeLevel(
tenantId,
user.shop_id,
user.user_id,
newLevel,
'系统自动计算',
`AUTO-RECALC-${Date.now()}`
);
updatedCount++;
}
} catch (error: any) {
logger.error(`[MemberLevelService] Failed to recalculate level: userId=${user.user_id}, error=${error.message}`);
}
}
logger.info(`[MemberLevelService] Batch recalculate completed: updated=${updatedCount}`);
return updatedCount;
}
/**
* 获取等级变更历史
* @param tenantId 租户ID
* @param userId 用户ID
* @param limit 限制条数
* @returns 变更历史
*/
static async getLevelHistory(
tenantId: string,
userId: string,
limit: number = 20
): Promise<Array<{
oldLevel: MemberLevel | null;
newLevel: MemberLevel;
changeType: string;
reason: string;
createdAt: Date;
}>> {
const rows = await db(this.LOG_TABLE)
.where({ tenant_id: tenantId, user_id: userId })
.orderBy('created_at', 'desc')
.limit(limit);
return rows.map(row => ({
oldLevel: row.old_level,
newLevel: row.new_level,
changeType: row.change_type,
reason: row.reason,
createdAt: row.created_at,
}));
}
/**
* 获取等级分布统计
* @param tenantId 租户ID
* @param shopId 店铺ID可选
* @returns 等级分布
*/
static async getLevelDistribution(
tenantId: string,
shopId?: string
): Promise<Record<MemberLevel, number>> {
let query = db('cf_user_asset').where('tenant_id', tenantId);
if (shopId) {
query = query.where('shop_id', shopId);
}
const rows = await query;
const distribution: Record<MemberLevel, number> = {
BRONZE: 0,
SILVER: 0,
GOLD: 0,
PLATINUM: 0,
DIAMOND: 0,
};
for (const row of rows) {
distribution[row.member_level as MemberLevel]++;
}
return distribution;
}
/**
* 初始化默认等级规则
*/
static async initDefaultRules(): Promise<void> {
const defaultRules: MemberLevelRuleCreateInput[] = [
{
level: 'BRONZE',
minScore: 0,
maxScore: 499,
discount: 1.0,
pointsMultiplier: 1.0,
benefits: ['基础积分获取', '生日优惠'],
},
{
level: 'SILVER',
minScore: 500,
maxScore: 1999,
discount: 0.98,
pointsMultiplier: 1.2,
benefits: ['基础积分获取', '生日优惠', '专属客服', '优先发货'],
},
{
level: 'GOLD',
minScore: 2000,
maxScore: 4999,
discount: 0.95,
pointsMultiplier: 1.5,
benefits: ['基础积分获取', '生日优惠', '专属客服', '优先发货', '会员专享价', '免费退换货'],
},
{
level: 'PLATINUM',
minScore: 5000,
maxScore: 9999,
discount: 0.92,
pointsMultiplier: 2.0,
benefits: ['基础积分获取', '生日优惠', '专属客服', '优先发货', '会员专享价', '免费退换货', '专属活动', '积分双倍日'],
},
{
level: 'DIAMOND',
minScore: 10000,
maxScore: 999999,
discount: 0.88,
pointsMultiplier: 3.0,
benefits: ['基础积分获取', '生日优惠', '专属客服', '优先发货', '会员专享价', '免费退换货', '专属活动', '积分双倍日', '一对一顾问', '新品优先体验', '年度礼包'],
},
];
for (const rule of defaultRules) {
await this.createOrUpdateRule(rule);
}
logger.info(`[MemberLevelService] Default rules initialized: count=${defaultRules.length}`);
}
/**
* 记录等级变更日志
*/
private static async logLevelChange(params: {
tenantId: string;
shopId: string;
userId: string;
oldLevel: MemberLevel;
newLevel: MemberLevel;
oldScore: number;
newScore: number;
changeType: string;
reason: string;
traceId: string;
}): Promise<void> {
await db(this.LOG_TABLE).insert({
id: this.generateId(),
tenant_id: params.tenantId,
shop_id: params.shopId,
user_id: params.userId,
trace_id: params.traceId,
old_level: params.oldLevel,
new_level: params.newLevel,
old_score: params.oldScore,
new_score: params.newScore,
change_type: params.changeType,
reason: params.reason,
created_at: new Date(),
});
}
/**
* 获取等级顺序
*/
private static getLevelOrder(level: MemberLevel): number {
const order: Record<MemberLevel, number> = {
BRONZE: 1,
SILVER: 2,
GOLD: 3,
PLATINUM: 4,
DIAMOND: 5,
};
return order[level] || 0;
}
/**
* 生成唯一ID
*/
private static generateId(): string {
return `MLR-${Date.now()}-${Math.random().toString(36).substring(2, 10)}`;
}
/**
* 将数据库行映射为规则对象
*/
private static mapRowToRule(row: Record<string, any>): MemberLevelRule {
return {
id: row.id,
tenantId: row.tenant_id,
level: row.level,
minScore: Number(row.min_score),
maxScore: Number(row.max_score),
discount: Number(row.discount),
pointsMultiplier: Number(row.points_multiplier),
benefits: JSON.parse(row.benefits),
isActive: Boolean(row.is_active),
createdAt: row.created_at,
updatedAt: row.updated_at,
};
}
}