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 { 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 { 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 { 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 { 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(); 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 { 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 { 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 { 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 { 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 { 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 { 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> { 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> { 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 = { 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 { 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 { 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 = { 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): 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, }; } }