2026-01-31 02:59:49 +08:00
"""
2026-04-06 16:47:36 +07:00
Community Service - indicator community service
2026-01-31 02:59:49 +08:00
2026-04-06 16:47:36 +07:00
Handles functions such as indicator markets, purchases, and comments.
2026-01-31 02:59:49 +08:00
"""
2026-04-09 14:30:51 +07:00
2026-02-27 01:57:04 +08:00
import json
2026-01-31 02:59:49 +08:00
import time
from decimal import Decimal
2026-04-09 14:30:51 +07:00
from typing import Any , Dict , Optional , Tuple
2026-01-31 02:59:49 +08:00
2026-04-09 14:30:51 +07:00
from app.services.billing_service import get_billing_service
2026-01-31 02:59:49 +08:00
from app.utils.db import get_db_connection
from app.utils.logger import get_logger
logger = get_logger ( __name__ )
class CommunityService :
2026-04-06 16:47:36 +07:00
"""Indicator Community Service Category"""
2026-04-09 14:30:51 +07:00
2026-01-31 02:59:49 +08:00
def __init__ ( self ):
self . billing = get_billing_service ()
2026-02-27 01:57:04 +08:00
# Best-effort: ensure vip_free column exists (for old databases)
try :
with get_db_connection () as db :
cur = db . cursor ()
cur . execute ( "ALTER TABLE qd_indicator_codes ADD COLUMN IF NOT EXISTS vip_free BOOLEAN DEFAULT FALSE" )
db . commit ()
cur . close ()
except Exception :
pass
2026-04-09 14:30:51 +07:00
2026-01-31 02:59:49 +08:00
# ==========================================
2026-04-06 16:47:36 +07:00
# indicator market
2026-01-31 02:59:49 +08:00
# ==========================================
2026-04-09 14:30:51 +07:00
2026-01-31 02:59:49 +08:00
def get_market_indicators (
self ,
page : int = 1 ,
page_size : int = 12 ,
keyword : str = None ,
pricing_type : str = None , # 'free' / 'paid' / None(all)
2026-04-09 14:30:51 +07:00
sort_by : str = "newest" , # 'newest' / 'hot' / 'price_asc' / 'price_desc' / 'rating'
user_id : int = None , # Current user ID, used to determine whether purchase has been made
2026-01-31 02:59:49 +08:00
) -> Dict [ str , Any ]:
2026-04-06 16:47:36 +07:00
"""Get a list of published indicators on the market"""
2026-01-31 02:59:49 +08:00
offset = ( page - 1 ) * page_size
2026-04-09 14:30:51 +07:00
2026-01-31 02:59:49 +08:00
try :
with get_db_connection () as db :
cur = db . cursor ()
2026-04-09 14:30:51 +07:00
2026-04-06 16:47:36 +07:00
# Build query conditions - only display published and approved indicators
2026-04-09 14:30:51 +07:00
where_clauses = [
"i.publish_to_community = 1" ,
"(i.review_status = 'approved' OR i.review_status IS NULL)" ,
]
2026-01-31 02:59:49 +08:00
params = []
2026-04-09 14:30:51 +07:00
2026-01-31 02:59:49 +08:00
if keyword and keyword . strip ():
where_clauses . append ( "(i.name ILIKE ? OR i.description ILIKE ?)" )
search_term = f "% { keyword . strip () } %"
params . extend ([ search_term , search_term ])
2026-04-09 14:30:51 +07:00
if pricing_type == "free" :
2026-01-31 02:59:49 +08:00
where_clauses . append ( "(i.pricing_type = 'free' OR i.price <= 0)" )
2026-04-09 14:30:51 +07:00
elif pricing_type == "paid" :
2026-01-31 02:59:49 +08:00
where_clauses . append ( "(i.pricing_type != 'free' AND i.price > 0)" )
2026-04-09 14:30:51 +07:00
2026-01-31 02:59:49 +08:00
where_sql = " AND " . join ( where_clauses )
2026-04-09 14:30:51 +07:00
2026-04-06 16:47:36 +07:00
# sort
2026-01-31 02:59:49 +08:00
order_map = {
2026-04-09 14:30:51 +07:00
"newest" : "i.created_at DESC" ,
"hot" : "i.purchase_count DESC, i.view_count DESC" ,
"price_asc" : "i.price ASC, i.created_at DESC" ,
"price_desc" : "i.price DESC, i.created_at DESC" ,
"rating" : "i.avg_rating DESC, i.rating_count DESC" ,
2026-01-31 02:59:49 +08:00
}
2026-04-09 14:30:51 +07:00
order_sql = order_map . get ( sort_by , "i.created_at DESC" )
2026-04-06 16:47:36 +07:00
# Get total
2026-01-31 02:59:49 +08:00
count_sql = f """
2026-04-09 14:30:51 +07:00
SELECT COUNT(*) as count
FROM qd_indicator_codes i
2026-01-31 02:59:49 +08:00
WHERE { where_sql }
"""
cur . execute ( count_sql , tuple ( params ))
2026-04-09 14:30:51 +07:00
total = cur . fetchone ()[ "count" ]
2026-04-06 16:47:36 +07:00
# Get the list (join the table to query author information)
2026-01-31 02:59:49 +08:00
query_sql = f """
2026-04-09 14:30:51 +07:00
SELECT
2026-02-27 01:57:04 +08:00
i.id, i.name, i.description, i.pricing_type, i.price, COALESCE(i.vip_free, FALSE) as vip_free,
2026-01-31 02:59:49 +08:00
i.preview_image, i.purchase_count, i.avg_rating, i.rating_count,
i.view_count, i.created_at, i.updated_at,
2026-04-09 14:30:51 +07:00
u.id as author_id, u.username as author_username,
2026-01-31 02:59:49 +08:00
u.nickname as author_nickname, u.avatar as author_avatar
FROM qd_indicator_codes i
LEFT JOIN qd_users u ON i.user_id = u.id
WHERE { where_sql }
ORDER BY { order_sql }
LIMIT ? OFFSET ?
"""
cur . execute ( query_sql , tuple ( params + [ page_size , offset ]))
rows = cur . fetchall () or []
2026-04-09 14:30:51 +07:00
2026-04-06 16:47:36 +07:00
# If there is a current user, query the purchased indicators
2026-01-31 02:59:49 +08:00
purchased_ids = set ()
if user_id :
2026-04-09 14:30:51 +07:00
indicator_ids = [ r [ "id" ] for r in rows ]
2026-01-31 02:59:49 +08:00
if indicator_ids :
2026-04-09 14:30:51 +07:00
placeholders = "," . join ([ "?" ] * len ( indicator_ids ))
2026-01-31 02:59:49 +08:00
cur . execute (
f "SELECT indicator_id FROM qd_indicator_purchases WHERE buyer_id = ? AND indicator_id IN ( { placeholders } )" ,
2026-04-09 14:30:51 +07:00
tuple ([ user_id ] + indicator_ids ),
2026-01-31 02:59:49 +08:00
)
2026-04-09 14:30:51 +07:00
purchased_ids = { r [ "indicator_id" ] for r in ( cur . fetchall () or [])}
2026-01-31 02:59:49 +08:00
cur . close ()
2026-04-09 14:30:51 +07:00
# Format return data
2026-01-31 02:59:49 +08:00
items = []
for row in rows :
2026-04-09 14:30:51 +07:00
items . append (
{
"id" : row [ "id" ],
"name" : row [ "name" ],
"description" : row [ "description" ][: 200 ] if row [ "description" ] else "" ,
"pricing_type" : row [ "pricing_type" ] or "free" ,
"price" : float ( row [ "price" ] or 0 ),
"vip_free" : bool ( row . get ( "vip_free" ) or False ),
"preview_image" : row [ "preview_image" ] or "" ,
"purchase_count" : row [ "purchase_count" ] or 0 ,
"avg_rating" : float ( row [ "avg_rating" ] or 0 ),
"rating_count" : row [ "rating_count" ] or 0 ,
"view_count" : row [ "view_count" ] or 0 ,
"created_at" : row [ "created_at" ] . isoformat () if row [ "created_at" ] else None ,
"author" : {
"id" : row [ "author_id" ],
"username" : row [ "author_username" ],
"nickname" : row [ "author_nickname" ] or row [ "author_username" ],
"avatar" : row [ "author_avatar" ] or "/avatar2.jpg" ,
},
"is_purchased" : row [ "id" ] in purchased_ids ,
"is_own" : row [ "author_id" ] == user_id ,
}
)
2026-01-31 02:59:49 +08:00
return {
2026-04-09 14:30:51 +07:00
"items" : items ,
"total" : total ,
"page" : page ,
"page_size" : page_size ,
"total_pages" : ( total + page_size - 1 ) // page_size if total > 0 else 0 ,
2026-01-31 02:59:49 +08:00
}
2026-04-09 14:30:51 +07:00
2026-01-31 02:59:49 +08:00
except Exception as e :
logger . error ( f "get_market_indicators failed: { e } " )
2026-04-09 14:30:51 +07:00
return { "items" : [], "total" : 0 , "page" : 1 , "page_size" : page_size , "total_pages" : 0 }
2026-01-31 02:59:49 +08:00
def get_indicator_detail ( self , indicator_id : int , user_id : int = None ) -> Optional [ Dict [ str , Any ]]:
2026-04-08 07:27:26 +07:00
"""Get indicator details."""
2026-01-31 02:59:49 +08:00
try :
with get_db_connection () as db :
cur = db . cursor ()
2026-04-09 14:30:51 +07:00
2026-04-06 16:47:36 +07:00
# Get indicator information
2026-04-09 14:30:51 +07:00
cur . execute (
"""
SELECT
2026-02-27 01:57:04 +08:00
i.id, i.name, i.description, i.pricing_type, i.price, COALESCE(i.vip_free, FALSE) as vip_free,
2026-01-31 02:59:49 +08:00
i.preview_image, i.purchase_count, i.avg_rating, i.rating_count,
i.view_count, i.publish_to_community, i.created_at, i.updated_at,
i.user_id,
2026-04-09 14:30:51 +07:00
u.id as author_id, u.username as author_username,
2026-01-31 02:59:49 +08:00
u.nickname as author_nickname, u.avatar as author_avatar
FROM qd_indicator_codes i
LEFT JOIN qd_users u ON i.user_id = u.id
WHERE i.id = ?
2026-04-09 14:30:51 +07:00
""" ,
( indicator_id ,),
)
2026-01-31 02:59:49 +08:00
row = cur . fetchone ()
2026-04-09 14:30:51 +07:00
2026-01-31 02:59:49 +08:00
if not row :
cur . close ()
return None
2026-04-09 14:30:51 +07:00
2026-04-06 16:47:36 +07:00
# Check if it has been published to the community (or its own indicator)
2026-04-09 14:30:51 +07:00
if not row [ "publish_to_community" ] and row [ "user_id" ] != user_id :
2026-01-31 02:59:49 +08:00
cur . close ()
return None
2026-04-09 14:30:51 +07:00
2026-04-06 16:47:36 +07:00
# Check if purchased
2026-01-31 02:59:49 +08:00
is_purchased = False
if user_id :
cur . execute (
"SELECT id FROM qd_indicator_purchases WHERE indicator_id = ? AND buyer_id = ?" ,
2026-04-09 14:30:51 +07:00
( indicator_id , user_id ),
2026-01-31 02:59:49 +08:00
)
is_purchased = cur . fetchone () is not None
2026-04-09 14:30:51 +07:00
2026-04-06 16:47:36 +07:00
# Increase the number of views
2026-01-31 02:59:49 +08:00
cur . execute (
"UPDATE qd_indicator_codes SET view_count = COALESCE(view_count, 0) + 1 WHERE id = ?" ,
2026-04-09 14:30:51 +07:00
( indicator_id ,),
2026-01-31 02:59:49 +08:00
)
db . commit ()
cur . close ()
2026-04-09 14:30:51 +07:00
2026-01-31 02:59:49 +08:00
return {
2026-04-09 14:30:51 +07:00
"id" : row [ "id" ],
"name" : row [ "name" ],
"description" : row [ "description" ] or "" ,
"pricing_type" : row [ "pricing_type" ] or "free" ,
"price" : float ( row [ "price" ] or 0 ),
"vip_free" : bool ( row . get ( "vip_free" ) or False ),
"preview_image" : row [ "preview_image" ] or "" ,
"purchase_count" : row [ "purchase_count" ] or 0 ,
"avg_rating" : float ( row [ "avg_rating" ] or 0 ),
"rating_count" : row [ "rating_count" ] or 0 ,
"view_count" : ( row [ "view_count" ] or 0 ) + 1 ,
"created_at" : row [ "created_at" ] . isoformat () if row [ "created_at" ] else None ,
"updated_at" : row [ "updated_at" ] . isoformat () if row [ "updated_at" ] else None ,
"author" : {
"id" : row [ "author_id" ],
"username" : row [ "author_username" ],
"nickname" : row [ "author_nickname" ] or row [ "author_username" ],
"avatar" : row [ "author_avatar" ] or "/avatar2.jpg" ,
2026-01-31 02:59:49 +08:00
},
2026-04-09 14:30:51 +07:00
"is_purchased" : is_purchased ,
"is_own" : row [ "user_id" ] == user_id ,
2026-01-31 02:59:49 +08:00
}
2026-04-09 14:30:51 +07:00
2026-01-31 02:59:49 +08:00
except Exception as e :
logger . error ( f "get_indicator_detail failed: { e } " )
return None
2026-04-09 14:30:51 +07:00
2026-01-31 02:59:49 +08:00
# ==========================================
2026-04-06 16:47:36 +07:00
# Purchase function
2026-01-31 02:59:49 +08:00
# ==========================================
2026-04-09 14:30:51 +07:00
2026-01-31 02:59:49 +08:00
def purchase_indicator ( self , buyer_id : int , indicator_id : int ) -> Tuple [ bool , str , Dict [ str , Any ]]:
"""
2026-04-08 07:27:26 +07:00
Purchase an indicator.
2026-01-31 02:59:49 +08:00
Returns:
(success, message, data)
"""
try :
with get_db_connection () as db :
cur = db . cursor ()
2026-04-09 14:30:51 +07:00
2026-04-06 16:47:36 +07:00
# 1. Get indicator information
2026-04-09 14:30:51 +07:00
cur . execute (
"""
2026-02-27 01:57:04 +08:00
SELECT id, user_id, name, code, description, pricing_type, price, COALESCE(vip_free, FALSE) as vip_free,
2026-01-31 02:59:49 +08:00
preview_image, is_encrypted
FROM qd_indicator_codes
WHERE id = ? AND publish_to_community = 1
2026-04-09 14:30:51 +07:00
""" ,
( indicator_id ,),
)
2026-01-31 02:59:49 +08:00
indicator = cur . fetchone ()
2026-04-09 14:30:51 +07:00
2026-01-31 02:59:49 +08:00
if not indicator :
cur . close ()
2026-04-09 14:30:51 +07:00
return False , "indicator_not_found" , {}
seller_id = indicator [ "user_id" ]
price = float ( indicator [ "price" ] or 0 )
pricing_type = indicator [ "pricing_type" ] or "free"
vip_free = bool ( indicator . get ( "vip_free" ) or False )
2026-02-27 01:57:04 +08:00
is_vip , _ = self . billing . get_user_vip_status ( buyer_id )
# VIP-free indicator: VIP users can get it without credits charge
effective_price = 0.0 if ( vip_free and is_vip ) else price
2026-04-09 14:30:51 +07:00
2026-04-06 16:47:36 +07:00
# 2. Check whether to buy your own indicator
2026-01-31 02:59:49 +08:00
if seller_id == buyer_id :
cur . close ()
2026-04-09 14:30:51 +07:00
return False , "cannot_buy_own" , {}
2026-04-06 16:47:36 +07:00
# 3. Check if purchased
2026-01-31 02:59:49 +08:00
cur . execute (
"SELECT id FROM qd_indicator_purchases WHERE indicator_id = ? AND buyer_id = ?" ,
2026-04-09 14:30:51 +07:00
( indicator_id , buyer_id ),
2026-01-31 02:59:49 +08:00
)
if cur . fetchone ():
cur . close ()
2026-04-09 14:30:51 +07:00
return False , "already_purchased" , {}
2026-04-06 16:47:36 +07:00
# 4. If it is a paid indicator, check and deduct points
2026-04-09 14:30:51 +07:00
if pricing_type != "free" and effective_price > 0 :
2026-01-31 02:59:49 +08:00
buyer_credits = self . billing . get_user_credits ( buyer_id )
2026-02-27 01:57:04 +08:00
if buyer_credits < effective_price :
2026-01-31 02:59:49 +08:00
cur . close ()
2026-04-09 14:30:51 +07:00
return (
False ,
"insufficient_credits" ,
{ "required" : effective_price , "current" : float ( buyer_credits )},
)
2026-04-06 16:47:36 +07:00
# Deduct buyer points
2026-02-27 01:57:04 +08:00
new_buyer_balance = buyer_credits - Decimal ( str ( effective_price ))
2026-01-31 02:59:49 +08:00
cur . execute (
"UPDATE qd_users SET credits = ?, updated_at = NOW() WHERE id = ?" ,
2026-04-09 14:30:51 +07:00
( float ( new_buyer_balance ), buyer_id ),
2026-01-31 02:59:49 +08:00
)
2026-04-09 14:30:51 +07:00
2026-04-06 16:47:36 +07:00
# Record buyer points log
2026-04-09 14:30:51 +07:00
cur . execute (
"""
INSERT INTO qd_credits_log
2026-01-31 02:59:49 +08:00
(user_id, action, amount, balance_after, feature, reference_id, remark, created_at)
VALUES (?, 'indicator_purchase', ?, ?, 'indicator_purchase', ?, ?, NOW())
2026-04-09 14:30:51 +07:00
""" ,
(
buyer_id ,
- effective_price ,
float ( new_buyer_balance ),
str ( indicator_id ),
f "Buy indicator: { indicator [ 'name' ] } " ,
),
)
2026-04-06 16:47:36 +07:00
# Increase points for sellers (the commission ratio can be configured, here 100% is given to sellers first)
2026-01-31 02:59:49 +08:00
seller_credits = self . billing . get_user_credits ( seller_id )
2026-02-27 01:57:04 +08:00
new_seller_balance = seller_credits + Decimal ( str ( effective_price ))
2026-01-31 02:59:49 +08:00
cur . execute (
"UPDATE qd_users SET credits = ?, updated_at = NOW() WHERE id = ?" ,
2026-04-09 14:30:51 +07:00
( float ( new_seller_balance ), seller_id ),
2026-01-31 02:59:49 +08:00
)
2026-04-09 14:30:51 +07:00
2026-04-06 16:47:36 +07:00
# Record seller points log
2026-04-09 14:30:51 +07:00
cur . execute (
"""
INSERT INTO qd_credits_log
2026-01-31 02:59:49 +08:00
(user_id, action, amount, balance_after, feature, reference_id, remark, created_at)
VALUES (?, 'indicator_sale', ?, ?, 'indicator_sale', ?, ?, NOW())
2026-04-09 14:30:51 +07:00
""" ,
(
seller_id ,
effective_price ,
float ( new_seller_balance ),
str ( indicator_id ),
f "Sell indicator: { indicator [ 'name' ] } " ,
),
)
2026-04-06 16:47:36 +07:00
# 5. Create purchase record
2026-04-09 14:30:51 +07:00
cur . execute (
"""
INSERT INTO qd_indicator_purchases
2026-01-31 02:59:49 +08:00
(indicator_id, buyer_id, seller_id, price, created_at)
VALUES (?, ?, ?, ?, NOW())
2026-04-09 14:30:51 +07:00
""" ,
( indicator_id , buyer_id , seller_id , effective_price ),
)
2026-04-06 16:47:36 +07:00
# 6. Copy the indicator to the buyer’ s account
2026-01-31 02:59:49 +08:00
now_ts = int ( time . time ())
2026-02-28 00:15:58 +08:00
# Get vip_free as boolean from indicator
2026-04-09 14:30:51 +07:00
vip_free_value = bool ( indicator . get ( "vip_free" ) or False )
cur . execute (
"""
2026-01-31 02:59:49 +08:00
INSERT INTO qd_indicator_codes
(user_id, is_buy, end_time, name, code, description,
2026-02-27 01:57:04 +08:00
publish_to_community, pricing_type, price, is_encrypted, preview_image, vip_free,
2026-01-31 02:59:49 +08:00
createtime, updatetime, created_at, updated_at)
2026-02-28 00:15:58 +08:00
VALUES (?, 1, 0, ?, ?, ?, 0, 'free', 0, ?, ?, ?, ?, ?, NOW(), NOW())
2026-04-09 14:30:51 +07:00
""" ,
(
buyer_id ,
indicator [ "name" ],
indicator [ "code" ],
indicator [ "description" ],
indicator [ "is_encrypted" ] or 0 ,
indicator [ "preview_image" ],
vip_free_value , # Use boolean value instead of integer 0
now_ts ,
now_ts ,
),
)
2026-04-06 16:47:36 +07:00
# 7. Update indicator purchase times
2026-04-09 14:30:51 +07:00
cur . execute (
"""
UPDATE qd_indicator_codes
SET purchase_count = COALESCE(purchase_count, 0) + 1
2026-01-31 02:59:49 +08:00
WHERE id = ?
2026-04-09 14:30:51 +07:00
""" ,
( indicator_id ,),
)
2026-01-31 02:59:49 +08:00
db . commit ()
cur . close ()
2026-04-09 14:30:51 +07:00
logger . info (
f "User { buyer_id } purchased indicator { indicator_id } for { effective_price } credits (vip_free= { vip_free } , is_vip= { is_vip } )"
)
return (
True ,
"success" ,
{
"indicator_name" : indicator [ "name" ],
"price" : price ,
"charged" : effective_price ,
"vip_free" : vip_free ,
},
)
2026-01-31 02:59:49 +08:00
except Exception as e :
logger . error ( f "purchase_indicator failed: { e } " )
2026-04-09 14:30:51 +07:00
return False , f "error: { str ( e ) } " , {}
2026-01-31 02:59:49 +08:00
def get_my_purchases ( self , user_id : int , page : int = 1 , page_size : int = 20 ) -> Dict [ str , Any ]:
2026-04-08 07:27:26 +07:00
"""Get the list of indicators purchased by the user."""
2026-01-31 02:59:49 +08:00
offset = ( page - 1 ) * page_size
2026-04-09 14:30:51 +07:00
2026-01-31 02:59:49 +08:00
try :
with get_db_connection () as db :
cur = db . cursor ()
2026-04-09 14:30:51 +07:00
2026-04-06 16:47:36 +07:00
# Get total
2026-04-09 14:30:51 +07:00
cur . execute ( "SELECT COUNT(*) as count FROM qd_indicator_purchases WHERE buyer_id = ?" , ( user_id ,))
total = cur . fetchone ()[ "count" ]
2026-04-06 16:47:36 +07:00
# Get list
2026-04-09 14:30:51 +07:00
cur . execute (
"""
SELECT
2026-01-31 02:59:49 +08:00
p.id as purchase_id, p.price as purchase_price, p.created_at as purchase_time,
i.id, i.name, i.description, i.preview_image, i.avg_rating,
u.nickname as seller_nickname, u.avatar as seller_avatar
FROM qd_indicator_purchases p
LEFT JOIN qd_indicator_codes i ON p.indicator_id = i.id
LEFT JOIN qd_users u ON p.seller_id = u.id
WHERE p.buyer_id = ?
ORDER BY p.created_at DESC
LIMIT ? OFFSET ?
2026-04-09 14:30:51 +07:00
""" ,
( user_id , page_size , offset ),
)
2026-01-31 02:59:49 +08:00
rows = cur . fetchall () or []
cur . close ()
2026-04-09 14:30:51 +07:00
2026-01-31 02:59:49 +08:00
items = []
for row in rows :
2026-04-09 14:30:51 +07:00
items . append (
{
"purchase_id" : row [ "purchase_id" ],
"purchase_price" : float ( row [ "purchase_price" ] or 0 ),
"purchase_time" : row [ "purchase_time" ] . isoformat () if row [ "purchase_time" ] else None ,
"indicator" : {
"id" : row [ "id" ],
"name" : row [ "name" ],
"description" : row [ "description" ][: 100 ] if row [ "description" ] else "" ,
"preview_image" : row [ "preview_image" ] or "" ,
"avg_rating" : float ( row [ "avg_rating" ] or 0 ),
},
"seller" : {
"nickname" : row [ "seller_nickname" ],
"avatar" : row [ "seller_avatar" ] or "/avatar2.jpg" ,
},
2026-01-31 02:59:49 +08:00
}
2026-04-09 14:30:51 +07:00
)
2026-01-31 02:59:49 +08:00
return {
2026-04-09 14:30:51 +07:00
"items" : items ,
"total" : total ,
"page" : page ,
"page_size" : page_size ,
"total_pages" : ( total + page_size - 1 ) // page_size if total > 0 else 0 ,
2026-01-31 02:59:49 +08:00
}
2026-04-09 14:30:51 +07:00
2026-01-31 02:59:49 +08:00
except Exception as e :
logger . error ( f "get_my_purchases failed: { e } " )
2026-04-09 14:30:51 +07:00
return { "items" : [], "total" : 0 , "page" : 1 , "page_size" : page_size , "total_pages" : 0 }
2026-01-31 02:59:49 +08:00
# ==========================================
2026-04-06 16:47:36 +07:00
# Comment function
2026-01-31 02:59:49 +08:00
# ==========================================
2026-04-09 14:30:51 +07:00
2026-01-31 02:59:49 +08:00
def get_comments ( self , indicator_id : int , page : int = 1 , page_size : int = 20 ) -> Dict [ str , Any ]:
2026-04-08 07:27:26 +07:00
"""Get the list of indicator comments."""
2026-01-31 02:59:49 +08:00
offset = ( page - 1 ) * page_size
2026-04-09 14:30:51 +07:00
2026-01-31 02:59:49 +08:00
try :
with get_db_connection () as db :
cur = db . cursor ()
2026-04-09 14:30:51 +07:00
2026-04-06 16:47:36 +07:00
# Get the total number (only first-level comments are counted)
2026-04-09 14:30:51 +07:00
cur . execute (
"""
SELECT COUNT(*) as count FROM qd_indicator_comments
2026-01-31 02:59:49 +08:00
WHERE indicator_id = ? AND parent_id IS NULL AND is_deleted = 0
2026-04-09 14:30:51 +07:00
""" ,
( indicator_id ,),
)
total = cur . fetchone ()[ "count" ]
2026-04-06 16:47:36 +07:00
# Get the list of comments
2026-04-09 14:30:51 +07:00
cur . execute (
"""
SELECT
2026-01-31 02:59:49 +08:00
c.id, c.rating, c.content, c.created_at,
u.id as user_id, u.nickname, u.avatar
FROM qd_indicator_comments c
LEFT JOIN qd_users u ON c.user_id = u.id
WHERE c.indicator_id = ? AND c.parent_id IS NULL AND c.is_deleted = 0
ORDER BY c.created_at DESC
LIMIT ? OFFSET ?
2026-04-09 14:30:51 +07:00
""" ,
( indicator_id , page_size , offset ),
)
2026-01-31 02:59:49 +08:00
rows = cur . fetchall () or []
cur . close ()
2026-04-09 14:30:51 +07:00
2026-01-31 02:59:49 +08:00
items = []
for row in rows :
2026-04-09 14:30:51 +07:00
items . append (
{
"id" : row [ "id" ],
"rating" : row [ "rating" ],
"content" : row [ "content" ],
"created_at" : row [ "created_at" ] . isoformat () if row [ "created_at" ] else None ,
"user" : {
"id" : row [ "user_id" ],
"nickname" : row [ "nickname" ],
"avatar" : row [ "avatar" ] or "/avatar2.jpg" ,
},
2026-01-31 02:59:49 +08:00
}
2026-04-09 14:30:51 +07:00
)
2026-01-31 02:59:49 +08:00
return {
2026-04-09 14:30:51 +07:00
"items" : items ,
"total" : total ,
"page" : page ,
"page_size" : page_size ,
"total_pages" : ( total + page_size - 1 ) // page_size if total > 0 else 0 ,
2026-01-31 02:59:49 +08:00
}
2026-04-09 14:30:51 +07:00
2026-01-31 02:59:49 +08:00
except Exception as e :
logger . error ( f "get_comments failed: { e } " )
2026-04-09 14:30:51 +07:00
return { "items" : [], "total" : 0 , "page" : 1 , "page_size" : page_size , "total_pages" : 0 }
2026-01-31 02:59:49 +08:00
def add_comment (
2026-04-09 14:30:51 +07:00
self , user_id : int , indicator_id : int , rating : int , content : str
2026-01-31 02:59:49 +08:00
) -> Tuple [ bool , str , Dict [ str , Any ]]:
"""
2026-04-08 07:27:26 +07:00
Add a comment.
Only users who purchased the indicator can comment, and only once.
2026-01-31 02:59:49 +08:00
"""
try :
2026-04-06 16:47:36 +07:00
# Verify score range
2026-01-31 02:59:49 +08:00
rating = max ( 1 , min ( 5 , int ( rating )))
2026-04-09 14:30:51 +07:00
content = ( content or "" ) . strip ()[: 500 ] # Limit 500 words
2026-01-31 02:59:49 +08:00
with get_db_connection () as db :
cur = db . cursor ()
2026-04-09 14:30:51 +07:00
2026-04-06 16:47:36 +07:00
# Check if the indicator exists
2026-01-31 02:59:49 +08:00
cur . execute (
"SELECT id, user_id FROM qd_indicator_codes WHERE id = ? AND publish_to_community = 1" ,
2026-04-09 14:30:51 +07:00
( indicator_id ,),
2026-01-31 02:59:49 +08:00
)
indicator = cur . fetchone ()
if not indicator :
cur . close ()
2026-04-09 14:30:51 +07:00
return False , "indicator_not_found" , {}
2026-04-06 16:47:36 +07:00
# Cannot comment on own indicators
2026-04-09 14:30:51 +07:00
if indicator [ "user_id" ] == user_id :
2026-01-31 02:59:49 +08:00
cur . close ()
2026-04-09 14:30:51 +07:00
return False , "cannot_comment_own" , {}
2026-04-06 16:47:36 +07:00
# Check if it has been purchased (free indicators also need to be "acquired" to comment)
2026-01-31 02:59:49 +08:00
cur . execute (
"SELECT id FROM qd_indicator_purchases WHERE indicator_id = ? AND buyer_id = ?" ,
2026-04-09 14:30:51 +07:00
( indicator_id , user_id ),
2026-01-31 02:59:49 +08:00
)
if not cur . fetchone ():
cur . close ()
2026-04-09 14:30:51 +07:00
return False , "not_purchased" , {}
2026-04-06 16:47:36 +07:00
# Check if comments have been made
2026-01-31 02:59:49 +08:00
cur . execute (
"SELECT id FROM qd_indicator_comments WHERE indicator_id = ? AND user_id = ? AND parent_id IS NULL" ,
2026-04-09 14:30:51 +07:00
( indicator_id , user_id ),
2026-01-31 02:59:49 +08:00
)
if cur . fetchone ():
cur . close ()
2026-04-09 14:30:51 +07:00
return False , "already_commented" , {}
# Add comment
cur . execute (
"""
INSERT INTO qd_indicator_comments
2026-01-31 02:59:49 +08:00
(indicator_id, user_id, rating, content, created_at, updated_at)
VALUES (?, ?, ?, ?, NOW(), NOW())
2026-04-09 14:30:51 +07:00
""" ,
( indicator_id , user_id , rating , content ),
)
2026-01-31 02:59:49 +08:00
comment_id = cur . lastrowid
2026-04-09 14:30:51 +07:00
2026-04-06 16:47:36 +07:00
# Update the scoring statistics of the indicator
2026-04-09 14:30:51 +07:00
cur . execute (
"""
UPDATE qd_indicator_codes
SET
2026-01-31 02:59:49 +08:00
rating_count = COALESCE(rating_count, 0) + 1,
avg_rating = (
2026-04-09 14:30:51 +07:00
SELECT AVG(rating) FROM qd_indicator_comments
2026-01-31 02:59:49 +08:00
WHERE indicator_id = ? AND parent_id IS NULL AND is_deleted = 0
)
WHERE id = ?
2026-04-09 14:30:51 +07:00
""" ,
( indicator_id , indicator_id ),
)
2026-01-31 02:59:49 +08:00
db . commit ()
cur . close ()
2026-04-09 14:30:51 +07:00
2026-01-31 02:59:49 +08:00
logger . info ( f "User { user_id } commented on indicator { indicator_id } with rating { rating } " )
2026-04-09 14:30:51 +07:00
return True , "success" , { "comment_id" : comment_id }
2026-01-31 02:59:49 +08:00
except Exception as e :
logger . error ( f "add_comment failed: { e } " )
2026-04-09 14:30:51 +07:00
return False , f "error: { str ( e ) } " , {}
2026-01-31 02:59:49 +08:00
def update_comment (
2026-04-09 14:30:51 +07:00
self , user_id : int , comment_id : int , indicator_id : int , rating : int , content : str
2026-01-31 02:59:49 +08:00
) -> Tuple [ bool , str , Dict [ str , Any ]]:
"""
2026-04-08 07:27:26 +07:00
Update a comment.
Users can only edit their own comments.
2026-01-31 02:59:49 +08:00
"""
try :
rating = max ( 1 , min ( 5 , int ( rating )))
2026-04-09 14:30:51 +07:00
content = ( content or "" ) . strip ()[: 500 ]
2026-01-31 02:59:49 +08:00
with get_db_connection () as db :
cur = db . cursor ()
2026-04-09 14:30:51 +07:00
2026-04-06 16:47:36 +07:00
# Check if the comment exists and belongs to the current user
2026-04-09 14:30:51 +07:00
cur . execute (
"""
SELECT id, rating as old_rating FROM qd_indicator_comments
2026-01-31 02:59:49 +08:00
WHERE id = ? AND user_id = ? AND indicator_id = ? AND is_deleted = 0
2026-04-09 14:30:51 +07:00
""" ,
( comment_id , user_id , indicator_id ),
)
2026-01-31 02:59:49 +08:00
comment = cur . fetchone ()
2026-04-09 14:30:51 +07:00
2026-01-31 02:59:49 +08:00
if not comment :
cur . close ()
2026-04-09 14:30:51 +07:00
return False , "comment_not_found" , {}
old_rating = comment [ "old_rating" ]
2026-04-06 16:47:36 +07:00
# Update comment
2026-04-09 14:30:51 +07:00
cur . execute (
"""
UPDATE qd_indicator_comments
2026-01-31 02:59:49 +08:00
SET rating = ?, content = ?, updated_at = NOW()
WHERE id = ?
2026-04-09 14:30:51 +07:00
""" ,
( rating , content , comment_id ),
)
2026-04-06 16:47:36 +07:00
# If the rating changes, update the average rating of the metric
2026-01-31 02:59:49 +08:00
if old_rating != rating :
2026-04-09 14:30:51 +07:00
cur . execute (
"""
UPDATE qd_indicator_codes
2026-01-31 02:59:49 +08:00
SET avg_rating = (
2026-04-09 14:30:51 +07:00
SELECT AVG(rating) FROM qd_indicator_comments
2026-01-31 02:59:49 +08:00
WHERE indicator_id = ? AND parent_id IS NULL AND is_deleted = 0
)
WHERE id = ?
2026-04-09 14:30:51 +07:00
""" ,
( indicator_id , indicator_id ),
)
2026-01-31 02:59:49 +08:00
db . commit ()
cur . close ()
2026-04-09 14:30:51 +07:00
2026-01-31 02:59:49 +08:00
logger . info ( f "User { user_id } updated comment { comment_id } " )
2026-04-09 14:30:51 +07:00
return True , "success" , { "comment_id" : comment_id }
2026-01-31 02:59:49 +08:00
except Exception as e :
logger . error ( f "update_comment failed: { e } " )
2026-04-09 14:30:51 +07:00
return False , f "error: { str ( e ) } " , {}
2026-01-31 02:59:49 +08:00
def get_user_comment ( self , user_id : int , indicator_id : int ) -> Optional [ Dict [ str , Any ]]:
2026-04-08 07:27:26 +07:00
"""Get the user's comment for a specific indicator."""
2026-01-31 02:59:49 +08:00
try :
with get_db_connection () as db :
cur = db . cursor ()
2026-04-09 14:30:51 +07:00
cur . execute (
"""
2026-01-31 02:59:49 +08:00
SELECT id, rating, content, created_at, updated_at
FROM qd_indicator_comments
WHERE user_id = ? AND indicator_id = ? AND parent_id IS NULL AND is_deleted = 0
2026-04-09 14:30:51 +07:00
""" ,
( user_id , indicator_id ),
)
2026-01-31 02:59:49 +08:00
row = cur . fetchone ()
cur . close ()
2026-04-09 14:30:51 +07:00
2026-01-31 02:59:49 +08:00
if not row :
return None
2026-04-09 14:30:51 +07:00
2026-01-31 02:59:49 +08:00
return {
2026-04-09 14:30:51 +07:00
"id" : row [ "id" ],
"rating" : row [ "rating" ],
"content" : row [ "content" ],
"created_at" : row [ "created_at" ] . isoformat () if row [ "created_at" ] else None ,
"updated_at" : row [ "updated_at" ] . isoformat () if row [ "updated_at" ] else None ,
2026-01-31 02:59:49 +08:00
}
2026-04-09 14:30:51 +07:00
2026-01-31 02:59:49 +08:00
except Exception as e :
logger . error ( f "get_user_comment failed: { e } " )
return None
2026-04-09 14:30:51 +07:00
2026-01-31 02:59:49 +08:00
# ==========================================
2026-04-06 16:47:36 +07:00
# Administrator review function
2026-01-31 02:59:49 +08:00
# ==========================================
2026-04-09 14:30:51 +07:00
2026-01-31 02:59:49 +08:00
def get_pending_indicators (
self ,
page : int = 1 ,
page_size : int = 20 ,
2026-04-09 14:30:51 +07:00
review_status : str = "pending" , # 'pending' / 'approved' / 'rejected' / 'all'
2026-01-31 02:59:49 +08:00
) -> Dict [ str , Any ]:
2026-04-08 07:27:26 +07:00
"""Get the list of indicators pending review for admins."""
2026-01-31 02:59:49 +08:00
offset = ( page - 1 ) * page_size
2026-04-09 14:30:51 +07:00
2026-01-31 02:59:49 +08:00
try :
with get_db_connection () as db :
cur = db . cursor ()
2026-04-09 14:30:51 +07:00
2026-04-06 16:47:36 +07:00
# Build query conditions
2026-01-31 02:59:49 +08:00
where_clauses = [ "i.publish_to_community = 1" ]
params = []
2026-04-09 14:30:51 +07:00
if review_status and review_status != "all" :
2026-01-31 02:59:49 +08:00
where_clauses . append ( "i.review_status = ?" )
params . append ( review_status )
2026-04-09 14:30:51 +07:00
2026-01-31 02:59:49 +08:00
where_sql = " AND " . join ( where_clauses )
2026-04-09 14:30:51 +07:00
2026-04-06 16:47:36 +07:00
# Get total
2026-01-31 02:59:49 +08:00
count_sql = f """
2026-04-09 14:30:51 +07:00
SELECT COUNT(*) as count
FROM qd_indicator_codes i
2026-01-31 02:59:49 +08:00
WHERE { where_sql }
"""
cur . execute ( count_sql , tuple ( params ))
2026-04-09 14:30:51 +07:00
total = cur . fetchone ()[ "count" ]
2026-04-06 16:47:36 +07:00
# Get the list
2026-01-31 02:59:49 +08:00
query_sql = f """
2026-04-09 14:30:51 +07:00
SELECT
2026-01-31 02:59:49 +08:00
i.id, i.name, i.description, i.pricing_type, i.price,
2026-04-09 14:30:51 +07:00
i.preview_image, i.code, i.review_status, i.review_note,
2026-01-31 02:59:49 +08:00
i.reviewed_at, i.reviewed_by, i.created_at,
2026-04-09 14:30:51 +07:00
u.id as author_id, u.username as author_username,
2026-01-31 02:59:49 +08:00
u.nickname as author_nickname, u.avatar as author_avatar,
r.username as reviewer_username
FROM qd_indicator_codes i
LEFT JOIN qd_users u ON i.user_id = u.id
LEFT JOIN qd_users r ON i.reviewed_by = r.id
WHERE { where_sql }
ORDER BY i.created_at DESC
LIMIT ? OFFSET ?
"""
cur . execute ( query_sql , tuple ( params + [ page_size , offset ]))
rows = cur . fetchall () or []
cur . close ()
2026-04-09 14:30:51 +07:00
2026-01-31 02:59:49 +08:00
items = []
for row in rows :
2026-04-09 14:30:51 +07:00
items . append (
{
"id" : row [ "id" ],
"name" : row [ "name" ],
"description" : row [ "description" ][: 300 ] if row [ "description" ] else "" ,
"pricing_type" : row [ "pricing_type" ] or "free" ,
"price" : float ( row [ "price" ] or 0 ),
"preview_image" : row [ "preview_image" ] or "" ,
"code" : row [ "code" ] or "" , # Administrators can view the code
"review_status" : row [ "review_status" ] or "pending" ,
"review_note" : row [ "review_note" ] or "" ,
"reviewed_at" : row [ "reviewed_at" ] . isoformat () if row [ "reviewed_at" ] else None ,
"reviewer_username" : row [ "reviewer_username" ],
"created_at" : row [ "created_at" ] . isoformat () if row [ "created_at" ] else None ,
"author" : {
"id" : row [ "author_id" ],
"username" : row [ "author_username" ],
"nickname" : row [ "author_nickname" ] or row [ "author_username" ],
"avatar" : row [ "author_avatar" ] or "/avatar2.jpg" ,
},
2026-01-31 02:59:49 +08:00
}
2026-04-09 14:30:51 +07:00
)
2026-01-31 02:59:49 +08:00
return {
2026-04-09 14:30:51 +07:00
"items" : items ,
"total" : total ,
"page" : page ,
"page_size" : page_size ,
"total_pages" : ( total + page_size - 1 ) // page_size if total > 0 else 0 ,
2026-01-31 02:59:49 +08:00
}
2026-04-09 14:30:51 +07:00
2026-01-31 02:59:49 +08:00
except Exception as e :
logger . error ( f "get_pending_indicators failed: { e } " )
2026-04-09 14:30:51 +07:00
return { "items" : [], "total" : 0 , "page" : 1 , "page_size" : page_size , "total_pages" : 0 }
2026-01-31 02:59:49 +08:00
def review_indicator (
self ,
admin_id : int ,
indicator_id : int ,
action : str , # 'approve' / 'reject'
2026-04-09 14:30:51 +07:00
note : str = "" ,
2026-01-31 02:59:49 +08:00
) -> Tuple [ bool , str ]:
2026-04-08 07:27:26 +07:00
"""Review an indicator."""
2026-01-31 02:59:49 +08:00
try :
2026-04-09 14:30:51 +07:00
new_status = "approved" if action == "approve" else "rejected"
note = ( note or "" ) . strip ()[: 500 ]
2026-01-31 02:59:49 +08:00
with get_db_connection () as db :
cur = db . cursor ()
2026-04-09 14:30:51 +07:00
2026-04-06 16:47:36 +07:00
# Check if the indicator exists and has been published to the community
2026-04-09 14:30:51 +07:00
cur . execute (
"""
SELECT id, name, user_id FROM qd_indicator_codes
2026-01-31 02:59:49 +08:00
WHERE id = ? AND publish_to_community = 1
2026-04-09 14:30:51 +07:00
""" ,
( indicator_id ,),
)
2026-01-31 02:59:49 +08:00
indicator = cur . fetchone ()
2026-04-09 14:30:51 +07:00
2026-01-31 02:59:49 +08:00
if not indicator :
cur . close ()
2026-04-09 14:30:51 +07:00
return False , "indicator_not_found"
2026-04-06 16:47:36 +07:00
# Update review status
2026-04-09 14:30:51 +07:00
cur . execute (
"""
UPDATE qd_indicator_codes
2026-01-31 02:59:49 +08:00
SET review_status = ?, review_note = ?, reviewed_at = NOW(), reviewed_by = ?
WHERE id = ?
2026-04-09 14:30:51 +07:00
""" ,
( new_status , note , admin_id , indicator_id ),
)
2026-01-31 02:59:49 +08:00
db . commit ()
cur . close ()
2026-04-09 14:30:51 +07:00
2026-01-31 02:59:49 +08:00
logger . info ( f "Admin { admin_id } { action } d indicator { indicator_id } " )
2026-04-09 14:30:51 +07:00
return True , "success"
2026-01-31 02:59:49 +08:00
except Exception as e :
logger . error ( f "review_indicator failed: { e } " )
2026-04-09 14:30:51 +07:00
return False , f "error: { str ( e ) } "
def unpublish_indicator ( self , admin_id : int , indicator_id : int , note : str = "" ) -> Tuple [ bool , str ]:
2026-04-08 07:27:26 +07:00
"""Unpublish an indicator."""
2026-01-31 02:59:49 +08:00
try :
2026-04-09 14:30:51 +07:00
note = ( note or "" ) . strip ()[: 500 ]
2026-01-31 02:59:49 +08:00
with get_db_connection () as db :
cur = db . cursor ()
2026-04-09 14:30:51 +07:00
2026-04-06 16:47:36 +07:00
# Check if the indicator exists
2026-04-09 14:30:51 +07:00
cur . execute (
"""
2026-01-31 02:59:49 +08:00
SELECT id, name FROM qd_indicator_codes WHERE id = ?
2026-04-09 14:30:51 +07:00
""" ,
( indicator_id ,),
)
2026-01-31 02:59:49 +08:00
indicator = cur . fetchone ()
2026-04-09 14:30:51 +07:00
2026-01-31 02:59:49 +08:00
if not indicator :
cur . close ()
2026-04-09 14:30:51 +07:00
return False , "indicator_not_found"
2026-04-06 16:47:36 +07:00
# Remove from shelves (cancel publication)
2026-04-09 14:30:51 +07:00
cur . execute (
"""
UPDATE qd_indicator_codes
SET publish_to_community = 0, review_status = 'rejected',
2026-01-31 02:59:49 +08:00
review_note = ?, reviewed_at = NOW(), reviewed_by = ?
WHERE id = ?
2026-04-09 14:30:51 +07:00
""" ,
( f "Removal: { note } " if note else "Removal by administrator" , admin_id , indicator_id ),
)
2026-01-31 02:59:49 +08:00
db . commit ()
cur . close ()
2026-04-09 14:30:51 +07:00
2026-01-31 02:59:49 +08:00
logger . info ( f "Admin { admin_id } unpublished indicator { indicator_id } " )
2026-04-09 14:30:51 +07:00
return True , "success"
2026-01-31 02:59:49 +08:00
except Exception as e :
logger . error ( f "unpublish_indicator failed: { e } " )
2026-04-09 14:30:51 +07:00
return False , f "error: { str ( e ) } "
2026-01-31 02:59:49 +08:00
def admin_delete_indicator ( self , admin_id : int , indicator_id : int ) -> Tuple [ bool , str ]:
2026-04-08 07:27:26 +07:00
"""Delete an indicator as an admin."""
2026-01-31 02:59:49 +08:00
try :
with get_db_connection () as db :
cur = db . cursor ()
2026-04-09 14:30:51 +07:00
2026-04-06 16:47:36 +07:00
# Check if the indicator exists
2026-01-31 02:59:49 +08:00
cur . execute ( "SELECT id, name FROM qd_indicator_codes WHERE id = ?" , ( indicator_id ,))
indicator = cur . fetchone ()
2026-04-09 14:30:51 +07:00
2026-01-31 02:59:49 +08:00
if not indicator :
cur . close ()
2026-04-09 14:30:51 +07:00
return False , "indicator_not_found"
2026-04-06 16:47:36 +07:00
# Delete associated comments
2026-01-31 02:59:49 +08:00
cur . execute ( "DELETE FROM qd_indicator_comments WHERE indicator_id = ?" , ( indicator_id ,))
2026-04-09 14:30:51 +07:00
2026-04-06 16:47:36 +07:00
# Delete associated purchase history
2026-01-31 02:59:49 +08:00
cur . execute ( "DELETE FROM qd_indicator_purchases WHERE indicator_id = ?" , ( indicator_id ,))
2026-04-09 14:30:51 +07:00
2026-04-06 16:47:36 +07:00
# Delete indicator
2026-01-31 02:59:49 +08:00
cur . execute ( "DELETE FROM qd_indicator_codes WHERE id = ?" , ( indicator_id ,))
2026-04-09 14:30:51 +07:00
2026-01-31 02:59:49 +08:00
db . commit ()
cur . close ()
2026-04-09 14:30:51 +07:00
2026-01-31 02:59:49 +08:00
logger . info ( f "Admin { admin_id } deleted indicator { indicator_id } " )
2026-04-09 14:30:51 +07:00
return True , "success"
2026-01-31 02:59:49 +08:00
except Exception as e :
logger . error ( f "admin_delete_indicator failed: { e } " )
2026-04-09 14:30:51 +07:00
return False , f "error: { str ( e ) } "
2026-01-31 02:59:49 +08:00
def get_review_stats ( self ) -> Dict [ str , int ]:
2026-04-06 16:47:36 +07:00
"""Get audit statistics"""
2026-01-31 02:59:49 +08:00
try :
with get_db_connection () as db :
cur = db . cursor ()
cur . execute ( """
2026-04-09 14:30:51 +07:00
SELECT
2026-01-31 02:59:49 +08:00
COUNT(*) FILTER (WHERE review_status = 'pending' OR review_status IS NULL) as pending_count,
COUNT(*) FILTER (WHERE review_status = 'approved') as approved_count,
COUNT(*) FILTER (WHERE review_status = 'rejected') as rejected_count
FROM qd_indicator_codes
WHERE publish_to_community = 1
""" )
row = cur . fetchone ()
cur . close ()
2026-04-09 14:30:51 +07:00
2026-01-31 02:59:49 +08:00
return {
2026-04-09 14:30:51 +07:00
"pending" : row [ "pending_count" ] or 0 ,
"approved" : row [ "approved_count" ] or 0 ,
"rejected" : row [ "rejected_count" ] or 0 ,
2026-01-31 02:59:49 +08:00
}
except Exception as e :
logger . error ( f "get_review_stats failed: { e } " )
2026-04-09 14:30:51 +07:00
return { "pending" : 0 , "approved" : 0 , "rejected" : 0 }
2026-01-31 02:59:49 +08:00
# ==========================================
2026-04-06 16:47:36 +07:00
# Real performance (aggregated backtest + real transaction data)
2026-01-31 02:59:49 +08:00
# ==========================================
2026-02-27 01:57:04 +08:00
2026-01-31 02:59:49 +08:00
def get_indicator_performance ( self , indicator_id : int ) -> Dict [ str , Any ]:
"""
2026-04-08 07:27:26 +07:00
Get live performance statistics for an indicator.
2026-02-27 01:57:04 +08:00
2026-04-08 07:27:26 +07:00
Data sources:
1. qd_backtest_runs - Backtest records (result_json contains totalReturn, winRate, etc.)
2. qd_strategy_trades + qd_strategies_trading - Real live trading records
2026-01-31 02:59:49 +08:00
"""
default_result = {
2026-04-09 14:30:51 +07:00
"strategy_count" : 0 ,
"trade_count" : 0 ,
"win_rate" : 0 ,
"total_profit" : 0 ,
"avg_return" : 0 ,
"max_drawdown" : 0 ,
2026-01-31 02:59:49 +08:00
}
2026-02-27 01:57:04 +08:00
2026-01-31 02:59:49 +08:00
try :
with get_db_connection () as db :
cur = db . cursor ()
2026-02-27 01:57:04 +08:00
2026-04-06 16:47:36 +07:00
# ---------- Part 1: Backtest data (parsed from result_json) ----------
2026-02-27 01:57:04 +08:00
bt_returns = []
bt_win_rates = []
bt_drawdowns = []
bt_trade_counts = []
try :
2026-04-09 14:30:51 +07:00
cur . execute (
"""
2026-02-27 01:57:04 +08:00
SELECT result_json
FROM qd_backtest_runs
WHERE indicator_id = %s AND status = 'success'
AND result_json IS NOT NULL AND result_json != ''
2026-04-09 14:30:51 +07:00
""" ,
( indicator_id ,),
)
2026-02-27 01:57:04 +08:00
rows = cur . fetchall ()
for row in rows :
try :
2026-04-09 14:30:51 +07:00
rj = json . loads ( row [ "result_json" ]) if isinstance ( row [ "result_json" ], str ) else {}
tr = float ( rj . get ( "totalReturn" , 0 ) or 0 )
wr = float ( rj . get ( "winRate" , 0 ) or 0 )
md = float ( rj . get ( "maxDrawdown" , 0 ) or 0 )
tc = int ( rj . get ( "totalTrades" , 0 ) or 0 )
2026-02-27 01:57:04 +08:00
bt_returns . append ( tr )
bt_win_rates . append ( wr )
bt_drawdowns . append ( md )
bt_trade_counts . append ( tc )
except ( json . JSONDecodeError , TypeError , ValueError ):
continue
except Exception :
logger . debug ( "Backtest runs query skipped or failed" , exc_info = True )
bt_run_count = len ( bt_returns )
2026-04-06 16:47:36 +07:00
# ---------- Part 2: Real transaction data ----------
2026-02-27 01:57:04 +08:00
live_strategy_count = 0
live_trade_count = 0
live_win_rate = 0.0
live_total_profit = 0.0
try :
2026-04-06 16:47:36 +07:00
# Find the strategy that uses this indicator (matches indicator_id in indicator_config JSON)
2026-04-09 14:30:51 +07:00
cur . execute (
"""
2026-02-27 01:57:04 +08:00
SELECT id FROM qd_strategies_trading
WHERE indicator_config::text LIKE %s
2026-04-09 14:30:51 +07:00
""" ,
( f '%"indicator_id": { indicator_id } %' ,),
)
2026-02-27 01:57:04 +08:00
strategy_rows = cur . fetchall ()
2026-04-06 16:47:36 +07:00
# Also try to match formats without spaces
2026-02-27 01:57:04 +08:00
if not strategy_rows :
2026-04-09 14:30:51 +07:00
cur . execute (
"""
2026-02-27 01:57:04 +08:00
SELECT id FROM qd_strategies_trading
WHERE indicator_config::text LIKE %s
2026-04-09 14:30:51 +07:00
""" ,
( f '%"indicator_id": { indicator_id } %' ,),
)
2026-02-27 01:57:04 +08:00
strategy_rows = cur . fetchall ()
if strategy_rows :
2026-04-09 14:30:51 +07:00
strategy_ids = [ r [ "id" ] for r in strategy_rows ]
2026-02-27 01:57:04 +08:00
live_strategy_count = len ( strategy_ids )
2026-04-09 14:30:51 +07:00
placeholders = "," . join ([ " %s " ] * len ( strategy_ids ))
cur . execute (
f """
SELECT
2026-02-27 01:57:04 +08:00
COUNT(*) as trade_count,
SUM(CASE WHEN profit > 0 THEN 1 ELSE 0 END) as win_count,
SUM(profit) as total_profit
FROM qd_strategy_trades
WHERE strategy_id IN ( { placeholders } )
AND profit != 0
2026-04-09 14:30:51 +07:00
""" ,
tuple ( strategy_ids ),
)
2026-02-27 01:57:04 +08:00
trade_row = cur . fetchone ()
2026-04-09 14:30:51 +07:00
if trade_row and ( trade_row [ "trade_count" ] or 0 ) > 0 :
live_trade_count = int ( trade_row [ "trade_count" ] or 0 )
win_count = int ( trade_row [ "win_count" ] or 0 )
live_win_rate = (
round ( win_count / live_trade_count * 100 , 2 ) if live_trade_count > 0 else 0.0
)
live_total_profit = round ( float ( trade_row [ "total_profit" ] or 0 ), 2 )
2026-02-27 01:57:04 +08:00
except Exception :
logger . debug ( "Live trading query skipped or failed" , exc_info = True )
2026-01-31 02:59:49 +08:00
cur . close ()
2026-02-27 01:57:04 +08:00
# ---------- Combine results ----------
total_strategy_count = bt_run_count + live_strategy_count
total_trade_count = sum ( bt_trade_counts ) + live_trade_count
2026-04-06 16:47:36 +07:00
# Comprehensive winning rate: Prioritize real offer > Backtest average
2026-02-27 01:57:04 +08:00
if live_trade_count > 0 :
combined_win_rate = live_win_rate
elif bt_win_rates :
combined_win_rate = round ( sum ( bt_win_rates ) / len ( bt_win_rates ), 2 )
else :
combined_win_rate = 0.0
2026-04-06 16:47:36 +07:00
# Average return (backtest totalReturn %)
2026-02-27 01:57:04 +08:00
avg_return = round ( sum ( bt_returns ) / len ( bt_returns ), 2 ) if bt_returns else 0.0
2026-04-09 14:30:51 +07:00
# Total profit: priority is given to the absolute profit of the real offer. If there is no real offer, the average return rate of the backtest will be displayed.
2026-02-27 01:57:04 +08:00
combined_profit = live_total_profit if live_trade_count > 0 else avg_return
2026-04-06 16:47:36 +07:00
# The maximum drawdown is the worst in the backtest (maxDrawdown is a negative number, the smallest is the worst)
2026-02-27 01:57:04 +08:00
avg_drawdown = round ( min ( bt_drawdowns ), 2 ) if bt_drawdowns else 0.0
if total_strategy_count == 0 and total_trade_count == 0 :
2026-01-31 02:59:49 +08:00
return default_result
2026-02-27 01:57:04 +08:00
2026-01-31 02:59:49 +08:00
return {
2026-04-09 14:30:51 +07:00
"strategy_count" : total_strategy_count ,
"trade_count" : total_trade_count ,
"win_rate" : combined_win_rate ,
"total_profit" : round ( combined_profit , 2 ),
"avg_return" : avg_return ,
"max_drawdown" : avg_drawdown ,
2026-01-31 02:59:49 +08:00
}
2026-02-27 01:57:04 +08:00
2026-01-31 02:59:49 +08:00
except Exception as e :
logger . error ( f "get_indicator_performance failed: { e } " )
return default_result
2026-04-06 16:47:36 +07:00
# Global singleton
2026-01-31 02:59:49 +08:00
_community_service = None
def get_community_service () -> CommunityService :
2026-04-08 07:27:26 +07:00
"""Get the community service singleton."""
2026-01-31 02:59:49 +08:00
global _community_service
if _community_service is None :
_community_service = CommunityService ()
return _community_service