feat: 会员/履约金页文案按俱乐部可配置

新增 club_recharge_page_copy 表与小程序/后台读写接口,kefu 菜单挂「充值页文案」。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
XingQue
2026-07-24 00:04:23 +08:00
parent dcb191ac3a
commit 4b5130c824
7 changed files with 402 additions and 1 deletions

View File

@@ -92,7 +92,7 @@ def _load_menu_pages():
pages = []
need_seed = not pages or not any(
p.page_id in ('miniapp.assets', 'product.fake-orders') for p in pages
p.page_id in ('miniapp.assets', 'product.fake-orders', 'miniapp.recharge-copy') for p in pages
)
if need_seed:
try:

View File

@@ -97,6 +97,8 @@ KEFU_MENU_ROW_DEFS = [
'perm_codes': ['dsks666', '8080a']},
{'page_id': 'miniapp.rank-reward', 'name': '排行榜奖励', 'path': '/miniapp/rank-reward', 'parent_id': 'miniapp', 'sort_order': 118,
'perm_codes': ['phbj666', '8080a']},
{'page_id': 'miniapp.recharge-copy', 'name': '充值页文案', 'path': '/miniapp/recharge-copy', 'parent_id': 'miniapp', 'sort_order': 119,
'perm_codes': ['8080a']},
{'page_id': 'shop', 'name': '店铺管理', 'path': '', 'parent_id': '', 'sort_order': 120},
{'page_id': 'shop.list', 'name': '店铺列表', 'path': '/shop/list', 'parent_id': 'shop', 'sort_order': 121,
'perm_codes': ['999a', '999b', '999c', '999d', '999e']},

View File

@@ -0,0 +1,204 @@
"""会员/履约金充值页文案按俱乐部读写。"""
import json
from jituan.models import ClubRechargePageCopy
from jituan.services.club_context import resolve_club_id_from_request, resolve_effective_club_id
PAGE_HUIYUAN = 'huiyuan'
PAGE_YAJIN = 'yajin'
PAGE_KEYS = {PAGE_HUIYUAN, PAGE_YAJIN}
REGIONS_BY_PAGE = {
PAGE_HUIYUAN: ('rules', 'benefits', 'advantages'),
PAGE_YAJIN: ('rules', 'benefits'),
}
REGION_SORT = {
'rules': 10,
'benefits': 20,
'advantages': 30,
}
def normalize_page_key(page_key):
key = (page_key or '').strip().lower()
return key if key in PAGE_KEYS else ''
def normalize_region_key(page_key, region_key):
key = (region_key or '').strip().lower()
allowed = REGIONS_BY_PAGE.get(page_key) or ()
return key if key in allowed else ''
def _as_items_list(raw):
if raw is None:
return []
if isinstance(raw, str):
try:
raw = json.loads(raw)
except (TypeError, ValueError):
return []
if not isinstance(raw, list):
return []
out = []
for item in raw:
if isinstance(item, dict):
out.append(item)
elif isinstance(item, str) and item.strip():
out.append({'text': item.strip()})
return out
def _normalize_items_for_region(region_key, items):
"""按区域清洗条目字段,去掉空条目。"""
cleaned = []
for item in _as_items_list(items):
if region_key == 'rules':
text = (item.get('text') or '').strip()
if text:
cleaned.append({'text': text})
elif region_key == 'benefits':
# 会员权益title/desc履约金权益label
title = (item.get('title') or '').strip()
label = (item.get('label') or '').strip()
desc = (item.get('desc') or '').strip()
icon_url = (item.get('icon_url') or '').strip()
accent = (item.get('accent_color') or '').strip()
row = {}
if title:
row['title'] = title
if label:
row['label'] = label
if desc:
row['desc'] = desc
if icon_url:
row['icon_url'] = icon_url
if accent:
row['accent_color'] = accent
if row.get('title') or row.get('label') or row.get('desc'):
cleaned.append(row)
elif region_key == 'advantages':
title = (item.get('title') or '').strip()
desc = (item.get('desc') or '').strip()
icon_url = (item.get('icon_url') or '').strip()
if not (title or desc):
continue
row = {}
if title:
row['title'] = title
if desc:
row['desc'] = desc
if icon_url:
row['icon_url'] = icon_url
cleaned.append(row)
return cleaned
def _row_to_region_payload(row, for_admin=False):
items = _as_items_list(row.items_json)
payload = {
'title': (row.title or '').strip(),
'items': items,
'enabled': bool(row.enabled),
'sort_order': int(row.sort_order or 0),
}
if for_admin:
payload['region_key'] = row.region_key
payload['page_key'] = row.page_key
return payload
def get_public_regions(club_id, page_key):
"""小程序只读:仅返回 enabled 且 items 非空的区域。"""
page_key = normalize_page_key(page_key)
if not page_key or not club_id:
return {}
rows = (
ClubRechargePageCopy.query
.filter(club_id=club_id, page_key=page_key, enabled=True)
.order_by('sort_order', 'id')
)
regions = {}
for row in rows:
items = _as_items_list(row.items_json)
if not items:
continue
regions[row.region_key] = {
'title': (row.title or '').strip(),
'items': items,
}
return regions
def get_admin_regions(club_id, page_key):
"""后台:返回该页全部区域(含未启用/空),缺省区域补空壳。"""
page_key = normalize_page_key(page_key)
if not page_key or not club_id:
return {}
allowed = REGIONS_BY_PAGE[page_key]
existing = {
row.region_key: row
for row in ClubRechargePageCopy.query.filter(club_id=club_id, page_key=page_key)
}
regions = {}
for rk in allowed:
row = existing.get(rk)
if row:
regions[rk] = _row_to_region_payload(row, for_admin=True)
else:
regions[rk] = {
'region_key': rk,
'page_key': page_key,
'title': '',
'items': [],
'enabled': False,
'sort_order': REGION_SORT.get(rk, 0),
}
return regions
def save_region(club_id, page_key, region_key, *, title='', items=None, enabled=True, sort_order=None):
page_key = normalize_page_key(page_key)
region_key = normalize_region_key(page_key, region_key)
if not club_id or not page_key or not region_key:
return None, 'page_key 或 region_key 无效'
items_clean = _normalize_items_for_region(region_key, items)
title = (title or '').strip()[:64]
enabled = bool(enabled)
if sort_order is None:
sort_order = REGION_SORT.get(region_key, 0)
try:
sort_order = int(sort_order)
except (TypeError, ValueError):
sort_order = REGION_SORT.get(region_key, 0)
row = ClubRechargePageCopy.query.filter(
club_id=club_id, page_key=page_key, region_key=region_key,
).first()
if row:
row.title = title
row.items_json = items_clean
row.enabled = enabled
row.sort_order = sort_order
row.save(update_fields=['title', 'items_json', 'enabled', 'sort_order', 'UpdateTime'])
else:
row = ClubRechargePageCopy.query.create(
club_id=club_id,
page_key=page_key,
region_key=region_key,
title=title,
items_json=items_clean,
enabled=enabled,
sort_order=sort_order,
)
return _row_to_region_payload(row, for_admin=True), None
def club_id_for_mini(request):
return resolve_club_id_from_request(request)
def club_id_for_admin(request, user):
return resolve_effective_club_id(request, user)