update pay function

This commit is contained in:
2025-11-19 19:41:26 +08:00
parent 17c04211bb
commit a4d16e7686
12 changed files with 4667 additions and 140 deletions

316
app.py
View File

@@ -1127,36 +1127,61 @@ def get_user_subscription_safe(user_id):
def activate_user_subscription(user_id, plan_type, billing_cycle, extend_from_now=False):
"""激活用户订阅
"""
激活用户订阅(新版:续费时从当前订阅结束时间开始延长)
Args:
user_id: 用户ID
plan_type: 套餐类型
billing_cycle: 计费周期
extend_from_now: 是否从当前时间开始延长(用于升级场景
plan_type: 套餐类型 (pro/max)
billing_cycle: 计费周期 (monthly/quarterly/semiannual/yearly)
extend_from_now: 废弃参数,保留以兼容(现在自动判断
Returns:
UserSubscription 对象 或 None
"""
try:
subscription = UserSubscription.query.filter_by(user_id=user_id).first()
if not subscription:
# 新用户,创建订阅记录
subscription = UserSubscription(user_id=user_id)
db.session.add(subscription)
# 更新订阅类型和状态
subscription.subscription_type = plan_type
subscription.subscription_status = 'active'
subscription.billing_cycle = billing_cycle
if not extend_from_now or not subscription.start_date:
subscription.start_date = beijing_now()
# 计算订阅周期天数
cycle_days_map = {
'monthly': 30,
'quarterly': 90, # 3个月
'semiannual': 180, # 6个月
'yearly': 365
}
days = cycle_days_map.get(billing_cycle, 30)
if billing_cycle == 'monthly':
subscription.end_date = beijing_now() + timedelta(days=30)
else: # yearly
subscription.end_date = beijing_now() + timedelta(days=365)
now = beijing_now()
# 判断是新购还是续费
if subscription.end_date and subscription.end_date > now:
# 续费:从当前订阅结束时间开始延长
start_date = subscription.end_date
end_date = start_date + timedelta(days=days)
else:
# 新购或过期后重新购买:从当前时间开始
start_date = now
end_date = now + timedelta(days=days)
subscription.start_date = start_date
subscription.end_date = end_date
subscription.updated_at = now
subscription.updated_at = beijing_now()
db.session.commit()
return subscription
except Exception as e:
print(f"激活订阅失败: {e}")
db.session.rollback()
return None
@@ -1233,33 +1258,29 @@ def calculate_discount(promo_code, amount):
return 0
def calculate_remaining_value(subscription, current_plan):
"""计算当前订阅的剩余价值"""
try:
if not subscription or not subscription.end_date:
return 0
def calculate_subscription_price_simple(user_id, to_plan_name, to_cycle, promo_code=None):
"""
简化版价格计算:续费用户和新用户价格完全一致,不计算剩余价值
now = beijing_now()
if subscription.end_date <= now:
return 0
days_left = (subscription.end_date - now).days
if subscription.billing_cycle == 'monthly':
daily_value = float(current_plan.monthly_price) / 30
else: # yearly
daily_value = float(current_plan.yearly_price) / 365
return daily_value * days_left
except:
return 0
def calculate_upgrade_price(user_id, to_plan_name, to_cycle, promo_code=None):
"""计算升级所需价格
Args:
user_id: 用户ID
to_plan_name: 目标套餐名称 (pro/max)
to_cycle: 计费周期 (monthly/quarterly/semiannual/yearly)
promo_code: 优惠码(可选)
Returns:
dict: 包含价格计算结果的字典
dict: {
'is_renewal': False/True, # 是否为续费
'subscription_type': 'new'/'renew', # 订阅类型
'current_plan': 'pro', # 当前套餐(如果有)
'current_cycle': 'yearly', # 当前周期(如果有)
'new_plan_price': 2699.00, # 新套餐价格
'original_amount': 2699.00, # 原价
'discount_amount': 0, # 优惠金额
'final_amount': 2699.00, # 实付金额
'promo_code': None, # 使用的优惠码
'promo_error': None # 优惠码错误信息
}
"""
try:
# 1. 获取当前订阅
@@ -1270,83 +1291,90 @@ def calculate_upgrade_price(user_id, to_plan_name, to_cycle, promo_code=None):
if not to_plan:
return {'error': '目标套餐不存在'}
# 3. 计算目标套餐价格
new_price = float(to_plan.yearly_price if to_cycle == 'yearly' else to_plan.monthly_price)
# 3. 根据计费周期获取价格
# 优先从 pricing_options 获取价格
price = None
if to_plan.pricing_options:
try:
pricing_opts = json.loads(to_plan.pricing_options)
# 查找匹配的周期
for opt in pricing_opts:
cycle_key = opt.get('cycle_key', '')
months = opt.get('months', 0)
# 4. 如果是新订阅(非升级)
if not current_sub or current_sub.subscription_type == 'free':
result = {
'is_upgrade': False,
'new_plan_price': new_price,
'remaining_value': 0,
'upgrade_amount': new_price,
'original_amount': new_price,
'discount_amount': 0,
'final_amount': new_price,
'promo_code': None
}
# 匹配逻辑
if (cycle_key == to_cycle or
(to_cycle == 'monthly' and months == 1) or
(to_cycle == 'quarterly' and months == 3) or
(to_cycle == 'semiannual' and months == 6) or
(to_cycle == 'yearly' and months == 12)):
price = float(opt.get('price', 0))
break
except:
pass
# 应用优惠码
if promo_code:
promo, error = validate_promo_code(promo_code, to_plan_name, to_cycle, new_price, user_id)
if promo:
discount = calculate_discount(promo, new_price)
result['discount_amount'] = discount
result['final_amount'] = new_price - discount
result['promo_code'] = promo.code
elif error:
result['promo_error'] = error
# 如果 pricing_options 中没有找到,使用旧的 monthly_price/yearly_price
if price is None:
if to_cycle == 'yearly':
price = float(to_plan.yearly_price) if to_plan.yearly_price else 0
else: # 默认月付
price = float(to_plan.monthly_price) if to_plan.monthly_price else 0
return result
if price <= 0:
return {'error': f'{to_cycle} 周期价格未配置'}
# 5. 升级场景:计算剩余价值
current_plan = SubscriptionPlan.query.filter_by(name=current_sub.subscription_type, is_active=True).first()
if not current_plan:
return {'error': '当前套餐信息不存在'}
# 4. 判断是新购还是续费
is_renewal = False
subscription_type = 'new'
current_plan = None
current_cycle = None
remaining_value = calculate_remaining_value(current_sub, current_plan)
# 6. 计算升级差价
upgrade_amount = max(0, new_price - remaining_value)
# 7. 判断升级类型
upgrade_type = 'new'
if current_sub.subscription_type != to_plan_name and current_sub.billing_cycle != to_cycle:
upgrade_type = 'both'
elif current_sub.subscription_type != to_plan_name:
upgrade_type = 'plan_upgrade'
elif current_sub.billing_cycle != to_cycle:
upgrade_type = 'cycle_change'
if current_sub and current_sub.subscription_type in ['pro', 'max']:
# 如果当前是付费用户,则为续费
is_renewal = True
subscription_type = 'renew'
current_plan = current_sub.subscription_type
current_cycle = current_sub.billing_cycle
# 5. 构建结果(续费和新购价格完全一致)
result = {
'is_upgrade': True,
'upgrade_type': upgrade_type,
'current_plan': current_sub.subscription_type,
'current_cycle': current_sub.billing_cycle,
'current_end_date': current_sub.end_date.isoformat() if current_sub.end_date else None,
'new_plan_price': new_price,
'remaining_value': remaining_value,
'upgrade_amount': upgrade_amount,
'original_amount': upgrade_amount,
'is_renewal': is_renewal,
'subscription_type': subscription_type,
'current_plan': current_plan,
'current_cycle': current_cycle,
'new_plan_price': price,
'original_amount': price,
'discount_amount': 0,
'final_amount': upgrade_amount,
'promo_code': None
'final_amount': price,
'promo_code': None,
'promo_error': None
}
# 8. 应用优惠码
if promo_code and upgrade_amount > 0:
promo, error = validate_promo_code(promo_code, to_plan_name, to_cycle, upgrade_amount, user_id)
# 6. 应用优惠码
if promo_code and promo_code.strip():
promo, error = validate_promo_code(promo_code, to_plan_name, to_cycle, price, user_id)
if promo:
discount = calculate_discount(promo, upgrade_amount)
result['discount_amount'] = discount
result['final_amount'] = upgrade_amount - discount
discount = calculate_discount(promo, price)
result['discount_amount'] = float(discount)
result['final_amount'] = price - float(discount)
result['promo_code'] = promo.code
elif error:
result['promo_error'] = error
return result
except Exception as e:
return {'error': str(e)}
return {'error': f'价格计算失败: {str(e)}'}
# 保留旧函数以兼容(标记为废弃)
def calculate_upgrade_price(user_id, to_plan_name, to_cycle, promo_code=None):
"""
【已废弃】旧版升级价格计算函数,保留以兼容旧代码
新代码请使用 calculate_subscription_price_simple
"""
# 直接调用新函数
return calculate_subscription_price_simple(user_id, to_plan_name, to_cycle, promo_code)
def initialize_subscription_plans_safe():
@@ -1594,7 +1622,33 @@ def validate_promo_code_api():
@app.route('/api/subscription/calculate-price', methods=['POST'])
def calculate_subscription_price():
"""计算订阅价格(支持升级和优惠码)"""
"""
计算订阅价格(新版:续费和新购价格一致)
Request Body:
{
"to_plan": "pro",
"to_cycle": "yearly",
"promo_code": "WELCOME2025" // 可选
}
Response:
{
"success": true,
"data": {
"is_renewal": true, // 是否为续费
"subscription_type": "renew", // new 或 renew
"current_plan": "pro", // 当前套餐(如果有)
"current_cycle": "monthly", // 当前周期(如果有)
"new_plan_price": 2699.00,
"original_amount": 2699.00,
"discount_amount": 0,
"final_amount": 2699.00,
"promo_code": null,
"promo_error": null
}
}
"""
try:
if 'user_id' not in session:
return jsonify({'success': False, 'error': '未登录'}), 401
@@ -1607,8 +1661,8 @@ def calculate_subscription_price():
if not to_plan or not to_cycle:
return jsonify({'success': False, 'error': '参数不完整'}), 400
# 计算价格
result = calculate_upgrade_price(session['user_id'], to_plan, to_cycle, promo_code)
# 使用新的简化价格计算函数
result = calculate_subscription_price_simple(session['user_id'], to_plan, to_cycle, promo_code)
if 'error' in result:
return jsonify({
@@ -1630,7 +1684,16 @@ def calculate_subscription_price():
@app.route('/api/payment/create-order', methods=['POST'])
def create_payment_order():
"""创建支付订单(支持升级和优惠码)"""
"""
创建支付订单(新版:简化逻辑,不再记录升级)
Request Body:
{
"plan_name": "pro",
"billing_cycle": "yearly",
"promo_code": "WELCOME2025" // 可选
}
"""
try:
if 'user_id' not in session:
return jsonify({'success': False, 'error': '未登录'}), 401
@@ -1643,16 +1706,14 @@ def create_payment_order():
if not plan_name or not billing_cycle:
return jsonify({'success': False, 'error': '参数不完整'}), 400
# 计算价格(包括升级和优惠码)
price_result = calculate_upgrade_price(session['user_id'], plan_name, billing_cycle, promo_code)
# 使用新的简化价格计算
price_result = calculate_subscription_price_simple(session['user_id'], plan_name, billing_cycle, promo_code)
if 'error' in price_result:
return jsonify({'success': False, 'error': price_result['error']}), 400
amount = price_result['final_amount']
original_amount = price_result['original_amount']
discount_amount = price_result['discount_amount']
is_upgrade = price_result.get('is_upgrade', False)
subscription_type = price_result.get('subscription_type', 'new') # new 或 renew
# 创建订单
try:
@@ -1663,48 +1724,23 @@ def create_payment_order():
amount=amount
)
# 添加扩展字段(使用动态属性
if hasattr(order, 'original_amount') or True: # 兼容性检查
order.original_amount = original_amount
order.discount_amount = discount_amount
order.is_upgrade = is_upgrade
# 添加订阅类型标记(用于前端展示
order.remark = f"{subscription_type}订阅" if subscription_type == 'renew' else "新购订阅"
# 如果使用了优惠码,关联优惠码
if promo_code and price_result.get('promo_code'):
promo_obj = PromoCode.query.filter_by(code=promo_code.upper()).first()
if promo_obj:
# 如果使用了优惠码,关联优惠码
if promo_code and price_result.get('promo_code'):
promo_obj = PromoCode.query.filter_by(code=promo_code.upper()).first()
if promo_obj:
# 注意:需要在 PaymentOrder 表中添加 promo_code_id 字段
# 如果没有该字段,这行会报错,可以注释掉
try:
order.promo_code_id = promo_obj.id
# 如果是升级,记录原套餐信息
if is_upgrade:
order.upgrade_from_plan = price_result.get('current_plan')
except:
pass # 如果表中没有该字段,跳过
db.session.add(order)
db.session.commit()
# 如果是升级订单,创建升级记录
if is_upgrade and price_result.get('upgrade_type'):
try:
upgrade_record = SubscriptionUpgrade(
user_id=session['user_id'],
order_id=order.id,
from_plan=price_result['current_plan'],
from_cycle=price_result['current_cycle'],
from_end_date=datetime.fromisoformat(price_result['current_end_date']) if price_result.get('current_end_date') else None,
to_plan=plan_name,
to_cycle=billing_cycle,
to_end_date=beijing_now() + timedelta(days=365 if billing_cycle == 'yearly' else 30),
remaining_value=price_result['remaining_value'],
upgrade_amount=price_result['upgrade_amount'],
actual_amount=amount,
upgrade_type=price_result['upgrade_type']
)
db.session.add(upgrade_record)
db.session.commit()
except Exception as e:
print(f"创建升级记录失败: {e}")
# 不影响主流程
except Exception as e:
db.session.rollback()
return jsonify({'success': False, 'error': f'订单创建失败: {str(e)}'}), 500

427
database_migration.sql Normal file
View File

@@ -0,0 +1,427 @@
-- ============================================
-- 订阅支付系统数据库迁移 SQL
-- 版本: v2.0.0
-- 日期: 2025-11-19
-- ============================================
-- ============================================
-- 第一步: 备份现有数据
-- ============================================
-- 创建备份表
CREATE TABLE IF NOT EXISTS user_subscriptions_backup AS SELECT * FROM user_subscriptions;
CREATE TABLE IF NOT EXISTS payment_orders_backup AS SELECT * FROM payment_orders;
CREATE TABLE IF NOT EXISTS subscription_plans_backup AS SELECT * FROM subscription_plans;
-- ============================================
-- 第二步: 删除旧表(先删除外键依赖的表)
-- ============================================
DROP TABLE IF EXISTS subscription_upgrades; -- 删除升级表,不再使用
DROP TABLE IF EXISTS promo_code_usage; -- 暂时删除,稍后重建
DROP TABLE IF EXISTS payment_orders; -- 删除旧订单表
DROP TABLE IF EXISTS user_subscriptions; -- 删除旧订阅表
DROP TABLE IF EXISTS subscription_plans; -- 删除旧套餐表
-- ============================================
-- 第三步: 创建新表结构
-- ============================================
-- 1. 订阅套餐表(重构)
CREATE TABLE subscription_plans (
id INT PRIMARY KEY AUTO_INCREMENT,
plan_code VARCHAR(20) NOT NULL UNIQUE COMMENT '套餐代码: pro, max',
plan_name VARCHAR(50) NOT NULL COMMENT '套餐名称: Pro专业版, Max旗舰版',
description TEXT COMMENT '套餐描述',
features JSON COMMENT '功能列表',
-- 价格配置(所有周期价格)
price_monthly DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT '月付价格',
price_quarterly DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT '季付价格(3个月)',
price_semiannual DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT '半年付价格(6个月)',
price_yearly DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT '年付价格(12个月)',
-- 状态字段
is_active BOOLEAN DEFAULT TRUE COMMENT '是否启用',
display_order INT DEFAULT 0 COMMENT '展示顺序',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_plan_code (plan_code),
INDEX idx_active_order (is_active, display_order)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订阅套餐配置表';
-- 2. 用户订阅记录表(重构)
CREATE TABLE user_subscriptions (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL COMMENT '用户ID',
subscription_id VARCHAR(32) UNIQUE NOT NULL COMMENT '订阅ID(唯一标识)',
-- 订阅基本信息
plan_code VARCHAR(20) NOT NULL COMMENT '套餐代码: pro, max, free',
billing_cycle VARCHAR(20) NOT NULL COMMENT '计费周期: monthly, quarterly, semiannual, yearly',
-- 订阅时间
start_date DATETIME NOT NULL COMMENT '订阅开始时间',
end_date DATETIME NOT NULL COMMENT '订阅结束时间',
-- 订阅状态
status VARCHAR(20) NOT NULL DEFAULT 'active' COMMENT '状态: active(有效), expired(已过期), cancelled(已取消)',
is_current BOOLEAN DEFAULT FALSE COMMENT '是否为当前生效的订阅',
-- 支付信息
payment_order_id INT COMMENT '关联的支付订单ID',
paid_amount DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT '实际支付金额',
original_price DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT '原价',
discount_amount DECIMAL(10,2) DEFAULT 0 COMMENT '优惠金额',
-- 订阅类型
subscription_type VARCHAR(20) DEFAULT 'new' COMMENT '订阅类型: new(新购), renew(续费)',
previous_subscription_id VARCHAR(32) COMMENT '上一个订阅ID(续费时记录)',
-- 自动续费
auto_renew BOOLEAN DEFAULT FALSE COMMENT '是否自动续费',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_user_id (user_id),
INDEX idx_subscription_id (subscription_id),
INDEX idx_user_current (user_id, is_current),
INDEX idx_status (status),
INDEX idx_end_date (end_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户订阅记录表';
-- 3. 支付订单表(重构)
CREATE TABLE payment_orders (
id INT PRIMARY KEY AUTO_INCREMENT,
order_no VARCHAR(32) UNIQUE NOT NULL COMMENT '订单号',
user_id INT NOT NULL COMMENT '用户ID',
-- 订阅信息
plan_code VARCHAR(20) NOT NULL COMMENT '套餐代码',
billing_cycle VARCHAR(20) NOT NULL COMMENT '计费周期',
subscription_type VARCHAR(20) DEFAULT 'new' COMMENT '订阅类型: new(新购), renew(续费)',
-- 价格信息
original_price DECIMAL(10,2) NOT NULL COMMENT '原价',
discount_amount DECIMAL(10,2) DEFAULT 0 COMMENT '优惠金额',
final_amount DECIMAL(10,2) NOT NULL COMMENT '实付金额',
-- 优惠码
promo_code_id INT COMMENT '优惠码ID',
promo_code VARCHAR(50) COMMENT '优惠码',
-- 支付信息
payment_method VARCHAR(20) DEFAULT 'wechat' COMMENT '支付方式: wechat, alipay',
payment_channel VARCHAR(50) COMMENT '支付渠道详情',
transaction_id VARCHAR(64) COMMENT '第三方交易号',
qr_code_url TEXT COMMENT '支付二维码URL',
-- 订单状态
status VARCHAR(20) DEFAULT 'pending' COMMENT '状态: pending(待支付), paid(已支付), expired(已过期), cancelled(已取消)',
-- 时间信息
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
paid_at TIMESTAMP NULL COMMENT '支付时间',
expired_at TIMESTAMP NULL COMMENT '过期时间',
-- 备注
remark TEXT COMMENT '备注信息',
INDEX idx_order_no (order_no),
INDEX idx_user_id (user_id),
INDEX idx_status (status),
INDEX idx_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='支付订单表';
-- 4. 优惠码使用记录表(重建)
CREATE TABLE promo_code_usage (
id INT PRIMARY KEY AUTO_INCREMENT,
promo_code_id INT NOT NULL,
user_id INT NOT NULL,
order_id INT NOT NULL,
discount_amount DECIMAL(10,2) NOT NULL COMMENT '实际优惠金额',
used_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_promo_code (promo_code_id),
INDEX idx_user_id (user_id),
INDEX idx_order_id (order_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='优惠码使用记录表';
-- ============================================
-- 第四步: 插入初始数据
-- ============================================
-- 插入套餐数据
INSERT INTO subscription_plans (
plan_code,
plan_name,
description,
price_monthly,
price_quarterly,
price_semiannual,
price_yearly,
features,
display_order,
is_active
) VALUES
(
'pro',
'Pro 专业版',
'为专业投资者打造,解锁高级分析功能',
299.00,
799.00,
1499.00,
2699.00,
JSON_ARRAY(
'新闻信息流',
'历史事件对比',
'事件传导链分析(AI)',
'事件-相关标的分析',
'相关概念展示',
'AI复盘功能',
'企业概览',
'个股深度分析(AI) - 50家/月',
'高效数据筛选工具',
'概念中心(548大概念)',
'历史时间轴查询 - 100天',
'涨停板块数据分析',
'个股涨停分析'
),
1,
TRUE
),
(
'max',
'Max 旗舰版',
'旗舰级体验,无限制使用所有功能',
599.00,
1599.00,
2999.00,
5399.00,
JSON_ARRAY(
'全部 Pro 版功能',
'板块深度分析(AI)',
'个股深度分析(AI) - 无限制',
'历史时间轴查询 - 无限制',
'概念高频更新',
'优先客服支持',
'独家功能抢先体验'
),
2,
TRUE
);
-- ============================================
-- 第五步: 数据迁移(可选)
-- ============================================
-- 如果需要迁移旧数据,取消以下注释:
/*
-- 迁移旧的用户订阅数据
INSERT INTO user_subscriptions (
user_id,
subscription_id,
plan_code,
billing_cycle,
start_date,
end_date,
status,
is_current,
paid_amount,
original_price,
subscription_type,
auto_renew,
created_at
)
SELECT
user_id,
CONCAT('SUB_', id, '_', UNIX_TIMESTAMP(NOW())), -- 生成订阅ID
subscription_type, -- 将 subscription_type 映射为 plan_code
COALESCE(billing_cycle, 'yearly'), -- 默认年付
COALESCE(start_date, NOW()),
COALESCE(end_date, DATE_ADD(NOW(), INTERVAL 365 DAY)),
subscription_status,
TRUE, -- 设为当前订阅
0, -- 旧数据没有支付金额,设为0
0, -- 旧数据没有原价,设为0
'new', -- 默认为新购
COALESCE(auto_renewal, FALSE),
created_at
FROM user_subscriptions_backup
WHERE subscription_type IN ('pro', 'max'); -- 只迁移付费用户
*/
-- ============================================
-- 第六步: 创建免费订阅记录(为所有用户)
-- ============================================
-- 为所有现有用户创建免费订阅记录(如果没有付费订阅)
/*
INSERT INTO user_subscriptions (
user_id,
subscription_id,
plan_code,
billing_cycle,
start_date,
end_date,
status,
is_current,
paid_amount,
original_price,
subscription_type
)
SELECT
id AS user_id,
CONCAT('FREE_', id, '_', UNIX_TIMESTAMP(NOW())),
'free',
'monthly',
NOW(),
'2099-12-31 23:59:59', -- 免费版永久有效
'active',
TRUE,
0,
0,
'new'
FROM user
WHERE id NOT IN (
SELECT DISTINCT user_id FROM user_subscriptions WHERE plan_code IN ('pro', 'max')
);
*/
-- ============================================
-- 第七步: 验证数据完整性
-- ============================================
-- 检查套餐数据
SELECT * FROM subscription_plans;
-- 检查用户订阅数据
SELECT
plan_code,
COUNT(*) as user_count,
SUM(CASE WHEN is_current = TRUE THEN 1 ELSE 0 END) as current_count
FROM user_subscriptions
GROUP BY plan_code;
-- 检查支付订单数据
SELECT
status,
COUNT(*) as order_count,
SUM(final_amount) as total_amount
FROM payment_orders
GROUP BY status;
-- ============================================
-- 第八步: 添加外键约束(可选)
-- ============================================
-- 注意: 只有在确认 users 表存在且数据完整时才执行
-- ALTER TABLE user_subscriptions
-- ADD CONSTRAINT fk_user_subscriptions_user
-- FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
-- ALTER TABLE payment_orders
-- ADD CONSTRAINT fk_payment_orders_user
-- FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
-- ALTER TABLE payment_orders
-- ADD CONSTRAINT fk_payment_orders_promo
-- FOREIGN KEY (promo_code_id) REFERENCES promo_codes(id) ON DELETE SET NULL;
-- ALTER TABLE promo_code_usage
-- ADD CONSTRAINT fk_promo_usage_promo
-- FOREIGN KEY (promo_code_id) REFERENCES promo_codes(id) ON DELETE CASCADE;
-- ALTER TABLE promo_code_usage
-- ADD CONSTRAINT fk_promo_usage_user
-- FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
-- ALTER TABLE promo_code_usage
-- ADD CONSTRAINT fk_promo_usage_order
-- FOREIGN KEY (order_id) REFERENCES payment_orders(id) ON DELETE CASCADE;
-- ============================================
-- 第九步: 创建测试数据(开发环境)
-- ============================================
-- 插入测试优惠码
INSERT INTO promo_codes (
code,
description,
discount_type,
discount_value,
applicable_plans,
applicable_cycles,
max_total_uses,
max_uses_per_user,
valid_from,
valid_until,
is_active
) VALUES
(
'WELCOME2025',
'2025新用户专享',
'percentage',
20.00,
NULL, -- 适用所有套餐
NULL, -- 适用所有周期
1000,
1,
NOW(),
DATE_ADD(NOW(), INTERVAL 90 DAY),
TRUE
),
(
'YEAR2025',
'年付专享',
'percentage',
10.00,
NULL,
JSON_ARRAY('yearly'), -- 仅适用年付
500,
1,
NOW(),
DATE_ADD(NOW(), INTERVAL 365 DAY),
TRUE
),
(
'TESTCODE',
'测试优惠码 - 固定减100元',
'fixed_amount',
100.00,
NULL,
NULL,
100,
1,
NOW(),
DATE_ADD(NOW(), INTERVAL 30 DAY),
TRUE
);
-- ============================================
-- 迁移完成提示
-- ============================================
SELECT '===================================' AS '';
SELECT '数据库迁移完成!' AS '状态';
SELECT '===================================' AS '';
SELECT '请检查以下数据:' AS '提示';
SELECT '1. subscription_plans 表是否有2条记录 (pro, max)' AS '';
SELECT '2. user_subscriptions 表数据是否正确' AS '';
SELECT '3. payment_orders 表结构是否正确' AS '';
SELECT '4. 备份表 (*_backup) 已创建' AS '';
SELECT '===================================' AS '';
SELECT '下一步: 更新后端代码 (app.py, models.py)' AS '';
SELECT '===================================' AS '';

View File

@@ -0,0 +1,576 @@
# 订阅支付系统重新设计方案
## 📊 问题分析
### 现有系统的问题
1. **价格配置混乱**
- 季付和月付价格相同(配置错误)
- `monthly_price``yearly_price` 字段命名不清晰
- 缺少季付、半年付等周期的价格配置
2. **升级逻辑复杂且不合理**
- 计算剩余价值折算(按天计算 `remaining_value`
- 用户难以理解升级价格
- 续费用户和新用户价格不一致
- 逻辑复杂,容易出错
3. **按钮文案不清晰**
- 已订阅用户应显示"续费 Pro"/"续费 Max"
- 而不是"升级至 Pro"/"切换至 Pro"
4. **数据库表设计问题**
- `SubscriptionUpgrade` 表记录升级,但逻辑过于复杂
- `PaymentOrder` 表缺少必要字段
- 价格配置分散在多个字段
---
## ✨ 新设计方案
### 核心原则
1. **简化续费逻辑**: **续费用户与新用户价格完全一致**,不做任何折算
2. **清晰的价格体系**: 每个套餐每个周期都有明确的价格
3. **统一的用户体验**: 无论是新购还是续费,价格透明一致
4. **独立的订阅记录**: 每次支付都创建新的订阅记录(历史可追溯)
---
## 📐 数据库表设计
### 1. `subscription_plans` - 订阅套餐表(重构)
```sql
CREATE TABLE subscription_plans (
id INT PRIMARY KEY AUTO_INCREMENT,
plan_code VARCHAR(20) NOT NULL UNIQUE COMMENT '套餐代码: pro, max',
plan_name VARCHAR(50) NOT NULL COMMENT '套餐名称: Pro专业版, Max旗舰版',
description TEXT COMMENT '套餐描述',
features JSON COMMENT '功能列表',
-- 价格配置(所有周期价格)
price_monthly DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT '月付价格',
price_quarterly DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT '季付价格(3个月)',
price_semiannual DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT '半年付价格(6个月)',
price_yearly DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT '年付价格(12个月)',
-- 状态字段
is_active BOOLEAN DEFAULT TRUE COMMENT '是否启用',
display_order INT DEFAULT 0 COMMENT '展示顺序',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_plan_code (plan_code),
INDEX idx_active_order (is_active, display_order)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订阅套餐配置表';
```
**示例数据**:
```sql
INSERT INTO subscription_plans (plan_code, plan_name, description, price_monthly, price_quarterly, price_semiannual, price_yearly) VALUES
('pro', 'Pro 专业版', '为专业投资者打造', 299.00, 799.00, 1499.00, 2699.00),
('max', 'Max 旗舰版', '旗舰级体验', 599.00, 1599.00, 2999.00, 5399.00);
```
---
### 2. `user_subscriptions` - 用户订阅记录表(重构)
```sql
CREATE TABLE user_subscriptions (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL COMMENT '用户ID',
subscription_id VARCHAR(32) UNIQUE NOT NULL COMMENT '订阅ID(唯一标识)',
-- 订阅基本信息
plan_code VARCHAR(20) NOT NULL COMMENT '套餐代码: pro, max',
billing_cycle VARCHAR(20) NOT NULL COMMENT '计费周期: monthly, quarterly, semiannual, yearly',
-- 订阅时间
start_date DATETIME NOT NULL COMMENT '订阅开始时间',
end_date DATETIME NOT NULL COMMENT '订阅结束时间',
-- 订阅状态
status VARCHAR(20) NOT NULL DEFAULT 'active' COMMENT '状态: active(有效), expired(已过期), cancelled(已取消)',
is_current BOOLEAN DEFAULT FALSE COMMENT '是否为当前生效的订阅',
-- 支付信息
payment_order_id INT COMMENT '关联的支付订单ID',
paid_amount DECIMAL(10,2) NOT NULL COMMENT '实际支付金额',
original_price DECIMAL(10,2) NOT NULL COMMENT '原价',
discount_amount DECIMAL(10,2) DEFAULT 0 COMMENT '优惠金额',
-- 订阅类型
subscription_type VARCHAR(20) DEFAULT 'new' COMMENT '订阅类型: new(新购), renew(续费)',
previous_subscription_id VARCHAR(32) COMMENT '上一个订阅ID(续费时记录)',
-- 自动续费
auto_renew BOOLEAN DEFAULT FALSE COMMENT '是否自动续费',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_user_id (user_id),
INDEX idx_subscription_id (subscription_id),
INDEX idx_user_current (user_id, is_current),
INDEX idx_status (status),
INDEX idx_end_date (end_date),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户订阅记录表';
```
**设计说明**:
- 每次支付都创建新的订阅记录
- 通过 `is_current` 标识当前生效的订阅
- 支持订阅历史追溯
- 续费时记录 `previous_subscription_id` 形成订阅链
---
### 3. `payment_orders` - 支付订单表(重构)
```sql
CREATE TABLE payment_orders (
id INT PRIMARY KEY AUTO_INCREMENT,
order_no VARCHAR(32) UNIQUE NOT NULL COMMENT '订单号',
user_id INT NOT NULL COMMENT '用户ID',
-- 订阅信息
plan_code VARCHAR(20) NOT NULL COMMENT '套餐代码',
billing_cycle VARCHAR(20) NOT NULL COMMENT '计费周期',
subscription_type VARCHAR(20) DEFAULT 'new' COMMENT '订阅类型: new(新购), renew(续费)',
-- 价格信息
original_price DECIMAL(10,2) NOT NULL COMMENT '原价',
discount_amount DECIMAL(10,2) DEFAULT 0 COMMENT '优惠金额',
final_amount DECIMAL(10,2) NOT NULL COMMENT '实付金额',
-- 优惠码
promo_code_id INT COMMENT '优惠码ID',
promo_code VARCHAR(50) COMMENT '优惠码',
-- 支付信息
payment_method VARCHAR(20) DEFAULT 'wechat' COMMENT '支付方式: wechat, alipay',
payment_channel VARCHAR(50) COMMENT '支付渠道详情',
transaction_id VARCHAR(64) COMMENT '第三方交易号',
qr_code_url TEXT COMMENT '支付二维码URL',
-- 订单状态
status VARCHAR(20) DEFAULT 'pending' COMMENT '状态: pending(待支付), paid(已支付), expired(已过期), cancelled(已取消)',
-- 时间信息
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
paid_at TIMESTAMP NULL COMMENT '支付时间',
expired_at TIMESTAMP NULL COMMENT '过期时间',
-- 备注
remark TEXT COMMENT '备注信息',
INDEX idx_order_no (order_no),
INDEX idx_user_id (user_id),
INDEX idx_status (status),
INDEX idx_created_at (created_at),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (promo_code_id) REFERENCES promo_codes(id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='支付订单表';
```
---
### 4. `promo_codes` - 优惠码表(保持不变,微调)
```sql
CREATE TABLE promo_codes (
id INT PRIMARY KEY AUTO_INCREMENT,
code VARCHAR(50) UNIQUE NOT NULL COMMENT '优惠码',
description VARCHAR(200) COMMENT '描述',
-- 折扣类型
discount_type VARCHAR(20) NOT NULL COMMENT '折扣类型: percentage(百分比), fixed_amount(固定金额)',
discount_value DECIMAL(10,2) NOT NULL COMMENT '折扣值',
-- 适用范围
applicable_plans JSON COMMENT '适用套餐: ["pro", "max"] 或 null(全部)',
applicable_cycles JSON COMMENT '适用周期: ["monthly", "yearly"] 或 null(全部)',
min_amount DECIMAL(10,2) COMMENT '最低消费金额',
-- 使用限制
max_total_uses INT COMMENT '最大使用次数(总)',
max_uses_per_user INT DEFAULT 1 COMMENT '每用户最大使用次数',
current_uses INT DEFAULT 0 COMMENT '当前使用次数',
-- 有效期
valid_from TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '生效时间',
valid_until TIMESTAMP NULL COMMENT '过期时间',
-- 状态
is_active BOOLEAN DEFAULT TRUE COMMENT '是否启用',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_code (code),
INDEX idx_active (is_active),
INDEX idx_valid_period (valid_from, valid_until)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='优惠码表';
```
---
### 5. `promo_code_usage` - 优惠码使用记录表(保持不变)
```sql
CREATE TABLE promo_code_usage (
id INT PRIMARY KEY AUTO_INCREMENT,
promo_code_id INT NOT NULL,
user_id INT NOT NULL,
order_id INT NOT NULL,
discount_amount DECIMAL(10,2) NOT NULL COMMENT '实际优惠金额',
used_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_promo_code (promo_code_id),
INDEX idx_user_id (user_id),
INDEX idx_order_id (order_id),
FOREIGN KEY (promo_code_id) REFERENCES promo_codes(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (order_id) REFERENCES payment_orders(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='优惠码使用记录表';
```
---
### 6. 删除不必要的表
**删除 `subscription_upgrades` 表** - 不再需要复杂的升级逻辑
---
## 💡 业务逻辑设计
### 1. 价格计算逻辑(简化版)
```python
def calculate_subscription_price(plan_code, billing_cycle, promo_code=None):
"""
计算订阅价格(新购和续费价格完全一致)
Args:
plan_code: 套餐代码 (pro/max)
billing_cycle: 计费周期 (monthly/quarterly/semiannual/yearly)
promo_code: 优惠码(可选)
Returns:
dict: {
'plan_code': 'pro',
'billing_cycle': 'yearly',
'original_price': 2699.00,
'discount_amount': 0,
'final_amount': 2699.00,
'promo_code': None,
'promo_error': None
}
"""
# 1. 查询套餐价格
plan = SubscriptionPlan.query.filter_by(plan_code=plan_code, is_active=True).first()
if not plan:
return {'error': '套餐不存在'}
# 2. 获取对应周期的价格
price_field = f'price_{billing_cycle}'
original_price = getattr(plan, price_field, 0)
if original_price <= 0:
return {'error': '价格配置错误'}
result = {
'plan_code': plan_code,
'plan_name': plan.plan_name,
'billing_cycle': billing_cycle,
'original_price': float(original_price),
'discount_amount': 0,
'final_amount': float(original_price),
'promo_code': None,
'promo_error': None
}
# 3. 应用优惠码(如果有)
if promo_code:
promo, error = validate_promo_code(promo_code, plan_code, billing_cycle, original_price, user_id)
if promo:
discount = calculate_discount(promo, original_price)
result['discount_amount'] = float(discount)
result['final_amount'] = float(original_price - discount)
result['promo_code'] = promo.code
elif error:
result['promo_error'] = error
return result
```
**关键点**:
- ✅ 不再计算 `remaining_value`(剩余价值)
- ✅ 不再区分新购/续费价格
- ✅ 逻辑简单,易于维护
- ✅ 用户体验清晰透明
---
### 2. 创建订单逻辑
```python
def create_subscription_order(user_id, plan_code, billing_cycle, promo_code=None):
"""
创建订阅支付订单
"""
# 1. 计算价格
price_result = calculate_subscription_price(plan_code, billing_cycle, promo_code)
if 'error' in price_result:
return {'success': False, 'error': price_result['error']}
# 2. 判断是新购还是续费
current_sub = get_current_subscription(user_id)
subscription_type = 'new'
if current_sub and current_sub.plan_code in ['pro', 'max']:
subscription_type = 'renew'
# 3. 创建支付订单
order = PaymentOrder(
order_no=generate_order_no(user_id),
user_id=user_id,
plan_code=plan_code,
billing_cycle=billing_cycle,
subscription_type=subscription_type,
original_price=price_result['original_price'],
discount_amount=price_result['discount_amount'],
final_amount=price_result['final_amount'],
promo_code=promo_code,
status='pending',
expired_at=datetime.now() + timedelta(minutes=30)
)
db.session.add(order)
db.session.commit()
# 4. 生成支付二维码(微信支付)
qr_code_url = generate_wechat_qr_code(order)
order.qr_code_url = qr_code_url
db.session.commit()
return {'success': True, 'order': order.to_dict()}
```
---
### 3. 支付成功后的订阅激活逻辑
```python
def activate_subscription_after_payment(order_id):
"""
支付成功后激活订阅
"""
order = PaymentOrder.query.get(order_id)
if not order or order.status != 'paid':
return {'success': False, 'error': '订单状态错误'}
user_id = order.user_id
plan_code = order.plan_code
billing_cycle = order.billing_cycle
# 1. 计算订阅周期
cycle_days = {
'monthly': 30,
'quarterly': 90,
'semiannual': 180,
'yearly': 365
}
days = cycle_days.get(billing_cycle, 30)
# 2. 获取当前订阅
current_sub = UserSubscription.query.filter_by(
user_id=user_id,
is_current=True
).first()
# 3. 计算开始和结束时间
now = datetime.now()
if current_sub and current_sub.end_date > now:
# 续费:从当前订阅结束时间开始
start_date = current_sub.end_date
else:
# 新购:从当前时间开始
start_date = now
end_date = start_date + timedelta(days=days)
# 4. 创建新订阅记录
new_subscription = UserSubscription(
user_id=user_id,
subscription_id=generate_subscription_id(),
plan_code=plan_code,
billing_cycle=billing_cycle,
start_date=start_date,
end_date=end_date,
status='active',
is_current=True,
payment_order_id=order.id,
paid_amount=order.final_amount,
original_price=order.original_price,
discount_amount=order.discount_amount,
subscription_type=order.subscription_type,
previous_subscription_id=current_sub.subscription_id if current_sub else None
)
# 5. 将旧订阅标记为非当前
if current_sub:
current_sub.is_current = False
db.session.add(new_subscription)
db.session.commit()
return {'success': True, 'subscription': new_subscription.to_dict()}
```
**关键特性**:
- ✅ 续费时从**当前订阅结束时间**开始,避免浪费
- ✅ 每次支付都创建新的订阅记录
- ✅ 保留历史订阅记录(通过 `previous_subscription_id` 形成链)
- ✅ 逻辑清晰,易于理解
---
### 4. 按钮文案逻辑
```python
def get_subscription_button_text(user, plan_code, billing_cycle):
"""
获取订阅按钮文字
Args:
user: 用户对象
plan_code: 套餐代码 (pro/max)
billing_cycle: 计费周期
Returns:
str: 按钮文字
"""
current_sub = get_current_subscription(user.id)
# 1. 如果没有订阅或订阅已过期
if not current_sub or current_sub.plan_code == 'free' or current_sub.status != 'active':
return f"选择 {get_plan_display_name(plan_code)}"
# 2. 如果是当前套餐且周期相同
if current_sub.plan_code == plan_code and current_sub.billing_cycle == billing_cycle:
return f"续费 {get_plan_display_name(plan_code)}"
# 3. 如果是当前套餐但周期不同
if current_sub.plan_code == plan_code:
return f"切换至{get_cycle_display_name(billing_cycle)}"
# 4. 如果是不同套餐
return f"选择 {get_plan_display_name(plan_code)}"
def get_plan_display_name(plan_code):
names = {'pro': 'Pro 专业版', 'max': 'Max 旗舰版'}
return names.get(plan_code, plan_code)
def get_cycle_display_name(billing_cycle):
names = {
'monthly': '月付',
'quarterly': '季付',
'semiannual': '半年付',
'yearly': '年付'
}
return names.get(billing_cycle, billing_cycle)
```
**示例**:
- 免费用户看 Pro 年付: "选择 Pro 专业版"
- Pro 月付用户看 Pro 年付: "切换至年付"
- Pro 年付用户看 Pro 年付: "续费 Pro 专业版"
- Pro 用户看 Max 年付: "选择 Max 旗舰版"
---
## 📊 价格配置示例
### Pro 专业版价格设定
| 计费周期 | 价格 | 原价 | 折扣 | 月均价格 |
|---------|------|------|------|---------|
| 月付 | ¥299 | - | - | ¥299 |
| 季付(3个月) | ¥799 | ¥897 | 11% | ¥266 |
| 半年付(6个月) | ¥1499 | ¥1794 | 16% | ¥250 |
| 年付(12个月) | ¥2699 | ¥3588 | 25% | ¥225 |
### Max 旗舰版价格设定
| 计费周期 | 价格 | 原价 | 折扣 | 月均价格 |
|---------|------|------|------|---------|
| 月付 | ¥599 | - | - | ¥599 |
| 季付(3个月) | ¥1599 | ¥1797 | 11% | ¥533 |
| 半年付(6个月) | ¥2999 | ¥3594 | 17% | ¥500 |
| 年付(12个月) | ¥5399 | ¥7188 | 25% | ¥450 |
---
## 🔄 迁移方案
### 数据迁移 SQL
参见 `database_migration.sql`
### 代码迁移步骤
1. **备份现有数据库**
2. **执行数据库迁移 SQL**
3. **更新数据库模型** (`models.py`)
4. **更新价格计算逻辑** (`calculate_price.py`)
5. **更新 API 路由** (`routes.py`)
6. **更新前端组件** (`SubscriptionContentNew.tsx`)
7. **测试完整流程**
8. **灰度发布**
---
## ✅ 优势总结
### 相比旧系统的改进
1. **价格透明** - 续费用户和新用户价格完全一致
2. **逻辑简化** - 不再计算剩余价值,代码减少 50%+
3. **易于理解** - 用户体验更清晰
4. **灵活扩展** - 轻松添加新的计费周期
5. **历史追溯** - 完整的订阅历史记录
6. **数据完整** - 每次支付都有完整的记录
### 用户体验改进
1. **按钮文案清晰** - "续费 Pro"/"选择 Pro"明确表达意图
2. **价格一致性** - 所有用户看到的价格都一样
3. **无隐藏费用** - 不会因为"升级折算"产生困惑
4. **透明计费** - 支付金额 = 显示价格 - 优惠码折扣
---
## 📝 后续优化建议
1. **自动续费** - 到期前自动扣款续费
2. **订阅提醒** - 到期前 7 天、3 天、1 天发送通知
3. **订阅暂停** - 允许用户暂停订阅
4. **订阅降级** - 从 Max 降级到 Pro当前周期结束后生效
5. **发票管理** - 支持开具电子发票
6. **支付方式扩展** - 支持支付宝、银行卡等
---
**设计时间**: 2025-11-19
**设计者**: Claude Code
**版本**: v2.0.0

339
index.pug Normal file
View File

@@ -0,0 +1,339 @@
extends layouts/layout
block content
+header(true, false, false)
<div class="overflow-hidden">
// hero
<div class="relative pt-58 pb-20 max-xl:pt-48 max-lg:pt-44 max-md:pt-21 max-md:pb-15">
<div class="center relative z-3" data-aos="fade">
<div class="max-w-187">
<div class="inline-flex items-center gap-2 mb-6 px-4 py-2 rounded-full bg-gradient-to-r from-green/20 to-green/5 border border-green/30 backdrop-blur-sm max-md:mb-3">
<svg class="size-4 fill-green" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path d="M8 0L9.798 5.579L15.708 4.292L11.854 8.854L15.708 13.416L9.798 12.129L8 18L6.202 12.421L0.292 13.708L4.146 9.146L0.292 4.584L6.202 5.871L8 0Z"/>
</svg>
<span class="text-title-5 text-green max-md:text-[14px]">金融AI技术领航者</span>
</div>
<div class="mb-8 text-big-title-1 bg-radial-white-1 bg-clip-text text-transparent max-xl:text-big-title-2 max-lg:text-title-1 max-lg:mb-10 max-md:mb-6 max-md:text-big-title-mobile">智能舆情分析系统</div>
<div class="flex flex-wrap gap-3 mb-8 max-lg:mb-6 max-md:mb-4">
<div class="inline-flex items-center gap-2 px-3.5 py-2 rounded-lg bg-black/30 border border-line/50 backdrop-blur-sm">
<svg class="size-4 fill-green" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zm0 14c-3.3 0-6-2.7-6-6s2.7-6 6-6 6 2.7 6 6-2.7 6-6 6zm3.5-6c0 1.9-1.6 3.5-3.5 3.5S4.5 9.9 4.5 8 6.1 4.5 8 4.5 11.5 6.1 11.5 8z"/>
</svg>
<span class="text-title-5 text-white/90 max-md:text-[13px]">深度数据挖掘</span>
</div>
<div class="inline-flex items-center gap-2 px-3.5 py-2 rounded-lg bg-black/30 border border-line/50 backdrop-blur-sm">
<svg class="size-4 fill-green" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path d="M13.5 2h-11C1.7 2 1 2.7 1 3.5v9c0 .8.7 1.5 1.5 1.5h11c.8 0 1.5-.7 1.5-1.5v-9c0-.8-.7-1.5-1.5-1.5zM8 11.5c-1.9 0-3.5-1.6-3.5-3.5S6.1 4.5 8 4.5s3.5 1.6 3.5 3.5-1.6 3.5-3.5 3.5z"/>
</svg>
<span class="text-title-5 text-white/90 max-md:text-[13px]">7×24小时监控</span>
</div>
</div>
<div class="max-w-94 mb-9.5 text-description max-lg:max-w-76 max-md:max-w-full max-md:mb-3.5">基于金融领域微调的大语言模型7×24小时不间断对舆情数据进行深度挖掘和分析对历史事件进行复盘关联相关标的为投资决策提供前瞻性的智能洞察。</div>
<div class="flex gap-7.5 max-md:mb-12.5">
<a class="wechat-icon-link fill-white transition-colors hover:fill-green relative" href="javascript:void(0)" data-wechat-img="wechat-app.jpg" title="微信小程序">
<svg class="size-5 fill-inherit" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20">
<path d="M13.889 8.333c.31 0 .611.028.903.078C14.292 5.31 11.403 3 7.917 3 4.083 3 1 5.686 1 9.028c0 1.944 1.028 3.639 2.639 4.861L3 16.111l2.5-1.25c.833.194 1.528.333 2.417.333.278 0 .556-.014.833-.042-.278-.805-.417-1.652-.417-2.513 0-3.264 2.764-5.903 6.556-5.903v-.403zM10.139 6.528c.583 0 1.055.472 1.055 1.055s-.472 1.055-1.055 1.055-1.055-.472-1.055-1.055.472-1.055 1.055-1.055zM5.694 8.639c-.583 0-1.055-.472-1.055-1.055s.472-1.055 1.055-1.055 1.055.472 1.055 1.055-.472 1.055-1.055 1.055zm8.195 1.694c-2.847 0-5.139 2.014-5.139 4.486 0 2.472 2.292 4.486 5.139 4.486.764 0 1.528-.139 2.222-.347L18.333 20l-.625-1.875c1.25-.972 2.014-2.361 2.014-3.958 0-2.472-2.292-4.486-5.139-4.486h-.694zm-2.084 3.125c.389 0 .695.306.695.694s-.306.695-.695.695-.694-.306-.694-.695.305-.694.694-.694zm4.167 0c.389 0 .694.306.694.694s-.305.695-.694.695-.695-.306-.695-.695.306-.694.695-.694z"/>
</svg>
</a>
<a class="wechat-icon-link fill-white transition-colors hover:fill-green relative" href="javascript:void(0)" data-wechat-img="public.jpg" title="微信公众号">
<svg class="size-5 fill-inherit" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20">
<path d="M10 0C4.477 0 0 4.477 0 10s4.477 10 10 10 10-4.477 10-10S15.523 0 10 0zm3.889 6.944c.139 0 .278.014.417.028-1.306-2.958-4.723-5.027-8.611-5.027C2.611 1.945 0 4.306 0 7.222c0 1.528.806 2.861 2.083 3.819l-.417 1.945 1.945-.972c.639.139 1.167.25 1.861.25.222 0 .444-.014.667-.028-.222-.639-.333-1.306-.333-1.986 0-2.569 2.181-4.653 5.139-4.653l.944-.653zm-5.278-2.5c.458 0 .833.375.833.833s-.375.833-.833.833-.833-.375-.833-.833.375-.833.833-.833zM4.167 6.111c-.458 0-.833-.375-.833-.833s.375-.833.833-.833.833.375.833.833-.375.833-.833.833zm9.722 3.333c-2.236 0-4.028 1.583-4.028 3.528s1.792 3.528 4.028 3.528c.597 0 1.194-.111 1.736-.278l1.542.694-.486-1.472c.972-.764 1.597-1.861 1.597-3.125 0-1.945-1.792-3.528-4.028-3.528h-.361zm-1.667 2.5c.306 0 .556.25.556.556s-.25.556-.556.556-.556-.25-.556-.556.25-.556.556-.556zm3.334 0c.305 0 .555.25.555.556s-.25.556-.555.556-.556-.25-.556-.556.25-.556.556-.556z"/>
</svg>
</a>
<a class="wechat-icon-link fill-white transition-colors hover:fill-green relative" href="javascript:void(0)" data-wechat-img="customer-service.jpg" title="微信客服号">
<svg class="size-5 fill-inherit" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20">
<path d="M10 0C4.477 0 0 4.477 0 10s4.477 10 10 10 10-4.477 10-10S15.523 0 10 0zm4.861 7.222c.167 0 .333.014.5.028C14.097 4.444 11.139 2 7.5 2 3.889 2 1 4.444 1 7.5c0 1.778.972 3.333 2.5 4.444l-.5 2.223 2.222-1.111c.722.167 1.333.278 2.111.278.278 0 .556-.014.834-.028-.278-.722-.417-1.5-.417-2.306 0-2.972 2.5-5.389 5.833-5.389l1.278-.389zm-6.028-2.777c.528 0 .945.417.945.945s-.417.944-.945.944-.944-.416-.944-.944.416-.945.944-.945zm-4.166 1.888c-.528 0-.945-.416-.945-.944s.417-.945.945-.945.944.417.944.945-.416.944-.944.944zm10.277 3.611c-2.569 0-4.611 1.806-4.611 4.028s2.042 4.028 4.611 4.028c.694 0 1.389-.125 2-.306L19 18.889l-.556-1.667c1.111-.889 1.833-2.139 1.833-3.611 0-2.222-2.042-4.028-4.611-4.028h-.722zm-1.944 2.778c.361 0 .639.278.639.639s-.278.639-.639.639-.639-.278-.639-.639.278-.639.639-.639zm3.889 0c.361 0 .639.278.639.639s-.278.639-.639.639-.639-.278-.639-.639.278-.639.639-.639zM10 14.444c0 .306-.25.556-.556.556H6.111c-.306 0-.556-.25-.556-.556s.25-.555.556-.555h3.333c.306 0 .556.25.556.555z"/>
</svg>
</a>
</div>
<div class="absolute right-20 bottom-0 flex gap-4 max-xl:right-10 max-md:static">
<div class="relative w-42 p-5 pb-6.5 rounded-[1.25rem] bg-content text-center shadow-1 backdrop-blur-[1.25rem] max-md:px-3">
<div class="absolute inset-0 border border-line rounded-[1.25rem] pointer-events-none"></div>
<div class="relative flex justify-center items-center size-11 mx-auto mb-4 rounded-lg bg-gradient-to-b from-black/15 to-white/15 shadow-[0.0625rem_0.0625rem_0.0625rem_0_rgba(255,255,255,0.10)_inset,0_0_0.625rem_0_rgba(255,255,255,0.10)_inset]">
<div class="absolute inset-0 border border-line rounded-lg"></div>
img(class="w-5" src=require('Images/clock.svg') alt="")
</div>
<div class="text-title-4 max-md:text-title-3-mobile">实时数据分析</div>
</div>
<div class="relative w-42 p-5 pb-6.5 rounded-[1.25rem] bg-content text-center shadow-1 backdrop-blur-[1.25rem] max-md:px-3">
<div class="absolute inset-0 border border-line rounded-[1.25rem] pointer-events-none"></div>
<div class="relative flex justify-center items-center size-11 mx-auto mb-4 rounded-lg bg-gradient-to-b from-black/15 to-white/15 shadow-[0.0625rem_0.0625rem_0.0625rem_0_rgba(255,255,255,0.10)_inset,0_0_0.625rem_0_rgba(255,255,255,0.10)_inset]">
<div class="absolute inset-0 border border-line rounded-lg"></div>
img(class="w-5" src=require('Images/floor.svg') alt="")
</div>
<div class="text-title-4 max-md:text-title-3-mobile">低延迟推理</div>
</div>
</div>
</div>
</div>
<div class="absolute top-23 right-[calc(50%-28.5rem)] size-178 rounded-full max-xl:size-140 max-md:top-36 max-md:right-auto max-md:left-8.5 max-md:size-133">
<div class="absolute -inset-[10%] mask-radial-at-center mask-radial-from-20% mask-radial-to-52%">
video(class="w-full" src=require('Videos/video-1.webm') autoplay loop muted playsinline)
</div>
<div class="absolute inset-0 rounded-full shadow-[0.875rem_1.0625rem_1.25rem_0_rgba(255,255,255,0.25)_inset] bg-black/1"></div>
</div>
<div>
<div class="absolute top-61.5 right-[calc(50%-35.18rem)] z-2 size-116.5 bg-green/20 rounded-full blur-[8rem] max-md:top-36 max-lg:-right-96 max-md:left-74 max-md:right-auto"></div>
<div class="absolute top-77 left-[calc(50%-57.5rem)] z-2 size-116.5 bg-green/20 rounded-full blur-[8rem] max-lg:-left-60 max-md:top-84 max-md:-left-52 max-md:size-80"></div>
</div>
</div>
// details
<div class="pt-40.5 pb-30.5 max-xl:pt-30 max-lg:py-24 max-md:py-15">
<div class="center">
<div class="flex flex-wrap -mt-4 -mx-2">
<div class="relative min-h-75 rounded-[1.25rem] bg-content shadow-2 backdrop-blur-[1.25rem] after:absolute after:inset-0 after:border after:border-line after:rounded-[1.25rem] after:pointer-events-none max-xl:min-h-70 flex w-[calc(50%-1rem)] h-full mt-4 mx-2 pt-6 pb-7 px-8.5 max-xl:px-6 max-lg:w-[calc(100%-1rem)] max-md:px-8 max-md:min-h-112.5" data-aos="fade">
<div class="relative z-2 max-w-58 flex flex-col max-md:max-w-full">
<div class="mb-auto bg-radial-white-2 bg-clip-text text-transparent text-title-1 max-xl:text-title-2 max-md:mb-0.5 max-md:text-title-1-mobile">99%</div>
<div class="mt-3 text-title-4 max-md:text-title-3-mobile">金融数据理解准确率</div>
<div class="mt-2.5 text-description max-md:mt-2">基于金融领域深度微调的大语言模型,精准理解市场动态和舆情变化。</div>
</div>
<div class="absolute top-0 right-0 bottom-0 flex items-center max-2xl:-right-16 max-lg:right-0 max-md:top-auto max-md:left-0 max-md:pl-7.5">
img(class="w-86.25 max-xl:w-72 max-md:w-full" src=require('Images/details-pic-1.png') alt="")
</div>
</div>
<div class="relative min-h-75 rounded-[1.25rem] bg-content shadow-2 backdrop-blur-[1.25rem] after:absolute after:inset-0 after:border after:border-line after:rounded-[1.25rem] after:pointer-events-none max-xl:min-h-70 flex w-[calc(50%-1rem)] h-full mt-4 mx-2 pt-6 pb-7 px-8.5 max-xl:px-6 max-lg:w-[calc(100%-1rem)] max-md:px-8 max-md:min-h-112.5" data-aos="fade">
<div class="relative z-2 max-w-58 flex flex-col max-md:max-w-full">
<div class="mb-auto bg-radial-white-2 bg-clip-text text-transparent text-title-1 max-xl:text-title-2 max-md:mb-0.5 max-md:text-title-1-mobile">24/7</div>
<div class="mt-3 text-title-4 max-md:text-title-3-mobile">全天候舆情监控</div>
<div class="mt-2.5 text-description max-md:mt-2">7×24小时不间断监控市场舆情第一时间捕捉关键信息。</div>
</div>
<div class="absolute top-0 right-0 bottom-0 flex items-center max-2xl:-right-16 max-lg:right-0 max-md:top-auto max-md:left-0 max-md:pl-7.5">
img(class="w-86.25 max-xl:w-72 max-md:w-full" src=require('Images/details-pic-2.png') alt="")
</div>
</div>
<div class="relative min-h-75 rounded-[1.25rem] bg-content shadow-2 backdrop-blur-[1.25rem] after:absolute after:inset-0 after:border after:border-line after:rounded-[1.25rem] after:pointer-events-none max-xl:min-h-70 flex items-end w-62.5 mt-4 mx-2 px-8.5 pb-7 max-xl:px-6 max-lg:w-[calc(50%-1rem)] max-md:w-[calc(100%-1rem)] max-md:min-h-72 max-md:px-7 max-md:pb-6" data-aos="fade">
<div class="absolute top-0 left-0 right-0 flex justify-center">
img(class="w-full max-lg:max-w-60 max-md:max-w-73.5" src=require('Images/details-pic-3.png') alt="")
</div>
<div class="relative z-2 max-w-58 flex flex-col">
<div class="mb-2.5 text-title-4 max-md:mb-1.5 max-md:text-title-3-mobile">深度模型微调</div>
<div class="text-description">针对金融领域数据进行专业化模型训练和优化。</div>
</div>
</div>
<div class="relative min-h-75 rounded-[1.25rem] bg-content shadow-2 backdrop-blur-[1.25rem] after:absolute after:inset-0 after:border after:border-line after:rounded-[1.25rem] after:pointer-events-none max-xl:min-h-70 flex items-end grow mt-4 mx-2 px-8.5 pb-7 overflow-hidden max-xl:px-6 max-lg:order-5" data-aos="fade">
<div class="absolute top-0 left-0 flex justify-center max-2xl:top-8 max-lg:top-0 max-md:-left-3 max-md:w-176">
img(class="w-full" src=require('Images/details-pic-4.png') alt="")
</div>
<div class="relative z-2 max-w-58 flex flex-col">
<div class="flex items-center gap-3 mb-3">
<div class="relative flex justify-center items-center shrink-0 w-12.5 h-12.5 rounded-lg bg-gradient-to-b from-[#F4D03F] to-[#D4AF37] shadow-[0.0625rem_0.0625rem_0.0625rem_0_rgba(212,175,55,0.30)_inset,_0_0_0.625rem_0_rgba(212,175,55,0.50)_inset] after:absolute after:inset-0 after:border after:border-line after:rounded-lg after:pointer-events-none">
img(class="w-4" src=require('Images/lightning.svg') alt="")
</div>
<div class="bg-radial-white-2 bg-clip-text text-transparent text-title-2 leading-tight max-xl:text-title-2 max-md:text-title-1-mobile">&lt;100ms</div>
</div>
<div class="text-title-4 max-md:text-title-3-mobile">低延迟推理系统</div>
<div class="mt-2.5 text-description max-md:mt-2">毫秒级响应速度,实时处理海量舆情数据。</div>
</div>
</div>
<div class="relative min-h-75 rounded-[1.25rem] bg-content shadow-2 backdrop-blur-[1.25rem] after:absolute after:inset-0 after:border after:border-line after:rounded-[1.25rem] after:pointer-events-none max-xl:min-h-70 flex items-end w-62.5 mt-4 mx-2 px-8.5 pb-7 max-xl:px-6 max-lg:w-[calc(50%-1rem)] max-md:w-[calc(100%-1rem)] max-md:min-h-72 max-md:px-7 max-md:pb-6" data-aos="fade">
<div class="absolute top-0 left-0 right-0 flex justify-center">
img(class="w-full max-lg:max-w-60 max-md:max-w-73.5" src=require('Images/details-pic-5.png') alt="")
</div>
<div class="relative z-2 max-w-58 flex flex-col">
<div class="bg-radial-white-2 bg-clip-text text-transparent text-title-1 max-xl:text-title-2 max-md:text-title-1-mobile">历史复盘</div>
<div class="text-description">对历史事件进行深度复盘分析,关联标的,辅助投资决策。</div>
</div>
</div>
</div>
</div>
</div>
// features
<div class="relative pt-34.5 pb-41 max-xl:pt-20 max-xl:pb-30 max-lg:py-24 max-md:pt-15 max-md:pb-14">
<div class="center relative z-2">
<div class="max-w-148 mx-auto mb-18 text-center max-xl:mb-14 max-md:mb-8.5" data-aos="fade">
<div class="label mb-3 max-md:mb-1">核心功能</div>
<div class="mb-6 bg-radial-white-2 bg-clip-text text-transparent text-title-1 max-lg:text-title-2 max-md:mb-3 max-md:text-title-1-mobile">我们能做什么?</div>
<div class="text-description">基于AI的舆情分析系统深度挖掘市场动态为投资决策提供实时智能洞察。</div>
</div>
<div class="flex flex-wrap -mt-4 -mx-2">
<div class="relative w-[calc(25%-1rem)] mt-4 mx-2 rounded-[1.25rem] bg-content shadow-2 backdrop-blur-[1.25rem] after:absolute after:inset-0 after:border after:border-line after:rounded-[1.25rem] after:pointer-events-none max-lg:w-[calc(50%-1rem)] max-md:w-[calc(100%-1rem)]" data-aos="fade">
<div class="max-md:text-center">
img(class="w-full max-md:max-w-73.5" src=require('Images/features-pic-1.png') alt="")
</div>
<div class="pt-0.5 px-8.5 pb-7.5 max-xl:px-5 max-xl:pb-5 max-lg:px-8 max-lg:pb-7 max-md:pb-6">
<div class="mb-2.5 text-title-4 max-md:mb-1 max-md:text-title-2-mobile">舆情数据挖掘</div>
<div class="text-description">实时采集和分析全网金融舆情,捕捉市场情绪变化。</div>
</div>
</div>
<div class="relative w-[calc(25%-1rem)] mt-4 mx-2 rounded-[1.25rem] bg-content shadow-2 backdrop-blur-[1.25rem] after:absolute after:inset-0 after:border after:border-line after:rounded-[1.25rem] after:pointer-events-none max-lg:w-[calc(50%-1rem)] max-md:w-[calc(100%-1rem)]" data-aos="fade">
<div class="max-md:text-center">
img(class="w-full max-md:max-w-73.5" src=require('Images/features-pic-2.png') alt="")
</div>
<div class="pt-0.5 px-8.5 pb-7.5 max-xl:px-5 max-xl:pb-5 max-lg:px-8 max-lg:pb-7 max-md:pb-6">
<div class="mb-2.5 text-title-4 max-md:mb-1 max-md:text-title-2-mobile">智能事件关联</div>
<div class="text-description">自动关联相关标的和历史事件,构建完整的信息图谱。</div>
</div>
</div>
<div class="relative w-[calc(25%-1rem)] mt-4 mx-2 rounded-[1.25rem] bg-content shadow-2 backdrop-blur-[1.25rem] after:absolute after:inset-0 after:border after:border-line after:rounded-[1.25rem] after:pointer-events-none max-lg:w-[calc(50%-1rem)] max-md:w-[calc(100%-1rem)]" data-aos="fade">
<div class="max-md:text-center">
img(class="w-full max-md:max-w-73.5" src=require('Images/features-pic-3.png') alt="")
</div>
<div class="pt-0.5 px-8.5 pb-7.5 max-xl:px-5 max-xl:pb-5 max-lg:px-8 max-lg:pb-7 max-md:pb-6">
<div class="mb-2.5 text-title-4 max-md:mb-1 max-md:text-title-2-mobile">历史复盘</div>
<div class="text-description">深度复盘历史事件走势,洞察关键节点与转折,为投资决策提供经验参考。</div>
</div>
</div>
<div class="relative w-[calc(25%-1rem)] mt-4 mx-2 rounded-[1.25rem] bg-content shadow-2 backdrop-blur-[1.25rem] after:absolute after:inset-0 after:border after:border-line after:rounded-[1.25rem] after:pointer-events-none max-lg:w-[calc(50%-1rem)] max-md:w-[calc(100%-1rem)]" data-aos="fade">
<div class="max-md:text-center">
img(class="w-full max-md:max-w-73.5" src=require('Images/features-pic-4.png') alt="")
</div>
<div class="pt-0.5 px-8.5 pb-7.5 max-xl:px-5 max-xl:pb-5 max-lg:px-8 max-lg:pb-7 max-md:pb-6">
<div class="mb-2.5 text-title-4 max-md:mb-1 max-md:text-title-2-mobile">专精金融的AI聊天</div>
<div class="text-description">基于金融领域深度训练的智能对话助手,即时解答市场问题,提供专业投资建议。</div>
</div>
</div>
</div>
</div>
<div class="max-md:hidden">
<div class="absolute top-47.5 left-[calc(50%-52.38rem)] size-98.5 bg-gold/15 rounded-full blur-[6.75rem]"></div>
<div class="absolute bottom-2.5 right-[calc(50%-51.44rem)] size-98.5 bg-gold/15 rounded-full blur-[6.75rem]"></div>
</div>
</div>
// pricing
<div class="pt-34.5 pb-25 max-2xl:pt-25 max-lg:py-20 max-md:py-15" id="pricing">
<div class="center">
<div class="max-w-175 mx-auto mb-17.5 text-center max-xl:mb-14 max-md:mb-8" data-aos="fade">
<div class="label mb-3 max-md:mb-1.5">订阅方案</div>
<div class="bg-radial-white-2 bg-clip-text text-transparent text-title-1 max-lg:text-title-2 max-md:text-title-1-mobile">立即开启智能决策</div>
</div>
<div class="flex justify-center gap-4 max-lg:-mx-10 max-lg:px-10 max-lg:overflow-x-auto max-lg:scrollbar-none max-md:-mx-5 max-md:px-5" data-aos="fade">
<div class="relative flex flex-col flex-1 max-w-md rounded-[1.25rem] overflow-hidden shadow-[0.0625rem_0.0625rem_0.0625rem_0_rgba(255,255,255,0.10)_inset] after:absolute after:inset-0 after:border after:border-line after:rounded-[1.25rem] after:pointer-events-none max-lg:shrink-0 max-lg:flex-auto max-lg:w-84">
<div class="relative z-2 pt-8 px-8.5 pb-10 text-title-4 max-md:text-title-5 text-white">PRO</div>
<div class="relative z-3 flex flex-col grow -mt-5 p-3.5 pb-8.25 backdrop-blur-[1.25rem] bg-white/1 rounded-[1.25rem] after:absolute after:inset-0 after:border after:border-line after:rounded-[1.25rem] after:pointer-events-none">
<div class="relative mb-8 p-5 rounded-[0.8125rem] bg-white/2 backdrop-blur-[1.25rem] shadow-2 after:absolute after:inset-0 after:border after:border-line after:rounded-[0.8125rem] after:pointer-events-none">
<div class="flex items-end gap-3 mb-4">
<div class="bg-radial-white-2 bg-clip-text text-transparent text-title-1 leading-[3.1rem] max-xl:text-title-2 max-xl:leading-[2.4rem]">¥198</div>
<div class="text-title-5">/月</div>
</div>
<a class="btn btn-secondary w-full bg-line !text-description hover:!text-white" href="https://valuefrontier.cn/home/pages/account/subscription" target="_blank">选择Pro版</a>
</div>
<div class="flex flex-col gap-6.5 px-3.5 max-xl:px-0 max-xl:gap-5 max-md:px-3.5">
<div class="flex items-center gap-2.5 text-description max-xl:text-description-2 max-md:text-description-mobile">
<div class="flex justify-center items-center shrink-0 size-5 bg-green rounded-full shadow-[0.0625rem_0.0625rem_0.0625rem_0_rgba(255,255,255,0.20)_inset,_0_0_0.625rem_0_rgba(255,255,255,0.50)_inset]">
<svg class="size-5 fill-black" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20">
<path d="M13.47 6.97A.75.75 0 0 1 14.53 8.03l-5 5a.75.75 0 0 1-1.061 0l-3-3A.75.75 0 0 1 6.53 8.97L9 11.439l4.47-4.469z" />
</svg>
</div>
<div>事件关联股票深度分析</div>
</div>
<div class="flex items-center gap-2.5 text-description max-xl:text-description-2 max-md:text-description-mobile">
<div class="flex justify-center items-center shrink-0 size-5 bg-green rounded-full shadow-[0.0625rem_0.0625rem_0.0625rem_0_rgba(255,255,255,0.20)_inset,_0_0_0.625rem_0_rgba(255,255,255,0.50)_inset]">
<svg class="size-5 fill-black" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20">
<path d="M13.47 6.97A.75.75 0 0 1 14.53 8.03l-5 5a.75.75 0 0 1-1.061 0l-3-3A.75.75 0 0 1 6.53 8.97L9 11.439l4.47-4.469z" />
</svg>
</div>
<div>历史事件智能对比复盘</div>
</div>
<div class="flex items-center gap-2.5 text-description max-xl:text-description-2 max-md:text-description-mobile">
<div class="flex justify-center items-center shrink-0 size-5 bg-green rounded-full shadow-[0.0625rem_0.0625rem_0.0625rem_0_rgba(255,255,255,0.20)_inset,_0_0_0.625rem_0_rgba(255,255,255,0.50)_inset]">
<svg class="size-5 fill-black" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20">
<path d="M13.47 6.97A.75.75 0 0 1 14.53 8.03l-5 5a.75.75 0 0 1-1.061 0l-3-3A.75.75 0 0 1 6.53 8.97L9 11.439l4.47-4.469z" />
</svg>
</div>
<div>事件概念关联与挖掘</div>
</div>
<div class="flex items-center gap-2.5 text-description max-xl:text-description-2 max-md:text-description-mobile">
<div class="flex justify-center items-center shrink-0 size-5 bg-green rounded-full shadow-[0.0625rem_0.0625rem_0.0625rem_0_rgba(255,255,255,0.20)_inset,_0_0_0.625rem_0_rgba(255,255,255,0.50)_inset]">
<svg class="size-5 fill-black" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20">
<path d="M13.47 6.97A.75.75 0 0 1 14.53 8.03l-5 5a.75.75 0 0 1-1.061 0l-3-3A.75.75 0 0 1 6.53 8.97L9 11.439l4.47-4.469z" />
</svg>
</div>
<div>概念板块个股追踪</div>
</div>
<div class="flex items-center gap-2.5 text-description max-xl:text-description-2 max-md:text-description-mobile">
<div class="flex justify-center items-center shrink-0 size-5 bg-green rounded-full shadow-[0.0625rem_0.0625rem_0.0625rem_0_rgba(255,255,255,0.20)_inset,_0_0_0.625rem_0_rgba(255,255,255,0.50)_inset]">
<svg class="size-5 fill-black" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20">
<path d="M13.47 6.97A.75.75 0 0 1 14.53 8.03l-5 5a.75.75 0 0 1-1.061 0l-3-3A.75.75 0 0 1 6.53 8.97L9 11.439l4.47-4.469z" />
</svg>
</div>
<div>概念深度研报与解读</div>
</div>
<div class="flex items-center gap-2.5 text-description max-xl:text-description-2 max-md:text-description-mobile">
<div class="flex justify-center items-center shrink-0 size-5 bg-green rounded-full shadow-[0.0625rem_0.0625rem_0.0625rem_0_rgba(255,255,255,0.20)_inset,_0_0_0.625rem_0_rgba(255,255,255,0.50)_inset]">
<svg class="size-5 fill-black" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20">
<path d="M13.47 6.97A.75.75 0 0 1 14.53 8.03l-5 5a.75.75 0 0 1-1.061 0l-3-3A.75.75 0 0 1 6.53 8.97L9 11.439l4.47-4.469z" />
</svg>
</div>
<div>个股异动实时预警</div>
</div>
</div>
</div>
</div>
<div class="relative flex flex-col flex-1 max-w-md rounded-[1.25rem] overflow-hidden shadow-2 before:absolute before:-top-20 before:left-1/2 before:z-1 before:-translate-x-1/2 before:w-65 before:h-57 before:bg-gold/15 before:rounded-full before:blur-[3.375rem] after:absolute after:inset-0 after:border after:border-gold/30 after:rounded-[1.25rem] after:pointer-events-none max-lg:shrink-0 max-lg:flex-auto max-lg:w-84">
<div class="absolute -top-36 left-13 w-105 mask-radial-at-center mask-radial-from-20% mask-radial-to-52%">
video(class="w-full" src=require('Videos/video-1.webm') autoplay loop muted playsinline)
</div>
<div class="relative z-2 pt-8 px-8.5 pb-10 text-title-4 max-md:text-title-5 bg-gradient-to-r from-gold-dark/20 to-gold/20 rounded-t-[1.25rem] text-gold">MAX</div>
<div class="relative z-3 flex flex-col grow -mt-5 p-3.5 pb-8.25 backdrop-blur-[2rem] shadow-2 bg-white/7 rounded-[1.25rem] after:absolute after:inset-0 after:border after:border-line after:rounded-[1.25rem] after:pointer-events-none">
<div class="relative mb-8 p-5 rounded-[0.8125rem] bg-line backdrop-blur-[1.25rem] shadow-2 after:absolute after:inset-0 after:border after:border-line after:rounded-[0.8125rem] after:pointer-events-none">
<div class="flex items-end gap-3 mb-4">
<div class="bg-radial-white-2 bg-clip-text text-transparent text-title-1 leading-[3.1rem] max-xl:text-title-2 max-xl:leading-[2.4rem]">¥998</div>
<div class="text-title-5">/月</div>
</div>
<a class="btn btn-primary w-full" href="https://valuefrontier.cn/home/pages/account/subscription" target="_blank">选择Max版</a>
</div>
<div class="flex flex-col gap-6.5 px-3.5 max-xl:px-0 max-xl:gap-5 max-md:px-3.5">
<div class="flex items-center gap-2.5 text-description max-xl:text-description-2 max-md:text-description-mobile">
<div class="flex justify-center items-center shrink-0 size-5 bg-gold rounded-full shadow-[0.0625rem_0.0625rem_0.0625rem_0_rgba(212,175,55,0.30)_inset,_0_0_0.625rem_0_rgba(212,175,55,0.50)_inset]">
<svg class="size-5 fill-black" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20">
<path d="M13.47 6.97A.75.75 0 0 1 14.53 8.03l-5 5a.75.75 0 0 1-1.061 0l-3-3A.75.75 0 0 1 6.53 8.97L9 11.439l4.47-4.469z" />
</svg>
</div>
<div class="font-medium">包含Pro版全部功能</div>
</div>
<div class="flex items-center gap-2.5 text-description max-xl:text-description-2 max-md:text-description-mobile">
<div class="flex justify-center items-center shrink-0 size-5 bg-green rounded-full shadow-[0.0625rem_0.0625rem_0.0625rem_0_rgba(255,255,255,0.20)_inset,_0_0_0.625rem_0_rgba(255,255,255,0.50)_inset]">
<svg class="size-5 fill-black" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20">
<path d="M13.47 6.97A.75.75 0 0 1 14.53 8.03l-5 5a.75.75 0 0 1-1.061 0l-3-3A.75.75 0 0 1 6.53 8.97L9 11.439l4.47-4.469z" />
</svg>
</div>
<div>事件传导链路智能分析</div>
</div>
<div class="flex items-center gap-2.5 text-description max-xl:text-description-2 max-md:text-description-mobile">
<div class="flex justify-center items-center shrink-0 size-5 bg-green rounded-full shadow-[0.0625rem_0.0625rem_0.0625rem_0_rgba(255,255,255,0.20)_inset,_0_0_0.625rem_0_rgba(255,255,255,0.50)_inset]">
<svg class="size-5 fill-black" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20">
<path d="M13.47 6.97A.75.75 0 0 1 14.53 8.03l-5 5a.75.75 0 0 1-1.061 0l-3-3A.75.75 0 0 1 6.53 8.97L9 11.439l4.47-4.469z" />
</svg>
</div>
<div>概念演变时间轴追溯</div>
</div>
<div class="flex items-center gap-2.5 text-description max-xl:text-description-2 max-md:text-description-mobile">
<div class="flex justify-center items-center shrink-0 size-5 bg-green rounded-full shadow-[0.0625rem_0.0625rem_0.0625rem_0_rgba(255,255,255,0.20)_inset,_0_0_0.625rem_0_rgba(255,255,255,0.50)_inset]">
<svg class="size-5 fill-black" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20">
<path d="M13.47 6.97A.75.75 0 0 1 14.53 8.03l-5 5a.75.75 0 0 1-1.061 0l-3-3A.75.75 0 0 1 6.53 8.97L9 11.439l4.47-4.469z" />
</svg>
</div>
<div>个股全方位深度研究</div>
</div>
<div class="flex items-center gap-2.5 text-description max-xl:text-description-2 max-md:text-description-mobile">
<div class="flex justify-center items-center shrink-0 size-5 bg-green rounded-full shadow-[0.0625rem_0.0625rem_0.0625rem_0_rgba(255,255,255,0.20)_inset,_0_0_0.625rem_0_rgba(255,255,255,0.50)_inset]">
<svg class="size-5 fill-black" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20">
<path d="M13.47 6.97A.75.75 0 0 1 14.53 8.03l-5 5a.75.75 0 0 1-1.061 0l-3-3A.75.75 0 0 1 6.53 8.97L9 11.439l4.47-4.469z" />
</svg>
</div>
<div>价小前投研助手无限使用</div>
</div>
<div class="flex items-center gap-2.5 text-description max-xl:text-description-2 max-md:text-description-mobile">
<div class="flex justify-center items-center shrink-0 size-5 bg-green rounded-full shadow-[0.0625rem_0.0625rem_0.0625rem_0_rgba(255,255,255,0.20)_inset,_0_0_0.625rem_0_rgba(255,255,255,0.50)_inset]">
<svg class="size-5 fill-black" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20">
<path d="M13.47 6.97A.75.75 0 0 1 14.53 8.03l-5 5a.75.75 0 0 1-1.061 0l-3-3A.75.75 0 0 1 6.53 8.97L9 11.439l4.47-4.469z" />
</svg>
</div>
<div>新功能优先体验权</div>
</div>
<div class="flex items-center gap-2.5 text-description max-xl:text-description-2 max-md:text-description-mobile">
<div class="flex justify-center items-center shrink-0 size-5 bg-green rounded-full shadow-[0.0625rem_0.0625rem_0.0625rem_0_rgba(255,255,255,0.20)_inset,_0_0_0.625rem_0_rgba(255,255,255,0.50)_inset]">
<svg class="size-5 fill-black" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20">
<path d="M13.47 6.97A.75.75 0 0 1 14.53 8.03l-5 5a.75.75 0 0 1-1.061 0l-3-3A.75.75 0 0 1 6.53 8.97L9 11.439l4.47-4.469z" />
</svg>
</div>
<div>专属客服一对一服务</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
include includes/start
</div>
+footer(true)

631
new_subscription_logic.py Normal file
View File

@@ -0,0 +1,631 @@
# -*- coding: utf-8 -*-
"""
新版订阅支付系统核心逻辑
版本: v2.0.0
日期: 2025-11-19
核心改进:
1. 续费价格与新购价格完全一致
2. 不再计算剩余价值折算
3. 逻辑简化,易于维护
"""
from datetime import datetime, timedelta
from decimal import Decimal
import json
import random
# ============================================
# 辅助函数
# ============================================
def beijing_now():
"""获取北京时间"""
from datetime import timezone, timedelta
utc_now = datetime.now(timezone.utc)
beijing_time = utc_now.astimezone(timezone(timedelta(hours=8)))
return beijing_time.replace(tzinfo=None)
def generate_order_no(user_id):
"""生成订单号"""
timestamp = int(beijing_now().timestamp() * 1000000)
random_suffix = random.randint(1000, 9999)
return f"{timestamp}{user_id:04d}{random_suffix}"
def generate_subscription_id():
"""生成订阅ID"""
timestamp = int(beijing_now().timestamp() * 1000)
random_suffix = random.randint(10000, 99999)
return f"SUB_{timestamp}_{random_suffix}"
# ============================================
# 核心业务逻辑
# ============================================
def calculate_subscription_price(plan_code, billing_cycle, promo_code=None, user_id=None, db_session=None):
"""
计算订阅价格(新购和续费价格完全一致)
Args:
plan_code: 套餐代码 (pro/max)
billing_cycle: 计费周期 (monthly/quarterly/semiannual/yearly)
promo_code: 优惠码(可选)
user_id: 用户ID可选,用于优惠码验证)
db_session: 数据库会话(可选)
Returns:
dict: {
'success': True/False,
'plan_code': 'pro',
'plan_name': 'Pro 专业版',
'billing_cycle': 'yearly',
'original_price': 2699.00,
'discount_amount': 0,
'final_amount': 2699.00,
'promo_code': None,
'promo_error': None,
'error': None # 如果有错误
}
"""
from models import SubscriptionPlan, PromoCode # 需要在实际使用时导入
try:
# 1. 查询套餐
plan = SubscriptionPlan.query.filter_by(plan_code=plan_code, is_active=True).first()
if not plan:
return {
'success': False,
'error': f'套餐 {plan_code} 不存在或已下架'
}
# 2. 获取对应周期的价格
price_field_map = {
'monthly': 'price_monthly',
'quarterly': 'price_quarterly',
'semiannual': 'price_semiannual',
'yearly': 'price_yearly'
}
price_field = price_field_map.get(billing_cycle)
if not price_field:
return {
'success': False,
'error': f'不支持的计费周期: {billing_cycle}'
}
original_price = getattr(plan, price_field, None)
if original_price is None or original_price <= 0:
return {
'success': False,
'error': f'{billing_cycle} 周期价格未配置'
}
original_price = float(original_price)
# 3. 构建基础结果
result = {
'success': True,
'plan_code': plan_code,
'plan_name': plan.plan_name,
'billing_cycle': billing_cycle,
'original_price': original_price,
'discount_amount': 0.0,
'final_amount': original_price,
'promo_code': None,
'promo_error': None,
'error': None
}
# 4. 应用优惠码(如果有)
if promo_code and promo_code.strip():
promo_code = promo_code.strip().upper()
# 验证优惠码
promo, error = validate_promo_code(
promo_code,
plan_code,
billing_cycle,
original_price,
user_id,
db_session
)
if promo:
# 计算折扣
discount = calculate_discount(promo, original_price)
result['discount_amount'] = float(discount)
result['final_amount'] = float(original_price - discount)
result['promo_code'] = promo.code
elif error:
result['promo_error'] = error
return result
except Exception as e:
return {
'success': False,
'error': f'价格计算失败: {str(e)}'
}
def get_current_subscription(user_id, db_session=None):
"""
获取用户当前生效的订阅
Args:
user_id: 用户ID
db_session: 数据库会话(可选)
Returns:
UserSubscription 对象 或 None
"""
from models import UserSubscription
try:
subscription = UserSubscription.query.filter_by(
user_id=user_id,
is_current=True
).first()
# 检查是否过期
if subscription and subscription.end_date < beijing_now():
subscription.status = 'expired'
subscription.is_current = False
if db_session:
db_session.commit()
return None
return subscription
except Exception as e:
print(f"获取当前订阅失败: {e}")
return None
def determine_subscription_type(user_id, plan_code, billing_cycle):
"""
判断订阅类型(新购还是续费)
Args:
user_id: 用户ID
plan_code: 目标套餐代码
billing_cycle: 目标计费周期
Returns:
str: 'new''renew'
"""
current_sub = get_current_subscription(user_id)
# 如果没有订阅或订阅是免费版,则为新购
if not current_sub or current_sub.plan_code == 'free':
return 'new'
# 如果是付费订阅,则为续费
if current_sub.plan_code in ['pro', 'max']:
return 'renew'
return 'new'
def create_subscription_order(user_id, plan_code, billing_cycle, promo_code=None, db_session=None):
"""
创建订阅支付订单
Args:
user_id: 用户ID
plan_code: 套餐代码
billing_cycle: 计费周期
promo_code: 优惠码(可选)
db_session: 数据库会话
Returns:
dict: {
'success': True/False,
'order': PaymentOrder 对象,
'error': None
}
"""
from models import PaymentOrder
try:
# 1. 计算价格
price_result = calculate_subscription_price(
plan_code,
billing_cycle,
promo_code,
user_id,
db_session
)
if not price_result.get('success'):
return {
'success': False,
'error': price_result.get('error', '价格计算失败')
}
# 2. 判断订阅类型
subscription_type = determine_subscription_type(user_id, plan_code, billing_cycle)
# 3. 创建支付订单
order = PaymentOrder(
order_no=generate_order_no(user_id),
user_id=user_id,
plan_code=plan_code,
billing_cycle=billing_cycle,
subscription_type=subscription_type,
original_price=Decimal(str(price_result['original_price'])),
discount_amount=Decimal(str(price_result['discount_amount'])),
final_amount=Decimal(str(price_result['final_amount'])),
promo_code=promo_code,
status='pending',
expired_at=beijing_now() + timedelta(minutes=30)
)
if db_session:
db_session.add(order)
db_session.commit()
return {
'success': True,
'order': order,
'subscription_type': subscription_type,
'error': None
}
except Exception as e:
if db_session:
db_session.rollback()
return {
'success': False,
'error': f'创建订单失败: {str(e)}'
}
def activate_subscription_after_payment(order_id, db_session=None):
"""
支付成功后激活订阅
Args:
order_id: 订单ID
db_session: 数据库会话
Returns:
dict: {
'success': True/False,
'subscription': UserSubscription 对象,
'error': None
}
"""
from models import PaymentOrder, UserSubscription, PromoCodeUsage
try:
# 1. 查询订单
order = PaymentOrder.query.get(order_id)
if not order:
return {'success': False, 'error': '订单不存在'}
if order.status != 'paid':
return {'success': False, 'error': '订单未支付'}
# 2. 检查是否已经激活
existing_sub = UserSubscription.query.filter_by(
payment_order_id=order.id
).first()
if existing_sub:
return {
'success': True,
'subscription': existing_sub,
'message': '订阅已激活'
}
# 3. 计算订阅周期天数
cycle_days_map = {
'monthly': 30,
'quarterly': 90,
'semiannual': 180,
'yearly': 365
}
days = cycle_days_map.get(order.billing_cycle, 30)
# 4. 获取当前订阅
current_sub = get_current_subscription(order.user_id, db_session)
# 5. 计算开始和结束时间
now = beijing_now()
if current_sub and current_sub.end_date > now:
# 续费:从当前订阅结束时间开始
start_date = current_sub.end_date
else:
# 新购:从当前时间开始
start_date = now
end_date = start_date + timedelta(days=days)
# 6. 创建新订阅记录
new_subscription = UserSubscription(
user_id=order.user_id,
subscription_id=generate_subscription_id(),
plan_code=order.plan_code,
billing_cycle=order.billing_cycle,
start_date=start_date,
end_date=end_date,
status='active',
is_current=True,
payment_order_id=order.id,
paid_amount=order.final_amount,
original_price=order.original_price,
discount_amount=order.discount_amount,
subscription_type=order.subscription_type,
previous_subscription_id=current_sub.subscription_id if current_sub else None,
auto_renew=False
)
# 7. 将旧订阅标记为非当前
if current_sub:
current_sub.is_current = False
if db_session:
db_session.add(new_subscription)
# 8. 记录优惠码使用
if order.promo_code_id:
usage = PromoCodeUsage(
promo_code_id=order.promo_code_id,
user_id=order.user_id,
order_id=order.id,
discount_amount=order.discount_amount
)
db_session.add(usage)
# 更新优惠码使用次数
from models import PromoCode
promo = PromoCode.query.get(order.promo_code_id)
if promo:
promo.current_uses += 1
db_session.commit()
return {
'success': True,
'subscription': new_subscription,
'error': None
}
except Exception as e:
if db_session:
db_session.rollback()
return {
'success': False,
'error': f'激活订阅失败: {str(e)}'
}
def get_subscription_button_text(user_id, plan_code, billing_cycle):
"""
获取订阅按钮文字
Args:
user_id: 用户ID
plan_code: 套餐代码 (pro/max)
billing_cycle: 计费周期
Returns:
str: 按钮文字
"""
from models import SubscriptionPlan
# 获取套餐显示名称
plan = SubscriptionPlan.query.filter_by(plan_code=plan_code).first()
plan_name = plan.plan_name if plan else plan_code.upper()
# 获取周期显示名称
cycle_names = {
'monthly': '月付',
'quarterly': '季付',
'semiannual': '半年付',
'yearly': '年付'
}
cycle_name = cycle_names.get(billing_cycle, billing_cycle)
# 获取当前订阅
current_sub = get_current_subscription(user_id)
# 1. 如果没有订阅或订阅已过期
if not current_sub or current_sub.plan_code == 'free' or current_sub.status != 'active':
return f"选择 {plan_name}"
# 2. 如果是当前套餐且周期相同
if current_sub.plan_code == plan_code and current_sub.billing_cycle == billing_cycle:
return f"续费 {plan_name}"
# 3. 如果是当前套餐但周期不同
if current_sub.plan_code == plan_code:
return f"切换至{cycle_name}"
# 4. 如果是不同套餐
return f"选择 {plan_name}"
# ============================================
# 优惠码相关函数
# ============================================
def validate_promo_code(code, plan_code, billing_cycle, amount, user_id=None, db_session=None):
"""
验证优惠码
Args:
code: 优惠码
plan_code: 套餐代码
billing_cycle: 计费周期
amount: 订单金额
user_id: 用户ID可选
db_session: 数据库会话(可选)
Returns:
tuple: (PromoCode对象 或 None, 错误信息 或 None)
"""
from models import PromoCode, PromoCodeUsage
try:
# 查询优惠码
promo = PromoCode.query.filter_by(code=code.upper(), is_active=True).first()
if not promo:
return None, "优惠码不存在或已失效"
# 检查有效期
now = beijing_now()
if promo.valid_from and now < promo.valid_from:
return None, "优惠码尚未生效"
if promo.valid_until and now > promo.valid_until:
return None, "优惠码已过期"
# 检查总使用次数
if promo.max_total_uses and promo.current_uses >= promo.max_total_uses:
return None, "优惠码使用次数已达上限"
# 检查每用户使用次数
if user_id and promo.max_uses_per_user:
user_usage_count = PromoCodeUsage.query.filter_by(
promo_code_id=promo.id,
user_id=user_id
).count()
if user_usage_count >= promo.max_uses_per_user:
return None, f"您已使用过此优惠码(限用{promo.max_uses_per_user}次)"
# 检查适用套餐
if promo.applicable_plans:
try:
applicable = json.loads(promo.applicable_plans)
if isinstance(applicable, list) and plan_code not in applicable:
return None, "该优惠码不适用于此套餐"
except:
pass
# 检查适用周期
if promo.applicable_cycles:
try:
applicable = json.loads(promo.applicable_cycles)
if isinstance(applicable, list) and billing_cycle not in applicable:
return None, "该优惠码不适用于此计费周期"
except:
pass
# 检查最低消费
if promo.min_amount and amount < float(promo.min_amount):
return None, f"需满 ¥{float(promo.min_amount):.2f} 才可使用此优惠码"
return promo, None
except Exception as e:
return None, f"验证优惠码时出错: {str(e)}"
def calculate_discount(promo_code, amount):
"""
计算优惠金额
Args:
promo_code: PromoCode 对象
amount: 订单金额
Returns:
Decimal: 优惠金额
"""
try:
if promo_code.discount_type == 'percentage':
# 百分比折扣
discount = Decimal(str(amount)) * Decimal(str(promo_code.discount_value)) / Decimal('100')
elif promo_code.discount_type == 'fixed_amount':
# 固定金额折扣
discount = Decimal(str(promo_code.discount_value))
else:
discount = Decimal('0')
# 确保折扣不超过总金额
discount = min(discount, Decimal(str(amount)))
return discount
except Exception as e:
print(f"计算折扣失败: {e}")
return Decimal('0')
# ============================================
# 辅助查询函数
# ============================================
def get_user_subscription_history(user_id, limit=10):
"""
获取用户订阅历史
Args:
user_id: 用户ID
limit: 返回记录数量限制
Returns:
list: UserSubscription 对象列表
"""
from models import UserSubscription
try:
subscriptions = UserSubscription.query.filter_by(
user_id=user_id
).order_by(
UserSubscription.created_at.desc()
).limit(limit).all()
return subscriptions
except Exception as e:
print(f"获取订阅历史失败: {e}")
return []
def check_subscription_status(user_id):
"""
检查用户订阅状态
Args:
user_id: 用户ID
Returns:
dict: {
'has_subscription': True/False,
'plan_code': 'pro''max''free',
'status': 'active''expired',
'end_date': datetime 或 None,
'days_left': int
}
"""
current_sub = get_current_subscription(user_id)
if not current_sub or current_sub.plan_code == 'free':
return {
'has_subscription': False,
'plan_code': 'free',
'status': 'active',
'end_date': None,
'days_left': 999
}
now = beijing_now()
days_left = (current_sub.end_date - now).days if current_sub.end_date > now else 0
return {
'has_subscription': True,
'plan_code': current_sub.plan_code,
'status': current_sub.status,
'end_date': current_sub.end_date,
'days_left': days_left
}

669
new_subscription_routes.py Normal file
View File

@@ -0,0 +1,669 @@
# -*- coding: utf-8 -*-
"""
新版订阅支付系统 API 路由
版本: v2.0.0
日期: 2025-11-19
使用方法:
将这些路由添加到你的 Flask app.py 中
"""
from flask import jsonify, request, session
from new_subscription_logic import (
calculate_subscription_price,
create_subscription_order,
activate_subscription_after_payment,
get_subscription_button_text,
get_current_subscription,
check_subscription_status,
get_user_subscription_history
)
# ============================================
# API 路由定义
# ============================================
@app.route('/api/v2/subscription/plans', methods=['GET'])
def get_subscription_plans_v2():
"""
获取订阅套餐列表(新版)
Response:
{
"success": true,
"data": [
{
"plan_code": "pro",
"plan_name": "Pro 专业版",
"description": "为专业投资者打造",
"prices": {
"monthly": 299.00,
"quarterly": 799.00,
"semiannual": 1499.00,
"yearly": 2699.00
},
"features": [...],
"is_active": true
},
...
]
}
"""
try:
from models import SubscriptionPlan
plans = SubscriptionPlan.query.filter_by(is_active=True).order_by(
SubscriptionPlan.display_order
).all()
data = []
for plan in plans:
data.append({
'plan_code': plan.plan_code,
'plan_name': plan.plan_name,
'description': plan.description,
'prices': {
'monthly': float(plan.price_monthly),
'quarterly': float(plan.price_quarterly),
'semiannual': float(plan.price_semiannual),
'yearly': float(plan.price_yearly)
},
'features': json.loads(plan.features) if plan.features else [],
'is_active': plan.is_active,
'display_order': plan.display_order
})
return jsonify({
'success': True,
'data': data
})
except Exception as e:
return jsonify({
'success': False,
'error': f'获取套餐列表失败: {str(e)}'
}), 500
@app.route('/api/v2/subscription/calculate-price', methods=['POST'])
def calculate_price_v2():
"""
计算订阅价格(新版 - 新购和续费价格一致)
Request Body:
{
"plan_code": "pro",
"billing_cycle": "yearly",
"promo_code": "WELCOME2025" // 可选
}
Response:
{
"success": true,
"data": {
"plan_code": "pro",
"plan_name": "Pro 专业版",
"billing_cycle": "yearly",
"original_price": 2699.00,
"discount_amount": 539.80,
"final_amount": 2159.20,
"promo_code": "WELCOME2025",
"promo_error": null
}
}
"""
try:
if 'user_id' not in session:
return jsonify({'success': False, 'error': '未登录'}), 401
data = request.get_json()
plan_code = data.get('plan_code')
billing_cycle = data.get('billing_cycle')
promo_code = data.get('promo_code')
if not plan_code or not billing_cycle:
return jsonify({
'success': False,
'error': '参数不完整'
}), 400
# 计算价格
result = calculate_subscription_price(
plan_code=plan_code,
billing_cycle=billing_cycle,
promo_code=promo_code,
user_id=session['user_id'],
db_session=db.session
)
if not result.get('success'):
return jsonify(result), 400
return jsonify({
'success': True,
'data': result
})
except Exception as e:
return jsonify({
'success': False,
'error': f'计算价格失败: {str(e)}'
}), 500
@app.route('/api/v2/payment/create-order', methods=['POST'])
def create_order_v2():
"""
创建支付订单(新版)
Request Body:
{
"plan_code": "pro",
"billing_cycle": "yearly",
"promo_code": "WELCOME2025" // 可选
}
Response:
{
"success": true,
"data": {
"order_no": "1732012345678901231234",
"plan_code": "pro",
"billing_cycle": "yearly",
"subscription_type": "renew", // 或 "new"
"original_price": 2699.00,
"discount_amount": 539.80,
"final_amount": 2159.20,
"qr_code_url": "https://...",
"status": "pending",
"expired_at": "2025-11-19T15:30:00",
...
}
}
"""
try:
if 'user_id' not in session:
return jsonify({'success': False, 'error': '未登录'}), 401
data = request.get_json()
plan_code = data.get('plan_code')
billing_cycle = data.get('billing_cycle')
promo_code = data.get('promo_code')
if not plan_code or not billing_cycle:
return jsonify({
'success': False,
'error': '参数不完整'
}), 400
# 创建订单
order_result = create_subscription_order(
user_id=session['user_id'],
plan_code=plan_code,
billing_cycle=billing_cycle,
promo_code=promo_code,
db_session=db.session
)
if not order_result.get('success'):
return jsonify({
'success': False,
'error': order_result.get('error')
}), 400
order = order_result['order']
# 生成微信支付二维码
try:
from wechat_pay import create_wechat_pay_instance, check_wechat_pay_ready
is_ready, ready_msg = check_wechat_pay_ready()
if not is_ready:
# 使用模拟二维码
order.qr_code_url = f"https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=wxpay://order/{order.order_no}"
order.remark = f"演示模式 - {ready_msg}"
else:
wechat_pay = create_wechat_pay_instance()
# 创建微信支付订单
plan_display = f"{plan_code.upper()}-{billing_cycle}"
wechat_result = wechat_pay.create_native_order(
order_no=order.order_no,
total_fee=float(order.final_amount),
body=f"VFr-{plan_display}",
product_id=f"{plan_code}_{billing_cycle}"
)
if wechat_result['success']:
wechat_code_url = wechat_result['code_url']
import urllib.parse
encoded_url = urllib.parse.quote(wechat_code_url, safe='')
qr_image_url = f"https://api.qrserver.com/v1/create-qr-code/?size=200x200&data={encoded_url}"
order.qr_code_url = qr_image_url
order.prepay_id = wechat_result.get('prepay_id')
order.remark = f"微信支付 - {wechat_code_url}"
else:
order.qr_code_url = f"https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=wxpay://order/{order.order_no}"
order.remark = f"微信支付失败: {wechat_result.get('error')}"
except Exception as e:
order.qr_code_url = f"https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=wxpay://order/{order.order_no}"
order.remark = f"支付异常: {str(e)}"
db.session.commit()
return jsonify({
'success': True,
'data': {
'id': order.id,
'order_no': order.order_no,
'plan_code': order.plan_code,
'billing_cycle': order.billing_cycle,
'subscription_type': order.subscription_type,
'original_price': float(order.original_price),
'discount_amount': float(order.discount_amount),
'final_amount': float(order.final_amount),
'promo_code': order.promo_code,
'qr_code_url': order.qr_code_url,
'status': order.status,
'expired_at': order.expired_at.isoformat() if order.expired_at else None,
'created_at': order.created_at.isoformat() if order.created_at else None
},
'message': '订单创建成功'
})
except Exception as e:
db.session.rollback()
return jsonify({
'success': False,
'error': f'创建订单失败: {str(e)}'
}), 500
@app.route('/api/v2/payment/order/<int:order_id>/status', methods=['GET'])
def check_order_status_v2(order_id):
"""
查询订单支付状态(新版)
Response:
{
"success": true,
"payment_success": true, // 是否支付成功
"data": {
"order_no": "...",
"status": "paid",
...
},
"message": "支付成功!订阅已激活"
}
"""
try:
if 'user_id' not in session:
return jsonify({'success': False, 'error': '未登录'}), 401
from models import PaymentOrder
order = PaymentOrder.query.filter_by(
id=order_id,
user_id=session['user_id']
).first()
if not order:
return jsonify({'success': False, 'error': '订单不存在'}), 404
# 如果订单已经是已支付状态
if order.status == 'paid':
return jsonify({
'success': True,
'payment_success': True,
'data': {
'order_no': order.order_no,
'status': order.status,
'final_amount': float(order.final_amount)
},
'message': '订单已支付'
})
# 如果订单过期
if order.is_expired():
order.status = 'expired'
db.session.commit()
return jsonify({
'success': True,
'payment_success': False,
'data': {'status': 'expired'},
'message': '订单已过期'
})
# 调用微信支付API查询状态
try:
from wechat_pay import create_wechat_pay_instance
wechat_pay = create_wechat_pay_instance()
query_result = wechat_pay.query_order(order_no=order.order_no)
if query_result['success']:
trade_state = query_result.get('trade_state')
transaction_id = query_result.get('transaction_id')
if trade_state == 'SUCCESS':
# 支付成功
order.mark_as_paid(transaction_id)
db.session.commit()
# 激活订阅
activate_result = activate_subscription_after_payment(
order.id,
db_session=db.session
)
if activate_result.get('success'):
return jsonify({
'success': True,
'payment_success': True,
'data': {
'order_no': order.order_no,
'status': 'paid'
},
'message': '支付成功!订阅已激活'
})
else:
return jsonify({
'success': True,
'payment_success': True,
'data': {'status': 'paid'},
'message': '支付成功,但激活失败,请联系客服'
})
elif trade_state in ['NOTPAY', 'USERPAYING']:
return jsonify({
'success': True,
'payment_success': False,
'data': {'status': 'pending'},
'message': '等待支付...'
})
else:
order.status = 'cancelled'
db.session.commit()
return jsonify({
'success': True,
'payment_success': False,
'data': {'status': 'cancelled'},
'message': '支付已取消'
})
except Exception as e:
# 查询失败,返回当前状态
pass
return jsonify({
'success': True,
'payment_success': False,
'data': {'status': order.status},
'message': '无法查询支付状态,请稍后重试'
})
except Exception as e:
return jsonify({
'success': False,
'error': f'查询失败: {str(e)}'
}), 500
@app.route('/api/v2/payment/order/<int:order_id>/force-update', methods=['POST'])
def force_update_status_v2(order_id):
"""
强制更新订单支付状态(新版)
用于支付完成但页面未更新的情况
"""
try:
if 'user_id' not in session:
return jsonify({'success': False, 'error': '未登录'}), 401
from models import PaymentOrder
order = PaymentOrder.query.filter_by(
id=order_id,
user_id=session['user_id']
).first()
if not order:
return jsonify({'success': False, 'error': '订单不存在'}), 404
# 检查微信支付状态
try:
from wechat_pay import create_wechat_pay_instance
wechat_pay = create_wechat_pay_instance()
query_result = wechat_pay.query_order(order_no=order.order_no)
if query_result['success'] and query_result.get('trade_state') == 'SUCCESS':
transaction_id = query_result.get('transaction_id')
# 标记订单为已支付
order.mark_as_paid(transaction_id)
db.session.commit()
# 激活订阅
activate_result = activate_subscription_after_payment(
order.id,
db_session=db.session
)
if activate_result.get('success'):
return jsonify({
'success': True,
'payment_success': True,
'message': '状态更新成功!订阅已激活'
})
else:
return jsonify({
'success': True,
'payment_success': True,
'message': '支付成功,但激活失败,请联系客服',
'error': activate_result.get('error')
})
else:
return jsonify({
'success': True,
'payment_success': False,
'message': '微信支付状态未更新,请继续等待'
})
except Exception as e:
return jsonify({
'success': False,
'error': f'查询微信支付状态失败: {str(e)}'
}), 500
except Exception as e:
return jsonify({
'success': False,
'error': f'强制更新失败: {str(e)}'
}), 500
@app.route('/api/v2/subscription/current', methods=['GET'])
def get_current_subscription_v2():
"""
获取当前用户订阅信息(新版)
Response:
{
"success": true,
"data": {
"subscription_id": "SUB_1732012345_12345",
"plan_code": "pro",
"plan_name": "Pro 专业版",
"billing_cycle": "yearly",
"status": "active",
"start_date": "2025-11-19T00:00:00",
"end_date": "2026-11-19T00:00:00",
"days_left": 365,
"auto_renew": false
}
}
"""
try:
if 'user_id' not in session:
return jsonify({'success': False, 'error': '未登录'}), 401
from models import SubscriptionPlan
subscription = get_current_subscription(session['user_id'])
if not subscription:
return jsonify({
'success': True,
'data': {
'plan_code': 'free',
'plan_name': '免费版',
'status': 'active'
}
})
# 获取套餐名称
plan = SubscriptionPlan.query.filter_by(plan_code=subscription.plan_code).first()
plan_name = plan.plan_name if plan else subscription.plan_code.upper()
# 计算剩余天数
from datetime import datetime
now = datetime.now()
days_left = (subscription.end_date - now).days if subscription.end_date > now else 0
return jsonify({
'success': True,
'data': {
'subscription_id': subscription.subscription_id,
'plan_code': subscription.plan_code,
'plan_name': plan_name,
'billing_cycle': subscription.billing_cycle,
'status': subscription.status,
'start_date': subscription.start_date.isoformat() if subscription.start_date else None,
'end_date': subscription.end_date.isoformat() if subscription.end_date else None,
'days_left': days_left,
'auto_renew': subscription.auto_renew
}
})
except Exception as e:
return jsonify({
'success': False,
'error': f'获取订阅信息失败: {str(e)}'
}), 500
@app.route('/api/v2/subscription/history', methods=['GET'])
def get_subscription_history_v2():
"""
获取用户订阅历史(新版)
Query Params:
limit: 返回记录数量默认10
Response:
{
"success": true,
"data": [
{
"subscription_id": "SUB_...",
"plan_code": "pro",
"billing_cycle": "yearly",
"start_date": "...",
"end_date": "...",
"paid_amount": 2699.00,
"status": "expired"
},
...
]
}
"""
try:
if 'user_id' not in session:
return jsonify({'success': False, 'error': '未登录'}), 401
limit = request.args.get('limit', 10, type=int)
subscriptions = get_user_subscription_history(session['user_id'], limit)
data = []
for sub in subscriptions:
data.append({
'subscription_id': sub.subscription_id,
'plan_code': sub.plan_code,
'billing_cycle': sub.billing_cycle,
'start_date': sub.start_date.isoformat() if sub.start_date else None,
'end_date': sub.end_date.isoformat() if sub.end_date else None,
'paid_amount': float(sub.paid_amount),
'original_price': float(sub.original_price),
'discount_amount': float(sub.discount_amount),
'status': sub.status,
'created_at': sub.created_at.isoformat() if sub.created_at else None
})
return jsonify({
'success': True,
'data': data
})
except Exception as e:
return jsonify({
'success': False,
'error': f'获取订阅历史失败: {str(e)}'
}), 500
@app.route('/api/v2/subscription/button-text', methods=['POST'])
def get_button_text_v2():
"""
获取订阅按钮文字(新版)
Request Body:
{
"plan_code": "pro",
"billing_cycle": "yearly"
}
Response:
{
"success": true,
"button_text": "续费 Pro 专业版"
}
"""
try:
if 'user_id' not in session:
return jsonify({
'success': True,
'button_text': '选择套餐'
})
data = request.get_json()
plan_code = data.get('plan_code')
billing_cycle = data.get('billing_cycle')
if not plan_code or not billing_cycle:
return jsonify({
'success': False,
'error': '参数不完整'
}), 400
button_text = get_subscription_button_text(
session['user_id'],
plan_code,
billing_cycle
)
return jsonify({
'success': True,
'button_text': button_text
})
except Exception as e:
return jsonify({
'success': False,
'error': f'获取按钮文字失败: {str(e)}'
}), 500

View File

@@ -0,0 +1,53 @@
import React from "react";
import Link, { LinkProps } from "next/link";
type CommonProps = {
className?: string;
children?: React.ReactNode;
isPrimary?: boolean;
isSecondary?: boolean;
};
type ButtonAsButton = {
as?: "button";
} & React.ButtonHTMLAttributes<HTMLButtonElement>;
type ButtonAsAnchor = {
as: "a";
} & React.AnchorHTMLAttributes<HTMLAnchorElement>;
type ButtonAsLink = {
as: "link";
} & LinkProps;
type ButtonProps = CommonProps &
(ButtonAsButton | ButtonAsAnchor | ButtonAsLink);
const Button: React.FC<ButtonProps> = ({
className,
children,
isPrimary,
isSecondary,
as = "button",
...props
}) => {
const isLink = as === "link";
const Component: React.ElementType = isLink ? Link : as;
return (
<Component
className={`relative inline-flex justify-center items-center h-10 px-3.5 rounded-lg text-title-5 cursor-pointer transition-all ${
isPrimary ? "bg-white text-black hover:bg-white/90" : ""
} ${
isSecondary
? "shadow-[0.0625rem_0.0625rem_0.0625rem_0_rgba(255,255,255,0.10)_inset] text-white after:absolute after:inset-0 after:border after:border-line after:rounded-lg after:pointer-events-none after:transition-colors hover:after:border-white"
: ""
} ${className || ""}`}
{...(isLink ? (props as LinkProps) : props)}
>
{children}
</Component>
);
};
export default Button;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,221 @@
export const subscriptionConfig = {
plans: [
{
name: 'free',
displayName: '基础版',
description: '免费体验核心功能,7项实用工具',
icon: 'star',
price: 0,
badge: '免费',
badgeColor: 'gray',
cardBorder: 'gray',
features: [
{ name: '新闻信息流', enabled: true },
{ name: '历史事件对比', enabled: true, limit: 'TOP3' },
{ name: '事件传导链分析(AI)', enabled: true, limit: '有限体验' },
{ name: 'AI复盘功能', enabled: true },
{ name: '企业概览', enabled: true, limit: '限制预览' },
{ name: '个股深度分析(AI)', enabled: true, limit: '10家/月' },
{ name: '概念中心(548大概念)', enabled: true, limit: 'TOP5' },
{ name: '涨停板块数据分析', enabled: true },
{ name: '个股涨停分析', enabled: true },
{ name: '事件-相关标的分析', enabled: false },
{ name: '相关概念展示', enabled: false },
{ name: '高效数据筛选工具', enabled: false },
],
},
{
name: 'pro',
displayName: 'Pro 专业版',
description: '为专业投资者打造,解锁高级分析功能',
icon: 'gem',
badge: '推荐',
badgeColor: 'gold',
cardBorder: 'gold',
highlight: false,
pricingOptions: [
{
cycleKey: 'monthly',
label: '月付',
months: 1,
price: 299,
originalPrice: null,
discountPercent: 0,
},
{
cycleKey: '3months',
label: '季付',
months: 3,
price: 799,
originalPrice: 897,
discountPercent: 11,
},
{
cycleKey: '6months',
label: '半年付',
months: 6,
price: 1499,
originalPrice: 1794,
discountPercent: 16,
},
{
cycleKey: 'yearly',
label: '年付',
months: 12,
price: 2699,
originalPrice: 3588,
discountPercent: 25,
},
],
features: [
{ name: '新闻信息流', enabled: true },
{ name: '历史事件对比', enabled: true },
{ name: '事件传导链分析(AI)', enabled: true },
{ name: '事件-相关标的分析', enabled: true },
{ name: '相关概念展示', enabled: true },
{ name: 'AI复盘功能', enabled: true },
{ name: '企业概览', enabled: true },
{ name: '个股深度分析(AI)', enabled: true, limit: '50家/月' },
{ name: '高效数据筛选工具', enabled: true },
{ name: '概念中心(548大概念)', enabled: true },
{ name: '历史时间轴查询', enabled: true, limit: '100天' },
{ name: '涨停板块数据分析', enabled: true },
{ name: '个股涨停分析', enabled: true },
{ name: '板块深度分析(AI)', enabled: false },
{ name: '概念高频更新', enabled: false },
],
},
{
name: 'max',
displayName: 'Max 旗舰版',
description: '旗舰级体验,无限制使用所有功能',
icon: 'crown',
badge: '最受欢迎',
badgeColor: 'gold',
cardBorder: 'gold',
highlight: true,
pricingOptions: [
{
cycleKey: 'monthly',
label: '月付',
months: 1,
price: 599,
originalPrice: null,
discountPercent: 0,
},
{
cycleKey: '3months',
label: '季付',
months: 3,
price: 1599,
originalPrice: 1797,
discountPercent: 11,
},
{
cycleKey: '6months',
label: '半年付',
months: 6,
price: 2999,
originalPrice: 3594,
discountPercent: 17,
},
{
cycleKey: 'yearly',
label: '年付',
months: 12,
price: 5399,
originalPrice: 7188,
discountPercent: 25,
},
],
features: [
{ name: '新闻信息流', enabled: true },
{ name: '历史事件对比', enabled: true },
{ name: '事件传导链分析(AI)', enabled: true },
{ name: '事件-相关标的分析', enabled: true },
{ name: '相关概念展示', enabled: true },
{ name: '板块深度分析(AI)', enabled: true },
{ name: 'AI复盘功能', enabled: true },
{ name: '企业概览', enabled: true },
{ name: '个股深度分析(AI)', enabled: true, limit: '无限制' },
{ name: '高效数据筛选工具', enabled: true },
{ name: '概念中心(548大概念)', enabled: true },
{ name: '历史时间轴查询', enabled: true, limit: '无限制' },
{ name: '概念高频更新', enabled: true },
{ name: '涨停板块数据分析', enabled: true },
{ name: '个股涨停分析', enabled: true },
],
},
],
faqs: [
{
question: '如何取消订阅?',
answer: '您可以随时在账户设置中取消订阅。取消后,您的订阅将在当前计费周期结束时到期,期间您仍可继续使用付费功能。取消后不会立即扣款,也不会自动续费。',
},
{
question: '支持哪些支付方式?',
answer: '我们目前支持微信支付。扫描支付二维码后,系统会自动检测支付状态并激活您的订阅。支付过程安全可靠,所有交易都经过加密处理。',
},
{
question: '升级或切换套餐时,原套餐的费用怎么办?',
answer: '当您升级套餐或切换计费周期时,系统会自动计算您当前订阅的剩余价值并用于抵扣新套餐的费用。\n\n计算方式\n• 剩余价值 = 原套餐价格 × (剩余天数 / 总天数)\n• 实付金额 = 新套餐价格 - 剩余价值 - 优惠码折扣\n\n例如您购买了年付Pro版¥2699,使用了180天后升级到Max版¥5399/年),剩余价值约¥1350将自动抵扣,实付约¥4049。',
},
{
question: '可以在不同计费周期之间切换吗?',
answer: '可以。您可以随时更改计费周期。如果从短期切换到长期,系统会计算剩余价值并应用到新的订阅中。长期套餐(季付、半年付、年付)可享受更大的折扣优惠。',
},
{
question: '是否支持退款?',
answer: '为了保障服务质量和维护公平的商业环境,我们不支持退款。\n\n建议您在订阅前\n• 充分了解各套餐的功能差异\n• 使用免费版体验基础功能\n• 根据实际需求选择合适的计费周期\n• 如有疑问可联系客服咨询\n\n提示选择长期套餐如半年付、年付可享受更大折扣,性价比更高。',
},
{
question: 'Pro版和Max版有什么区别',
answer: 'Pro版适合个人专业用户,提供高级图表、历史数据分析等功能,有一定的使用限制。Max版则是为重度用户设计,提供无限制的数据查询、板块深度分析、概念高频更新等独家功能,并享有优先技术支持。',
},
],
};
// 主题颜色配置 - 黑金配色
export const themeColors = {
// 背景渐变
bgGradient: 'linear-gradient(135deg, #0a0a0a 0%, #1a1a1a 100%)',
bgRadialGold: 'radial-gradient(circle at center, rgba(212, 175, 55, 0.1) 0%, transparent 70%)',
// 主色调
primary: {
gold: '#D4AF37', // 金色
goldLight: '#F4E3A7', // 浅金色
goldDark: '#B8941F', // 深金色
},
// 背景色
bg: {
primary: '#0a0a0a', // 主背景(纯黑)
secondary: '#1a1a1a', // 次级背景(深黑)
card: '#1e1e1e', // 卡片背景
cardHover: '#252525', // 卡片悬停
},
// 文字颜色
text: {
primary: '#ffffff', // 主文字(纯白)
secondary: '#b8b8b8', // 次级文字(灰白)
muted: '#808080', // 弱化文字(灰)
gold: '#D4AF37', // 金色文字
},
// 边框颜色
border: {
default: 'rgba(255, 255, 255, 0.1)',
gold: 'rgba(212, 175, 55, 0.3)',
goldGlow: 'rgba(212, 175, 55, 0.5)',
},
// 状态颜色
status: {
active: '#00ff88', // 激活(绿色)
inactive: '#ff4444', // 未激活(红色)
warning: '#ff9900', // 警告(橙色)
},
};

View File

@@ -0,0 +1,35 @@
export const pricing = [
{
title: "STARTER",
price: 99,
features: [
"1 Active Bot",
"1,000 Conversations per month",
"Web & WhatsApp Integration",
"Basic Dashboard & Chat Reports",
"Email Support",
],
},
{
title: "PRO",
price: 149,
features: [
"Up to 5 Active Bots",
"10,000 Conversations per month",
"Multi-Channel (Web, WhatsApp, IG, Telegram)",
"Custom Workflows & Automation",
"Real-Time Reports & Zapier Integration",
],
},
{
title: "ENTERPRISE",
price: 199,
features: [
"Unlimited Bots & Chats",
"Role-Based Access & Team Management",
"Integration to CRM & Custom APIs",
"Advanced AI Training (LLM/NLP)",
"Dedicated Onboarding Team",
],
},
];

129
src/views/Pricing/index.tsx Normal file
View File

@@ -0,0 +1,129 @@
import { motion } from "framer-motion";
import Button from "@/components/Button2";
import { pricing } from "./content";
const Pricing = () => (
<div
id="pricing"
className="pt-34.5 pb-25 max-2xl:pt-25 max-lg:py-20 max-md:py-15"
>
<div className="center">
<motion.div
className="max-w-175 mx-auto mb-17.5 text-center max-xl:mb-14 max-md:mb-8"
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
transition={{ delay: 0.2, duration: 0.7 }}
viewport={{ amount: 0.7 }}
>
<div className="label mb-3 max-md:mb-1.5">Pricing</div>
<div className="bg-radial-white-2 bg-clip-text text-transparent text-title-1 max-lg:text-title-2 max-md:text-title-1-mobile">
Start Automation Today
</div>
</motion.div>
<motion.div
className="flex gap-4 max-lg:-mx-10 max-lg:px-10 max-lg:overflow-x-auto max-lg:scrollbar-none max-md:-mx-5 max-md:px-5"
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
transition={{ delay: 0.2, duration: 0.7 }}
viewport={{ amount: 0.35 }}
>
{pricing.map((item, index) => (
<div
className={`relative flex flex-col flex-1 rounded-[1.25rem] overflow-hidden after:absolute after:inset-0 after:border after:border-line after:rounded-[1.25rem] after:pointer-events-none max-lg:shrink-0 max-lg:flex-auto max-lg:w-84 ${
item.title === "PRO"
? "shadow-2 before:absolute before:-top-20 before:left-1/2 before:z-1 before:-translate-x-1/2 before:w-65 before:h-57 before:bg-green/10 before:rounded-full before:blur-[3.375rem]"
: "shadow-[0.0625rem_0.0625rem_0.0625rem_0_rgba(255,255,255,0.10)_inset]"
}`}
key={index}
>
{item.title === "PRO" && (
<div className="absolute -top-36 left-13 w-105 mask-radial-at-center mask-radial-from-20% mask-radial-to-52%">
<video
className="w-full"
src="/videos/video-1.mp4"
autoPlay
loop
muted
playsInline
/>
</div>
)}
<div
className={`relative z-2 pt-8 px-8.5 pb-10 text-title-4 max-md:text-title-5 ${
item.title === "PRO"
? "bg-[#175673]/20 rounded-t-[1.25rem] text-green"
: "text-white"
}`}
>
{item.title}
</div>
<div
className={`relative z-3 flex flex-col grow -mt-5 p-3.5 pb-8.25 rounded-[1.25rem] after:absolute after:inset-0 after:border after:border-line after:rounded-[1.25rem] after:pointer-events-none ${
item.title === "PRO"
? "backdrop-blur-[2rem] shadow-2 bg-white/7"
: "backdrop-blur-[1.25rem] bg-white/1"
}`}
>
<div
className={`relative mb-8 p-5 rounded-[0.8125rem] backdrop-blur-[1.25rem] shadow-2 after:absolute after:inset-0 after:border after:border-line after:rounded-[0.8125rem] after:pointer-events-none ${
item.title === "PRO"
? "bg-line"
: "bg-white/2"
}`}
>
<div className="flex items-end gap-3 mb-4">
<div className="bg-radial-white-2 bg-clip-text text-transparent text-title-1 leading-[3.1rem] max-xl:text-title-2 max-xl:leading-[2.4rem]">
${item.price}
</div>
<div className="text-title-5">/Month</div>
</div>
<Button
className={`w-full bg-line ${
item.title !== "PRO"
? "!text-description hover:!text-white"
: ""
}`}
isPrimary={item.title === "PRO"}
isSecondary={item.title !== "PRO"}
>
{item.title === "STARTER"
? "Start with Beginner"
: item.title === "PRO"
? "Choose Pro Plan"
: "Contact for Enterprise"}
</Button>
</div>
<div className="flex flex-col gap-6.5 px-3.5 max-xl:px-0 max-xl:gap-5 max-md:px-3.5">
{item.features.map((feature, index) => (
<div
className="flex items-center gap-2.5 text-description max-xl:text-description-2 max-md:text-description-mobile"
key={index}
>
<div className="flex justify-center items-center shrink-0 size-5 bg-green rounded-full shadow-[0.0625rem_0.0625rem_0.0625rem_0_rgba(255,255,255,0.20)_inset,_0_0_0.625rem_0_rgba(255,255,255,0.50)_inset]">
<svg
className="size-5 fill-black"
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 20 20"
>
<path d="M13.47 6.97A.75.75 0 0 1 14.53 8.03l-5 5a.75.75 0 0 1-1.061 0l-3-3A.75.75 0 0 1 6.53 8.97L9 11.439l4.47-4.469z" />
</svg>
</div>
{feature}
</div>
))}
</div>
</div>
</div>
))}
</motion.div>
<div className="mt-13.5 text-center max-md:mt-8 max-md:text-title-3-mobile">
Free 7 Day Trial
</div>
</div>
</div>
);
export default Pricing;

100
update_pricing_options.sql Normal file
View File

@@ -0,0 +1,100 @@
-- ============================================
-- 更新订阅套餐价格配置
-- 用途:为 subscription_plans 表添加季付、半年付价格
-- 日期2025-11-19
-- ============================================
-- 更新 Pro 专业版的 pricing_options
UPDATE subscription_plans
SET pricing_options = JSON_ARRAY(
JSON_OBJECT(
'months', 1,
'price', 299.00,
'label', '月付',
'cycle_key', 'monthly',
'discount_percent', 0
),
JSON_OBJECT(
'months', 3,
'price', 799.00,
'label', '季付',
'cycle_key', 'quarterly',
'discount_percent', 11,
'original_price', 897.00
),
JSON_OBJECT(
'months', 6,
'price', 1499.00,
'label', '半年付',
'cycle_key', 'semiannual',
'discount_percent', 16,
'original_price', 1794.00
),
JSON_OBJECT(
'months', 12,
'price', 2699.00,
'label', '年付',
'cycle_key', 'yearly',
'discount_percent', 25,
'original_price', 3588.00
)
)
WHERE name = 'pro';
-- 更新 Max 旗舰版的 pricing_options
UPDATE subscription_plans
SET pricing_options = JSON_ARRAY(
JSON_OBJECT(
'months', 1,
'price', 599.00,
'label', '月付',
'cycle_key', 'monthly',
'discount_percent', 0
),
JSON_OBJECT(
'months', 3,
'price', 1599.00,
'label', '季付',
'cycle_key', 'quarterly',
'discount_percent', 11,
'original_price', 1797.00
),
JSON_OBJECT(
'months', 6,
'price', 2999.00,
'label', '半年付',
'cycle_key', 'semiannual',
'discount_percent', 17,
'original_price', 3594.00
),
JSON_OBJECT(
'months', 12,
'price', 5399.00,
'label', '年付',
'cycle_key', 'yearly',
'discount_percent', 25,
'original_price', 7188.00
)
)
WHERE name = 'max';
-- 验证更新结果
SELECT
name AS '套餐',
display_name AS '显示名称',
pricing_options AS '价格配置'
FROM subscription_plans
WHERE name IN ('pro', 'max');
-- 完成提示
SELECT '价格配置已更新!' AS '状态';
SELECT '新价格:' AS '';
SELECT ' Pro 月付: ¥299' AS '';
SELECT ' Pro 季付: ¥799 (省11%)' AS '';
SELECT ' Pro 半年付: ¥1499 (省16%)' AS '';
SELECT ' Pro 年付: ¥2699 (省25%)' AS '';
SELECT '' AS '';
SELECT ' Max 月付: ¥599' AS '';
SELECT ' Max 季付: ¥1599 (省11%)' AS '';
SELECT ' Max 半年付: ¥2999 (省17%)' AS '';
SELECT ' Max 年付: ¥5399 (省25%)' AS '';