排行榜按收益/流水排序;小程序配置与页面资源入库可后台管理

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
XingQue
2026-06-27 01:04:04 +08:00
parent ceaca8bf4d
commit 2c4ad6e13a
15 changed files with 718 additions and 111 deletions

View File

@@ -3,7 +3,7 @@
"""
import logging
from django.conf import settings
from utils.xcx_sys_config import wx_cfg
logger = logging.getLogger('weixin_broadcast')
@@ -29,7 +29,7 @@ def build_order_broadcast_payload(dingdan, order_type='普通订单', game_type=
def submit_dingdan_guangbo(order_info):
"""提交 Celery 广播任务broadcast 优先队列)"""
if not getattr(settings, 'WEIXIN_BROADCAST_ENABLED', True):
if not wx_cfg.WEIXIN_BROADCAST_ENABLED:
logger.info('服务号广播已关闭,跳过')
return False
if not order_info or not order_info.get('dingdan_id'):

View File

@@ -0,0 +1,91 @@
"""小程序页面静态资源(背景图/图标)— 数据库配置 + 默认值"""
from django.core.cache import cache
_CACHE_PREFIX = 'xcx_page_ziyuan_v1'
_CACHE_TTL = 120
# 默认资源(相对 OSS 路径,不含域名)
DEFAULT_ZIYUAN = {
'paihangbang.pageBg': 'beijing/paihangbang/page-bg.jpg',
'paihangbang.cardBg': 'beijing/paihangbang/list-card-bg.png',
'paihangbang.emptyIcon': 'beijing/paihangbang/empty.png',
'paihangbang.refreshIcon': 'beijing/paihangbang/icon-refresh.png',
'shangjiaduan.pageBg': 'beijing/shangjiaduan/page_bg.png',
'shangjiaduan.avatarCardBg': 'beijing/shangjiaduan/avatar_card_bg.png',
'shangjiaduan.assetCardBg': 'beijing/shangjiaduan/asset_card_bg.png',
'shangjiaduan.statCardBg': 'beijing/shangjiaduan/stat_card_bg.png',
'shangjiaduan.funcItemBg': 'beijing/shangjiaduan/func_item_bg.png',
'shangjiaduan.primaryFuncBg': 'beijing/shangjiaduan/primary_func_bg.png',
'shangjiaduan.highlightFuncBg': 'beijing/shangjiaduan/highlight_func_bg.png',
'shangjiaduan.iconRelease': 'beijing/shangjiaduan/icon_release.png',
'shangjiaduan.iconFast': 'beijing/shangjiaduan/icon_fast.png',
'shangjiaduan.iconOrders': 'beijing/shangjiaduan/icon_orders.png',
'shangjiaduan.iconService': 'beijing/shangjiaduan/icon_service.png',
'shangjiaduan.iconPunish': 'beijing/shangjiaduan/icon_punish.png',
'shangjiaduan.iconRank': 'beijing/shangjiaduan/icon_rank.png',
'shangjiaduan.iconRecharge': 'beijing/shangjiaduan/icon_recharge.png',
'shangjiaduan.iconWithdraw': 'beijing/shangjiaduan/icon_withdraw.png',
'shangjiaduan.iconRefresh': 'beijing/shangjiaduan/icon_refresh.png',
'shangjiaduan.iconCopy': 'beijing/shangjiaduan/icon_copy.png',
'shangjiaduan.avatarFrame': 'beijing/shangjiaduan/avatar_frame.png',
}
def clear_page_ziyuan_cache():
cache.delete_many([f'{_CACHE_PREFIX}:all', f'{_CACHE_PREFIX}:paihangbang', f'{_CACHE_PREFIX}:shangjiaduan'])
def _load_all_from_db():
try:
from peizhi.models import XcxPageZiyuan
rows = XcxPageZiyuan.objects.all()
data = {}
for r in rows:
key = f'{r.ziyuan_zu}.{r.ziyuan_key}'
if r.ziyuan_path:
data[key] = r.ziyuan_path.lstrip('/')
return data
except Exception:
return {}
def get_all_ziyuan_merged():
cached = cache.get(f'{_CACHE_PREFIX}:all')
if cached is not None:
return cached
merged = dict(DEFAULT_ZIYUAN)
merged.update(_load_all_from_db())
cache.set(f'{_CACHE_PREFIX}:all', merged, _CACHE_TTL)
return merged
def get_ziyuan_group(zu: str) -> dict:
cache_key = f'{_CACHE_PREFIX}:{zu}'
cached = cache.get(cache_key)
if cached is not None:
return cached
merged = get_all_ziyuan_merged()
group = {}
prefix = f'{zu}.'
for k, v in merged.items():
if k.startswith(prefix):
group[k[len(prefix):]] = v
cache.set(cache_key, group, _CACHE_TTL)
return group
def build_paihang_assets_for_api():
g = get_ziyuan_group('paihangbang')
return {
'pageBg': g.get('pageBg', DEFAULT_ZIYUAN['paihangbang.pageBg']),
'cardBg': g.get('cardBg', DEFAULT_ZIYUAN['paihangbang.cardBg']),
'emptyIcon': g.get('emptyIcon', DEFAULT_ZIYUAN['paihangbang.emptyIcon']),
'refreshIcon': g.get('refreshIcon', DEFAULT_ZIYUAN['paihangbang.refreshIcon']),
}
def build_page_assets_for_api():
return {
'shangjiaduan': get_ziyuan_group('shangjiaduan'),
'paihangbang': get_ziyuan_group('paihangbang'),
}

View File

@@ -7,6 +7,7 @@ import requests
from django.conf import settings
from django.core.cache import cache
from utils.xcx_sys_config import wx_cfg
from yonghu.models import OfficialAccountUser
logger = logging.getLogger('weixin_broadcast')
@@ -18,9 +19,9 @@ class WeixinBroadcastSender:
"""微信服务号模板消息发送器(连接复用 + 线程池并发)"""
def __init__(self):
self.appid = getattr(settings, 'WEIXIN_OFFICIAL_APPID', '')
self.secret = getattr(settings, 'WEIXIN_OFFICIAL_SECRET', '')
self.template_id = getattr(settings, 'WEIXIN_TEMPLATE_ID', '')
self.appid = wx_cfg.WEIXIN_OFFICIAL_APPID
self.secret = wx_cfg.WEIXIN_OFFICIAL_SECRET
self.template_id = wx_cfg.WEIXIN_TEMPLATE_ID
self._session = requests.Session()
def get_access_token(self):
@@ -65,9 +66,9 @@ class WeixinBroadcastSender:
'template_id': self.template_id,
'data': template_data,
}
if order_id and getattr(settings, 'WEIXIN_APPID', None):
if order_id and wx_cfg.WEIXIN_APPID:
request_data['miniprogram'] = {
'appid': settings.WEIXIN_APPID,
'appid': wx_cfg.WEIXIN_APPID,
'pagepath': f'pages/jiedan/jiedan?id={order_id}',
}
@@ -98,7 +99,7 @@ class WeixinBroadcastSender:
return {'success': True, 'msg': '无订阅用户', 'total': 0, 'success_count': 0, 'fail_count': 0}
max_workers = min(
int(getattr(settings, 'WEIXIN_BROADCAST_WORKERS', 30)),
int(wx_cfg.WEIXIN_BROADCAST_WORKERS),
max(8, total),
)
logger.info(f'广播开始 订单={order_id} 人数={total} 并发={max_workers}')

123
utils/xcx_sys_config.py Normal file
View File

@@ -0,0 +1,123 @@
"""
小程序/微信系统配置 — 优先读数据库 XcxSysPeizhi空则回退 settings.py兼容旧部署
"""
import logging
from django.conf import settings
from django.core.cache import cache
logger = logging.getLogger(__name__)
_CACHE_KEY = 'xcx_sys_peizhi_row_v1'
_CACHE_TTL = 120
def clear_xcx_sys_peizhi_cache():
cache.delete(_CACHE_KEY)
def _load_row():
cached = cache.get(_CACHE_KEY)
if cached is not _CACHE_NONE and cached is not None:
return cached
try:
from peizhi.models import XcxSysPeizhi
row = XcxSysPeizhi.objects.first()
except Exception as e:
logger.warning('读取 XcxSysPeizhi 失败,使用 settings 回退: %s', e)
row = None
cache.set(_CACHE_KEY, row if row else _CACHE_NONE, _CACHE_TTL)
return row if row else None
_CACHE_NONE = object()
def _val(db_attr, settings_attr, default=None):
row = _load_row()
if row is not None:
v = getattr(row, db_attr, None)
if v is not None and v != '':
return v
if settings_attr:
v = getattr(settings, settings_attr, default)
return v if v is not None and v != '' else default
return default
class WxConfig:
"""微信/小程序配置代理(属性名与 settings 保持一致,便于渐进替换)"""
@property
def WEIXIN_APPID(self):
return _val('weixin_appid', 'WEIXIN_APPID', '')
@property
def WEIXIN_SECRET(self):
return _val('weixin_secret', 'WEIXIN_SECRET', '')
@property
def WEIXIN_OFFICIAL_APPID(self):
return _val('weixin_official_appid', 'WEIXIN_OFFICIAL_APPID', '')
@property
def WEIXIN_OFFICIAL_SECRET(self):
return _val('weixin_official_secret', 'WEIXIN_OFFICIAL_SECRET', '')
@property
def WEIXIN_OFFICIAL_TOKEN(self):
return _val('weixin_official_token', 'WEIXIN_OFFICIAL_TOKEN', '')
@property
def WEIXIN_OFFICIAL_ENCODING_AES_KEY(self):
return _val('weixin_official_encoding_aes_key', 'WEIXIN_OFFICIAL_ENCODING_AES_KEY', '')
@property
def WEIXIN_TEMPLATE_ID(self):
return _val('weixin_template_id', 'WEIXIN_TEMPLATE_ID', '')
@property
def WEIXIN_TEMPLATE_MAX_PER_MINUTE(self):
return _val('weixin_template_max_per_minute', 'WEIXIN_TEMPLATE_MAX_PER_MINUTE', 950)
@property
def WEIXIN_TEMPLATE_BATCH_SIZE(self):
return _val('weixin_template_batch_size', 'WEIXIN_TEMPLATE_BATCH_SIZE', 100)
@property
def WEIXIN_BROADCAST_ENABLED(self):
row = _load_row()
if row is not None:
return bool(row.weixin_broadcast_enabled)
return getattr(settings, 'WEIXIN_BROADCAST_ENABLED', True)
@property
def WEIXIN_BROADCAST_WORKERS(self):
return _val('weixin_broadcast_workers', 'WEIXIN_BROADCAST_WORKERS', 30)
@property
def WEIXIN_MCHID(self):
return _val('weixin_mchid', 'WEIXIN_MCHID', '')
@property
def WEIXIN_SHANGHUMIYAO(self):
return _val('weixin_shanghu_miyao', 'WEIXIN_SHANGHUMIYAO', '')
@property
def WEIXIN_NOTIFY_URL(self):
return _val('weixin_notify_url', 'WEIXIN_NOTIFY_URL', '')
@property
def WEIXIN_CERT_PATH(self):
return _val('weixin_cert_path', 'WEIXIN_CERT_PATH', '')
@property
def WEIXIN_KEY_PATH(self):
return _val('weixin_key_path', 'WEIXIN_KEY_PATH', '')
@property
def NOTIFY_BASE_URL(self):
return _val('notify_base_url', None, 'https://wenhe.nmslb.com/hqhd').rstrip('/')
wx_cfg = WxConfig()