diff --git a/dingdan/tongzhi_tasks.py b/dingdan/tongzhi_tasks.py index a884fa1..05186a1 100644 --- a/dingdan/tongzhi_tasks.py +++ b/dingdan/tongzhi_tasks.py @@ -1,7 +1,8 @@ -# dingdan/tongzhi_tasks.py +# dingdan/tongzhi_tasks.py import logging from django.conf import settings +from utils.xcx_sys_config import wx_cfg from a_long_dianjing.celery import app from utils.weixin_broadcast import WeixinBroadcastSender @@ -24,7 +25,7 @@ def dingdan_guangbo(self, order_info): """ Celery:并发向所有关注服务号的用户推送订单模板消息 """ - if not getattr(settings, 'WEIXIN_BROADCAST_ENABLED', True): + if not wx_cfg.WEIXIN_BROADCAST_ENABLED: return {'success': True, 'msg': '广播已关闭'} order_id = order_info.get('dingdan_id') diff --git a/dingdan/views.py b/dingdan/views.py index 4c5234e..8b3dce9 100644 --- a/dingdan/views.py +++ b/dingdan/views.py @@ -1,4 +1,4 @@ -import os +import os from django.db.models import F from django.shortcuts import render from rest_framework.permissions import AllowAny # 新增这行 @@ -16,6 +16,7 @@ import uuid from decimal import Decimal from django.db import transaction from django.conf import settings +from utils.xcx_sys_config import wx_cfg from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import permissions, status @@ -256,10 +257,10 @@ class CreateOrderView(APIView): """ try: # 微信支付配置 - APPID = getattr(settings, 'WEIXIN_APPID', '') - MCHID = getattr(settings, 'WEIXIN_MCHID', '') - KEY = getattr(settings, 'WEIXIN_SHANGHUMIYAO', '') - NOTIFY_URL = getattr(settings, 'WEIXIN_NOTIFY_URL', '') + APPID = wx_cfg.WEIXIN_APPID + MCHID = wx_cfg.WEIXIN_MCHID + KEY = wx_cfg.WEIXIN_SHANGHUMIYAO + NOTIFY_URL = wx_cfg.WEIXIN_NOTIFY_URL if not all([APPID, MCHID, KEY, NOTIFY_URL]): raise Exception('微信支付配置不完整') @@ -428,6 +429,7 @@ from decimal import Decimal from django.db import transaction, models from django.core.cache import cache from django.conf import settings +from utils.xcx_sys_config import wx_cfg from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import permissions, status @@ -505,7 +507,7 @@ class WechatPayNotifyView(APIView): # 按key排序 sorted_keys = sorted(data.keys()) stringA = '&'.join([f"{k}={data[k]}" for k in sorted_keys]) - stringSignTemp = f"{stringA}&key={settings.WEIXIN_SHANGHUMIYAO}" + stringSignTemp = f"{stringA}&key={wx_cfg.WEIXIN_SHANGHUMIYAO}" calc_sign = hashlib.md5(stringSignTemp.encode('utf-8')).hexdigest().upper() logger.debug(f"计算签名: {calc_sign}, 微信签名: {sign}") return calc_sign == sign @@ -828,9 +830,9 @@ class PaymentVerifyView(APIView): 调用微信支付订单查询接口,返回 (trade_state, transaction_id) 若查询失败,抛出异常 """ - appid = settings.WEIXIN_APPID - mch_id = settings.WEIXIN_MCHID - key = settings.WEIXIN_SHANGHUMIYAO + appid = wx_cfg.WEIXIN_APPID + mch_id = wx_cfg.WEIXIN_MCHID + key = wx_cfg.WEIXIN_SHANGHUMIYAO nonce_str = self._generate_nonce_str() params = { @@ -917,6 +919,7 @@ from decimal import Decimal import requests from django.conf import settings +from utils.xcx_sys_config import wx_cfg from django.db import transaction from rest_framework.views import APIView from rest_framework.response import Response @@ -1089,8 +1092,8 @@ class VirtualCreateOrderView(APIView): """ url = 'https://api.weixin.qq.com/sns/jscode2session' params = { - 'appid': settings.WEIXIN_APPID, - 'secret': settings.WEIXIN_SECRET, + 'appid': wx_cfg.WEIXIN_APPID, + 'secret': wx_cfg.WEIXIN_SECRET, 'js_code': code, 'grant_type': 'authorization_code' } @@ -1378,7 +1381,7 @@ class VirtualPayNotifyView(APIView): print(f"\n{'='*60}") print(f"[获取AccessToken-步骤1] 开始获取 access_token") - print(f"[获取AccessToken-步骤1] AppID: {settings.WEIXIN_APPID}") + print(f"[获取AccessToken-步骤1] AppID: {wx_cfg.WEIXIN_APPID}") print(f"{'='*60}") cache_key = "virtual_payment_access_token" @@ -1398,8 +1401,8 @@ class VirtualPayNotifyView(APIView): "https://api.weixin.qq.com/cgi-bin/token", params={ "grant_type": "client_credential", - "appid": settings.WEIXIN_APPID, - "secret": settings.WEIXIN_SECRET, + "appid": wx_cfg.WEIXIN_APPID, + "secret": wx_cfg.WEIXIN_SECRET, }, timeout=10, ) @@ -1816,8 +1819,8 @@ class VirtualPaymentVerifyView(APIView): "https://api.weixin.qq.com/cgi-bin/token", params={ "grant_type": "client_credential", - "appid": settings.WEIXIN_APPID, - "secret": settings.WEIXIN_SECRET, + "appid": wx_cfg.WEIXIN_APPID, + "secret": wx_cfg.WEIXIN_SECRET, }, timeout=10, ) @@ -3584,6 +3587,7 @@ class ShangjiaTuikuanShenqingView(APIView): import threading import requests from django.conf import settings +from utils.xcx_sys_config import wx_cfg def send_refund_notify(): url = f"{settings.PARTNER_BASE_URL}{settings.PARTNER_REFUND_NOTIFY_URL}" @@ -4103,6 +4107,7 @@ from rest_framework.permissions import IsAuthenticated from django.utils import timezone from django.db.models import F from django.conf import settings +from utils.xcx_sys_config import wx_cfg import json import requests from datetime import datetime @@ -5065,6 +5070,7 @@ from tencentcloud.common import credential from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException from tencentcloud.sts.v20180813 import sts_client, models from django.conf import settings +from utils.xcx_sys_config import wx_cfg from rest_framework.views import APIView from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response @@ -5420,6 +5426,7 @@ from tencentcloud.common import credential from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException from tencentcloud.sts.v20180813 import sts_client, models from django.conf import settings +from utils.xcx_sys_config import wx_cfg from rest_framework.views import APIView from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response @@ -6923,6 +6930,7 @@ import xml.etree.ElementTree as ET from decimal import Decimal from django.db import transaction from django.conf import settings +from utils.xcx_sys_config import wx_cfg from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import status @@ -7152,13 +7160,13 @@ class AdTongYiTuiKuanPingTai(APIView): # 构造请求参数 refund_data = { - 'appid': settings.WEIXIN_APPID, - 'mch_id': settings.WEIXIN_MCHID, + 'appid': wx_cfg.WEIXIN_APPID, + 'mch_id': wx_cfg.WEIXIN_MCHID, 'nonce_str': hashlib.md5(str(time.time()).encode()).hexdigest(), 'out_refund_no': refund_id, 'total_fee': total_fee_in_fen, 'refund_fee': total_fee_in_fen, - 'op_user_id': settings.WEIXIN_MCHID, + 'op_user_id': wx_cfg.WEIXIN_MCHID, 'refund_account': 'REFUND_SOURCE_UNSETTLED_FUNDS' } # 优先使用 transaction_id,否则用 out_trade_no @@ -7170,7 +7178,7 @@ class AdTongYiTuiKuanPingTai(APIView): # 生成签名 sorted_params = sorted(refund_data.items()) sign_string = '&'.join([f"{k}={v}" for k, v in sorted_params if v]) - sign_string += f"&key={settings.WEIXIN_SHANGHUMIYAO}" + sign_string += f"&key={wx_cfg.WEIXIN_SHANGHUMIYAO}" sign = hashlib.md5(sign_string.encode()).hexdigest().upper() refund_data['sign'] = sign @@ -7180,7 +7188,7 @@ class AdTongYiTuiKuanPingTai(APIView): url, data=xml_data.encode('utf-8'), headers={'Content-Type': 'application/xml'}, - cert=(settings.WEIXIN_CERT_PATH, settings.WEIXIN_KEY_PATH), + cert=(wx_cfg.WEIXIN_CERT_PATH, wx_cfg.WEIXIN_KEY_PATH), timeout=30 ) @@ -7229,6 +7237,7 @@ from rest_framework.parsers import JSONParser from rest_framework import status from django.db import transaction from django.conf import settings +from utils.xcx_sys_config import wx_cfg import logging import time import random @@ -7251,6 +7260,7 @@ from rest_framework.parsers import JSONParser from rest_framework import status from django.db import transaction from django.conf import settings +from utils.xcx_sys_config import wx_cfg import logging import time import random @@ -7530,21 +7540,21 @@ class AdTongYiTuiKuanPingTai(APIView): # 准备微信退款参数 refund_data = { - 'appid': settings.WEIXIN_APPID, - 'mch_id': settings.WEIXIN_MCHID, + 'appid': wx_cfg.WEIXIN_APPID, + 'mch_id': wx_cfg.WEIXIN_MCHID, 'nonce_str': hashlib.md5(str(time.time()).encode()).hexdigest(), 'out_trade_no': dingdan_id, # 商户订单号(我们的订单ID) 'out_refund_no': refund_id, # 商户退款单号 'total_fee': total_fee_in_fen, # 订单总金额(分) 'refund_fee': total_fee_in_fen, # 退款金额(分),全额退款 - 'op_user_id': settings.WEIXIN_MCHID, # 操作员ID,默认为商户号 + 'op_user_id': wx_cfg.WEIXIN_MCHID, # 操作员ID,默认为商户号 'refund_account': 'REFUND_SOURCE_UNSETTLED_FUNDS' # 退款资金来源 } # 生成签名 sorted_params = sorted(refund_data.items()) sign_string = '&'.join([f"{key}={value}" for key, value in sorted_params if value]) - sign_string += f"&key={settings.WEIXIN_SHANGHUMIYAO}" + sign_string += f"&key={wx_cfg.WEIXIN_SHANGHUMIYAO}" sign = hashlib.md5(sign_string.encode()).hexdigest().upper() refund_data['sign'] = sign @@ -7563,7 +7573,7 @@ class AdTongYiTuiKuanPingTai(APIView): weixin_refund_url, data=xml_data.encode('utf-8'), headers=headers, - cert=(settings.WEIXIN_CERT_PATH, settings.WEIXIN_KEY_PATH), + cert=(wx_cfg.WEIXIN_CERT_PATH, wx_cfg.WEIXIN_KEY_PATH), timeout=timeout ) diff --git a/houtai/view.py b/houtai/view.py index 7b1a258..453a781 100644 --- a/houtai/view.py +++ b/houtai/view.py @@ -1,4 +1,4 @@ -import threading +import threading import traceback from calendar import monthrange @@ -27,6 +27,7 @@ from houtai.utils import verify_kefu_permission import xmltodict from django.conf import settings +from utils.xcx_sys_config import wx_cfg from dingdan.models import Dashoutupian, Chufajilu, PreSettlement, OrderDashouHistory # apps/houtai/views.py 继续添加 @@ -646,6 +647,7 @@ import xmltodict from decimal import Decimal from django.conf import settings +from utils.xcx_sys_config import wx_cfg from django.db import transaction from django.core.cache import cache from rest_framework.views import APIView @@ -1055,8 +1057,8 @@ class CrossPlatformRefundView(APIView): "https://api.weixin.qq.com/cgi-bin/token", params={ "grant_type": "client_credential", - "appid": settings.WEIXIN_APPID, - "secret": settings.WEIXIN_SECRET, + "appid": wx_cfg.WEIXIN_APPID, + "secret": wx_cfg.WEIXIN_SECRET, }, timeout=10, ) @@ -1483,11 +1485,11 @@ class CrossPlatformRefundView(APIView): def _call_wechat_refund(self, order): """调用微信支付退款接口,返回 {'code':0, 'msg':'success'} 或错误信息""" # 获取微信支付配置 - appid = getattr(settings, 'WEIXIN_APPID', '') - mch_id = getattr(settings, 'WEIXIN_MCHID', '') - key = getattr(settings, 'WEIXIN_SHANGHUMIYAO', '') - cert_path = getattr(settings, 'WEIXIN_CERT_PATH', '') - key_path = getattr(settings, 'WEIXIN_KEY_PATH', '') + appid = wx_cfg.WEIXIN_APPID + mch_id = wx_cfg.WEIXIN_MCHID + key = wx_cfg.WEIXIN_SHANGHUMIYAO + cert_path = wx_cfg.WEIXIN_CERT_PATH + key_path = wx_cfg.WEIXIN_KEY_PATH if not all([appid, mch_id, key, cert_path, key_path]): missing = [k for k, v in {'appid': appid, 'mch_id': mch_id, 'key': key, 'cert_path': cert_path, 'key_path': key_path}.items() if not v] @@ -10891,6 +10893,7 @@ class ZxkfghdsView(APIView): from django.conf import settings +from utils.xcx_sys_config import wx_cfg from django.db.models import ObjectDoesNotExist from rest_framework import status from rest_framework.parsers import JSONParser diff --git a/peizhi/migrations/0016_xcx_sys_peizhi_page_ziyuan.py b/peizhi/migrations/0016_xcx_sys_peizhi_page_ziyuan.py new file mode 100644 index 0000000..fb8db55 --- /dev/null +++ b/peizhi/migrations/0016_xcx_sys_peizhi_page_ziyuan.py @@ -0,0 +1,88 @@ +# Generated manually for XcxSysPeizhi & XcxPageZiyuan + +from django.conf import settings +from django.db import migrations, models + + +def seed_xcx_config(apps, schema_editor): + XcxSysPeizhi = apps.get_model('peizhi', 'XcxSysPeizhi') + if XcxSysPeizhi.objects.exists(): + return + XcxSysPeizhi.objects.create( + id=1, + weixin_appid=getattr(settings, 'WEIXIN_APPID', ''), + weixin_secret=getattr(settings, 'WEIXIN_SECRET', ''), + weixin_official_appid=getattr(settings, 'WEIXIN_OFFICIAL_APPID', ''), + weixin_official_secret=getattr(settings, 'WEIXIN_OFFICIAL_SECRET', ''), + weixin_official_token=getattr(settings, 'WEIXIN_OFFICIAL_TOKEN', ''), + weixin_official_encoding_aes_key=getattr(settings, 'WEIXIN_OFFICIAL_ENCODING_AES_KEY', ''), + weixin_template_id=getattr(settings, 'WEIXIN_TEMPLATE_ID', ''), + weixin_template_max_per_minute=getattr(settings, 'WEIXIN_TEMPLATE_MAX_PER_MINUTE', 950), + weixin_template_batch_size=getattr(settings, 'WEIXIN_TEMPLATE_BATCH_SIZE', 100), + weixin_broadcast_enabled=getattr(settings, 'WEIXIN_BROADCAST_ENABLED', True), + weixin_broadcast_workers=getattr(settings, 'WEIXIN_BROADCAST_WORKERS', 30), + weixin_mchid=getattr(settings, 'WEIXIN_MCHID', ''), + weixin_shanghu_miyao=getattr(settings, 'WEIXIN_SHANGHUMIYAO', ''), + weixin_notify_url=getattr(settings, 'WEIXIN_NOTIFY_URL', ''), + weixin_cert_path=getattr(settings, 'WEIXIN_CERT_PATH', ''), + weixin_key_path=getattr(settings, 'WEIXIN_KEY_PATH', ''), + notify_base_url='https://wenhe.nmslb.com/hqhd', + ) + + +class Migration(migrations.Migration): + + dependencies = [ + ('peizhi', '0015_huashuconfig'), + ] + + operations = [ + migrations.CreateModel( + name='XcxSysPeizhi', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('weixin_appid', models.CharField(blank=True, default='', max_length=64, verbose_name='小程序AppID')), + ('weixin_secret', models.CharField(blank=True, default='', max_length=128, verbose_name='小程序Secret')), + ('weixin_official_appid', models.CharField(blank=True, default='', max_length=64, verbose_name='服务号AppID')), + ('weixin_official_secret', models.CharField(blank=True, default='', max_length=128, verbose_name='服务号Secret')), + ('weixin_official_token', models.CharField(blank=True, default='', max_length=128, verbose_name='服务号Token')), + ('weixin_official_encoding_aes_key', models.CharField(blank=True, default='', max_length=128, verbose_name='服务号EncodingAESKey')), + ('weixin_template_id', models.CharField(blank=True, default='', max_length=128, verbose_name='模板消息ID')), + ('weixin_template_max_per_minute', models.PositiveIntegerField(default=950, verbose_name='模板消息每分钟上限')), + ('weixin_template_batch_size', models.PositiveIntegerField(default=100, verbose_name='模板消息批大小')), + ('weixin_broadcast_enabled', models.BooleanField(default=True, verbose_name='服务号广播开关')), + ('weixin_broadcast_workers', models.PositiveIntegerField(default=30, verbose_name='广播并发线程数')), + ('weixin_mchid', models.CharField(blank=True, default='', max_length=32, verbose_name='微信支付商户号')), + ('weixin_shanghu_miyao', models.CharField(blank=True, default='', max_length=128, verbose_name='商户密钥')), + ('weixin_notify_url', models.CharField(blank=True, default='', max_length=300, verbose_name='支付回调URL')), + ('weixin_cert_path', models.CharField(blank=True, default='', max_length=500, verbose_name='退款证书路径')), + ('weixin_key_path', models.CharField(blank=True, default='', max_length=500, verbose_name='退款密钥路径')), + ('notify_base_url', models.CharField(blank=True, default='https://wenhe.nmslb.com/hqhd', max_length=300, verbose_name='业务回调根URL')), + ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ], + options={ + 'verbose_name': '小程序系统配置', + 'verbose_name_plural': '小程序系统配置', + 'db_table': 'xcx_sys_peizhi', + }, + ), + migrations.CreateModel( + name='XcxPageZiyuan', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('ziyuan_zu', models.CharField(db_index=True, max_length=32, verbose_name='资源组')), + ('ziyuan_key', models.CharField(max_length=64, verbose_name='资源键')), + ('ziyuan_path', models.CharField(max_length=500, verbose_name='OSS相对路径')), + ('miaoshu', models.CharField(blank=True, default='', max_length=200, verbose_name='说明')), + ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ], + options={ + 'verbose_name': '小程序页面资源', + 'verbose_name_plural': '小程序页面资源', + 'db_table': 'xcx_page_ziyuan', + 'indexes': [models.Index(fields=['ziyuan_zu'], name='xcx_page_zu_idx')], + 'unique_together': {('ziyuan_zu', 'ziyuan_key')}, + }, + ), + migrations.RunPython(seed_xcx_config, migrations.RunPython.noop), + ] diff --git a/peizhi/models.py b/peizhi/models.py index 79177ce..dd0d87e 100644 --- a/peizhi/models.py +++ b/peizhi/models.py @@ -669,4 +669,56 @@ class HuashuConfig(models.Model): ordering = ['scene_key', '-priority', 'sort_order', 'id'] def __str__(self): - return f'{self.get_scene_key_display()} - {self.get_item_type_display()}' \ No newline at end of file + return f'{self.get_scene_key_display()} - {self.get_item_type_display()}' + + +class XcxSysPeizhi(models.Model): + """小程序/微信系统配置(单例表,后台可改,优先于 settings.py)""" + + weixin_appid = models.CharField(max_length=64, blank=True, default='', verbose_name='小程序AppID') + weixin_secret = models.CharField(max_length=128, blank=True, default='', verbose_name='小程序Secret') + weixin_official_appid = models.CharField(max_length=64, blank=True, default='', verbose_name='服务号AppID') + weixin_official_secret = models.CharField(max_length=128, blank=True, default='', verbose_name='服务号Secret') + weixin_official_token = models.CharField(max_length=128, blank=True, default='', verbose_name='服务号Token') + weixin_official_encoding_aes_key = models.CharField( + max_length=128, blank=True, default='', verbose_name='服务号EncodingAESKey' + ) + weixin_template_id = models.CharField(max_length=128, blank=True, default='', verbose_name='模板消息ID') + weixin_template_max_per_minute = models.PositiveIntegerField(default=950, verbose_name='模板消息每分钟上限') + weixin_template_batch_size = models.PositiveIntegerField(default=100, verbose_name='模板消息批大小') + weixin_broadcast_enabled = models.BooleanField(default=True, verbose_name='服务号广播开关') + weixin_broadcast_workers = models.PositiveIntegerField(default=30, verbose_name='广播并发线程数') + weixin_mchid = models.CharField(max_length=32, blank=True, default='', verbose_name='微信支付商户号') + weixin_shanghu_miyao = models.CharField(max_length=128, blank=True, default='', verbose_name='商户密钥') + weixin_notify_url = models.CharField(max_length=300, blank=True, default='', verbose_name='支付回调URL') + weixin_cert_path = models.CharField(max_length=500, blank=True, default='', verbose_name='退款证书路径') + weixin_key_path = models.CharField(max_length=500, blank=True, default='', verbose_name='退款密钥路径') + notify_base_url = models.CharField( + max_length=300, blank=True, default='https://wenhe.nmslb.com/hqhd', verbose_name='业务回调根URL' + ) + update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间') + + class Meta: + db_table = 'xcx_sys_peizhi' + verbose_name = '小程序系统配置' + verbose_name_plural = verbose_name + + +class XcxPageZiyuan(models.Model): + """小程序页面背景图/图标(按组+键唯一)""" + + ziyuan_zu = models.CharField(max_length=32, db_index=True, verbose_name='资源组') + ziyuan_key = models.CharField(max_length=64, verbose_name='资源键') + ziyuan_path = models.CharField(max_length=500, verbose_name='OSS相对路径') + miaoshu = models.CharField(max_length=200, blank=True, default='', verbose_name='说明') + update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间') + + class Meta: + db_table = 'xcx_page_ziyuan' + verbose_name = '小程序页面资源' + verbose_name_plural = verbose_name + unique_together = [['ziyuan_zu', 'ziyuan_key']] + indexes = [models.Index(fields=['ziyuan_zu'])] + + def __str__(self): + return f'{self.ziyuan_zu}.{self.ziyuan_key}' \ No newline at end of file diff --git a/peizhi/urls.py b/peizhi/urls.py index 7bb6dda..cdcf770 100644 --- a/peizhi/urls.py +++ b/peizhi/urls.py @@ -7,6 +7,10 @@ from .views import ShangpinGonggaoView, AdminUpdateConfigView, AdminUploadImageV GuanZhuAListView, ZuzhangHaibaoView,GetDynamicConfigView, PopupConfigView, GetWithdrawModeView,\ CheckPhoneAuthView,ShangjiaLianjieListView from .views_huashu import HuashuQueryView, HuashuMatchView +from .xcx_config_views import ( + XcxSysPeizhiQueryView, XcxSysPeizhiUpdateView, + XcxPageZiyuanQueryView, XcxPageZiyuanUpdateView, XcxPageZiyuanUploadView, +) urlpatterns = [ path('shangpingonggao/', ShangpinGonggaoView.as_view(), name='商品公告轮播获取'), @@ -43,4 +47,9 @@ urlpatterns = [ path('hqsjlslj', ShangjiaLianjieListView.as_view(), name='商家获取生成链接'), path('huashuhq', HuashuQueryView.as_view(), name='小程序获取话术配置'), path('huashu_match', HuashuMatchView.as_view(), name='小程序客服自动回复匹配'), + path('xcxsyshq', XcxSysPeizhiQueryView.as_view(), name='管理员获取小程序系统配置'), + path('xcxsysgx', XcxSysPeizhiUpdateView.as_view(), name='管理员更新小程序系统配置'), + path('xcxyzycx', XcxPageZiyuanQueryView.as_view(), name='管理员查询页面资源'), + path('xcxyzgx', XcxPageZiyuanUpdateView.as_view(), name='管理员更新页面资源'), + path('xcxyzsc', XcxPageZiyuanUploadView.as_view(), name='管理员上传页面资源'), ] \ No newline at end of file diff --git a/peizhi/views.py b/peizhi/views.py index 46d4fc1..1bcdb0e 100644 --- a/peizhi/views.py +++ b/peizhi/views.py @@ -1,4 +1,4 @@ -# peizhi/views.py +# peizhi/views.py from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import status @@ -19,6 +19,7 @@ from rest_framework.views import APIView from rest_framework.permissions import AllowAny from rest_framework.parsers import JSONParser from .models import ClubConfig +from utils.page_ziyuan_service import build_paihang_assets_for_api, build_page_assets_for_api class GetDynamicConfigView(APIView): @@ -73,6 +74,8 @@ class GetDynamicConfigView(APIView): # 其他配置可在此扩展 # 构造返回数据 + paihang_assets = build_paihang_assets_for_api() + page_assets = build_page_assets_for_api() data = { "code": 0, "data": { @@ -89,14 +92,9 @@ class GetDynamicConfigView(APIView): "otherConfig": { "morentouxiang": morentouxiang, "dashouguize": dashouguize, - "paihangAssets": { - "basePath": "beijing/paihangbang/", - "pageBg": "page-bg.jpg", - "cardBg": "list-card-bg.png", - "emptyIcon": "empty.png", - "refreshIcon": "icon-refresh.png", - }, + "paihangAssets": paihang_assets, }, + "pageAssets": page_assets, "kefu": { "link": kefu_link, "enterpriseId": kefu_enterprise_id @@ -2017,6 +2015,7 @@ import secrets import logging from decimal import Decimal from django.conf import settings +from utils.xcx_sys_config import wx_cfg from django.db import transaction from django.db.models import F from django.utils import timezone @@ -4307,8 +4306,8 @@ class GuanshiQRCodeView(APIView): """ cache_key = 'wechat_access_token' lock_key = 'wechat_access_token_lock' - appid = getattr(settings, 'WEIXIN_APPID', '') - secret = getattr(settings, 'WEIXIN_SECRET', '') + appid = wx_cfg.WEIXIN_APPID + secret = wx_cfg.WEIXIN_SECRET if not appid or not secret: logger.error("微信小程序 AppID 或 Secret 未配置") return None @@ -4489,8 +4488,8 @@ class GuanshiQRCodeView(APIView): if token: return token - appid = getattr(settings, 'WEIXIN_APPID', '') - secret = getattr(settings, 'WEIXIN_SECRET', '') + appid = wx_cfg.WEIXIN_APPID + secret = wx_cfg.WEIXIN_SECRET if not appid or not secret: logger.error("微信小程序配置缺失") return None @@ -4770,8 +4769,8 @@ from yonghu.models import UserZuzhang if token: return token - appid = getattr(settings, 'WEIXIN_APPID', '') - secret = getattr(settings, 'WEIXIN_SECRET', '') + appid = wx_cfg.WEIXIN_APPID + secret = wx_cfg.WEIXIN_SECRET if not appid or not secret: logger.error("微信小程序配置缺失") return None @@ -4971,8 +4970,8 @@ class ZuzhangHaibaoView(APIView): if token: return token - appid = getattr(settings, 'WEIXIN_APPID', '') - secret = getattr(settings, 'WEIXIN_SECRET', '') + appid = wx_cfg.WEIXIN_APPID + secret = wx_cfg.WEIXIN_SECRET if not appid or not secret: logger.error("微信小程序配置缺失") return None diff --git a/peizhi/xcx_config_views.py b/peizhi/xcx_config_views.py new file mode 100644 index 0000000..08de07f --- /dev/null +++ b/peizhi/xcx_config_views.py @@ -0,0 +1,213 @@ +"""小程序系统配置 & 页面资源 — 后台管理接口""" +import logging + +from django.db import transaction +from rest_framework import permissions, status +from rest_framework.parsers import FormParser, JSONParser, MultiPartParser +from rest_framework.response import Response +from rest_framework.views import APIView + +from peizhi.models import XcxSysPeizhi, XcxPageZiyuan +from utils.oss_utils import upload_to_oss, validate_image +from utils.page_ziyuan_service import DEFAULT_ZIYUAN, build_page_assets_for_api, clear_page_ziyuan_cache +from utils.xcx_sys_config import clear_xcx_sys_peizhi_cache, wx_cfg +from yonghu.models import AdminProfile + +logger = logging.getLogger(__name__) + + +def _verify_admin(request, zhanghao): + if not zhanghao: + return False, Response({'code': 1, 'msg': '账号不能为空', 'data': None}, status=status.HTTP_400_BAD_REQUEST) + user_main = request.user + if user_main.phone != zhanghao or user_main.user_type != 'admin': + return False, Response({'code': 1, 'msg': '无权限', 'data': None}, status=status.HTTP_401_UNAUTHORIZED) + try: + user_main.admin_profile + except AdminProfile.DoesNotExist: + return False, Response({'code': 1, 'msg': '无权限', 'data': None}, status=status.HTTP_401_UNAUTHORIZED) + return True, None + + +def _mask_secret(val): + if not val: + return '' + if len(val) <= 8: + return '****' + return val[:4] + '****' + val[-4:] + + +class XcxSysPeizhiQueryView(APIView): + """POST /peizhi/xcxsyshq — 管理员获取小程序系统配置""" + + permission_classes = [permissions.IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + ok, err = _verify_admin(request, request.data.get('zhanghao')) + if not ok: + return err + row = XcxSysPeizhi.objects.first() + data = { + 'weixin_appid': row.weixin_appid if row else wx_cfg.WEIXIN_APPID, + 'weixin_secret': _mask_secret(row.weixin_secret if row else wx_cfg.WEIXIN_SECRET), + 'weixin_official_appid': row.weixin_official_appid if row else wx_cfg.WEIXIN_OFFICIAL_APPID, + 'weixin_official_secret': _mask_secret( + row.weixin_official_secret if row else wx_cfg.WEIXIN_OFFICIAL_SECRET + ), + 'weixin_official_token': row.weixin_official_token if row else wx_cfg.WEIXIN_OFFICIAL_TOKEN, + 'weixin_official_encoding_aes_key': _mask_secret( + row.weixin_official_encoding_aes_key if row else wx_cfg.WEIXIN_OFFICIAL_ENCODING_AES_KEY + ), + 'weixin_template_id': row.weixin_template_id if row else wx_cfg.WEIXIN_TEMPLATE_ID, + 'weixin_template_max_per_minute': ( + row.weixin_template_max_per_minute if row else wx_cfg.WEIXIN_TEMPLATE_MAX_PER_MINUTE + ), + 'weixin_template_batch_size': ( + row.weixin_template_batch_size if row else wx_cfg.WEIXIN_TEMPLATE_BATCH_SIZE + ), + 'weixin_broadcast_enabled': ( + row.weixin_broadcast_enabled if row else wx_cfg.WEIXIN_BROADCAST_ENABLED + ), + 'weixin_broadcast_workers': row.weixin_broadcast_workers if row else wx_cfg.WEIXIN_BROADCAST_WORKERS, + 'weixin_mchid': row.weixin_mchid if row else wx_cfg.WEIXIN_MCHID, + 'weixin_shanghu_miyao': _mask_secret(row.weixin_shanghu_miyao if row else wx_cfg.WEIXIN_SHANGHUMIYAO), + 'weixin_notify_url': row.weixin_notify_url if row else wx_cfg.WEIXIN_NOTIFY_URL, + 'weixin_cert_path': row.weixin_cert_path if row else wx_cfg.WEIXIN_CERT_PATH, + 'weixin_key_path': row.weixin_key_path if row else wx_cfg.WEIXIN_KEY_PATH, + 'notify_base_url': row.notify_base_url if row else wx_cfg.NOTIFY_BASE_URL, + 'from_database': bool(row), + } + return Response({'code': 0, 'msg': 'success', 'data': data}) + + +class XcxSysPeizhiUpdateView(APIView): + """POST /peizhi/xcxsysgx — 管理员更新小程序系统配置""" + + permission_classes = [permissions.IsAuthenticated] + parser_classes = [JSONParser] + + UPDATABLE = frozenset({ + 'weixin_appid', 'weixin_secret', 'weixin_official_appid', 'weixin_official_secret', + 'weixin_official_token', 'weixin_official_encoding_aes_key', 'weixin_template_id', + 'weixin_template_max_per_minute', 'weixin_template_batch_size', 'weixin_broadcast_enabled', + 'weixin_broadcast_workers', 'weixin_mchid', 'weixin_shanghu_miyao', 'weixin_notify_url', + 'weixin_cert_path', 'weixin_key_path', 'notify_base_url', + }) + + def post(self, request): + ok, err = _verify_admin(request, request.data.get('zhanghao')) + if not ok: + return err + payload = request.data.get('config') or request.data + with transaction.atomic(): + row, _ = XcxSysPeizhi.objects.get_or_create(id=1) + for key in self.UPDATABLE: + if key in payload and payload[key] is not None: + val = payload[key] + if key.endswith('_secret') or key.endswith('_miyao') or key.endswith('_aes_key'): + if val == '' or '****' in str(val): + continue + if key == 'weixin_broadcast_enabled': + row.weixin_broadcast_enabled = bool(val) + elif key in ('weixin_template_max_per_minute', 'weixin_template_batch_size', 'weixin_broadcast_workers'): + row.__setattr__(key, int(val)) + else: + row.__setattr__(key, str(val).strip()) + row.save() + clear_xcx_sys_peizhi_cache() + return Response({'code': 0, 'msg': '配置已更新', 'data': None}) + + +class XcxPageZiyuanQueryView(APIView): + """POST /peizhi/xcxyzycx — 管理员查询页面资源列表""" + + permission_classes = [permissions.IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + ok, err = _verify_admin(request, request.data.get('zhanghao')) + if not ok: + return err + zu = (request.data.get('ziyuan_zu') or '').strip() + merged = build_page_assets_for_api() + if zu: + groups = {zu: merged.get(zu, {})} + else: + groups = merged + items = [] + for gzu, keys in groups.items(): + for key, path in keys.items(): + items.append({ + 'ziyuan_zu': gzu, + 'ziyuan_key': key, + 'ziyuan_path': path, + 'miaoshu': DEFAULT_ZIYUAN.get(f'{gzu}.{key}', ''), + }) + return Response({'code': 0, 'msg': 'success', 'data': {'list': items, 'groups': groups}}) + + +class XcxPageZiyuanUpdateView(APIView): + """POST /peizhi/xcxyzgx — 管理员更新页面资源路径""" + + permission_classes = [permissions.IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + ok, err = _verify_admin(request, request.data.get('zhanghao')) + if not ok: + return err + items = request.data.get('items') or [] + if not isinstance(items, list) or not items: + return Response({'code': 1, 'msg': 'items 不能为空', 'data': None}, status=400) + with transaction.atomic(): + for item in items: + zu = (item.get('ziyuan_zu') or '').strip() + key = (item.get('ziyuan_key') or '').strip() + path = (item.get('ziyuan_path') or '').strip().lstrip('/') + if not zu or not key or not path: + continue + XcxPageZiyuan.objects.update_or_create( + ziyuan_zu=zu, ziyuan_key=key, + defaults={'ziyuan_path': path, 'miaoshu': item.get('miaoshu') or ''}, + ) + clear_page_ziyuan_cache() + return Response({'code': 0, 'msg': '资源已更新', 'data': None}) + + +class XcxPageZiyuanUploadView(APIView): + """POST /peizhi/xcxyzsc — 管理员上传页面资源图片""" + + permission_classes = [permissions.IsAuthenticated] + parser_classes = (MultiPartParser, FormParser) + + def post(self, request): + ok, err = _verify_admin(request, request.data.get('zhanghao')) + if not ok: + return err + zu = (request.data.get('ziyuan_zu') or '').strip() + key = (request.data.get('ziyuan_key') or '').strip() + image_file = request.FILES.get('image') + if not zu or not key: + return Response({'code': 1, 'msg': 'ziyuan_zu/ziyuan_key 必填', 'data': None}, status=400) + if not image_file: + return Response({'code': 2, 'msg': '请选择图片', 'data': None}, status=400) + is_valid, error_msg = validate_image(image_file) + if not is_valid: + return Response({'code': 3, 'msg': error_msg, 'data': None}, status=400) + + default_key = f'{zu}.{key}' + default_path = DEFAULT_ZIYUAN.get(default_key, f'beijing/{zu}/{key}.png') + oss_path = default_path + image_file.seek(0) + full_url = upload_to_oss(image_file, oss_path) + if not full_url: + return Response({'code': 4, 'msg': '上传失败', 'data': None}, status=500) + + with transaction.atomic(): + XcxPageZiyuan.objects.update_or_create( + ziyuan_zu=zu, ziyuan_key=key, + defaults={'ziyuan_path': oss_path}, + ) + clear_page_ziyuan_cache() + return Response({'code': 0, 'msg': '上传成功', 'data': {'ziyuan_path': oss_path}}) diff --git a/shangpin/views.py b/shangpin/views.py index 62137cc..99bb982 100644 --- a/shangpin/views.py +++ b/shangpin/views.py @@ -1,4 +1,4 @@ -# shangpin/views.py +# shangpin/views.py from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import status @@ -363,6 +363,7 @@ import hashlib import xml.etree.ElementTree as ET import requests from django.conf import settings +from utils.xcx_sys_config import wx_cfg from rest_framework.views import APIView from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response @@ -479,9 +480,9 @@ class YajinGoumai(APIView): def generate_wechat_pay_params(self, dingdanid, jine, openid, pay_type): """生成微信小程序支付参数""" # 获取配置 - APPID = settings.WEIXIN_APPID - MCHID = settings.WEIXIN_MCHID - KEY = settings.WEIXIN_SHANGHUMIYAO + APPID = wx_cfg.WEIXIN_APPID + MCHID = wx_cfg.WEIXIN_MCHID + KEY = wx_cfg.WEIXIN_SHANGHUMIYAO # 根据支付类型获取对应的配置 if pay_type == 'yajin': @@ -629,6 +630,7 @@ from shangpin.models import Czjilu import xml.etree.ElementTree as ET import hashlib from django.conf import settings +from utils.xcx_sys_config import wx_cfg import logging logger = logging.getLogger(__name__) @@ -806,7 +808,7 @@ class YajinHuitiao(View): # 拼接字符串 sign_string = '&'.join([f"{key}={value}" for key, value in sorted_params if value]) - sign_string += f'&key={settings.WEIXIN_SHANGHUMIYAO}' + sign_string += f'&key={wx_cfg.WEIXIN_SHANGHUMIYAO}' # 计算签名 calculated_sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper() @@ -1055,9 +1057,9 @@ class JifenBuchong(APIView): def generate_wechat_pay_params(self, dingdanid, jine, openid, pay_type): """生成微信支付参数(完整版,无省略)""" # 微信支付配置 - 从settings中获取 - APPID = settings.WEIXIN_APPID - MCHID = settings.WEIXIN_MCHID - KEY = settings.WEIXIN_SHANGHUMIYAO + APPID = wx_cfg.WEIXIN_APPID + MCHID = wx_cfg.WEIXIN_MCHID + KEY = wx_cfg.WEIXIN_SHANGHUMIYAO # 根据支付类型获取对应的配置 if pay_type == 'yajin': @@ -1359,7 +1361,7 @@ class JifenHuitiao(View): # 拼接字符串 sign_string = '&'.join([f"{key}={value}" for key, value in sorted_params if value]) - sign_string += f'&key={settings.WEIXIN_SHANGHUMIYAO}' + sign_string += f'&key={wx_cfg.WEIXIN_SHANGHUMIYAO}' # 计算签名 calculated_sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper() @@ -1541,6 +1543,7 @@ import hashlib import xml.etree.ElementTree as ET import requests from django.conf import settings +from utils.xcx_sys_config import wx_cfg import logging logger = logging.getLogger(__name__) @@ -1652,9 +1655,9 @@ class HuiyuanGoumai(APIView): def generate_wechat_pay_params(self, dingdanid, jine, openid, pay_type): """生成微信支付参数(完整版,无省略)""" # 微信支付配置 - 从settings中获取 - APPID = settings.WEIXIN_APPID - MCHID = settings.WEIXIN_MCHID - KEY = settings.WEIXIN_SHANGHUMIYAO + APPID = wx_cfg.WEIXIN_APPID + MCHID = wx_cfg.WEIXIN_MCHID + KEY = wx_cfg.WEIXIN_SHANGHUMIYAO # 根据支付类型获取对应的配置 if pay_type == 'yajin': @@ -2198,7 +2201,7 @@ class HuiyuanHuitiao(View): sorted_params = sorted(params.items(), key=lambda x: x[0]) sign_string = '&'.join([f"{key}={value}" for key, value in sorted_params if value]) - sign_string += f'&key={settings.WEIXIN_SHANGHUMIYAO}' + sign_string += f'&key={wx_cfg.WEIXIN_SHANGHUMIYAO}' calculated_sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper() return sign == calculated_sign except Exception as e: @@ -2228,6 +2231,7 @@ from shangpin.models import Czjilu, Huiyuan, Huiyuangoumai, Gsfenhong import xml.etree.ElementTree as ET import hashlib from django.conf import settings +from utils.xcx_sys_config import wx_cfg from django.utils import timezone from datetime import timedelta import logging @@ -2560,7 +2564,7 @@ class HuiyuanHuitiao(View): # 拼接字符串 sign_string = '&'.join([f"{key}={value}" for key, value in sorted_params if value]) - sign_string += f'&key={settings.WEIXIN_SHANGHUMIYAO}' + sign_string += f'&key={wx_cfg.WEIXIN_SHANGHUMIYAO}' # 计算签名 calculated_sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper() @@ -2763,6 +2767,7 @@ import xml.etree.ElementTree as ET import requests from decimal import Decimal from django.conf import settings +from utils.xcx_sys_config import wx_cfg from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import status @@ -2889,9 +2894,9 @@ class ShangjiaChongzhi(APIView): def generate_wechat_pay_params(self, dingdanid, jine, openid, pay_type): """生成微信小程序支付参数""" # 获取配置 - APPID = settings.WEIXIN_APPID - MCHID = settings.WEIXIN_MCHID - KEY = settings.WEIXIN_SHANGHUMIYAO + APPID = wx_cfg.WEIXIN_APPID + MCHID = wx_cfg.WEIXIN_MCHID + KEY = wx_cfg.WEIXIN_SHANGHUMIYAO # 根据支付类型获取对应的配置 if pay_type == 'shangjia': @@ -3189,7 +3194,7 @@ class ShangjiaHuitiao(View): # 拼接字符串 sign_string = '&'.join([f"{key}={value}" for key, value in sorted_params if value]) - sign_string += f'&key={settings.WEIXIN_SHANGHUMIYAO}' + sign_string += f'&key={wx_cfg.WEIXIN_SHANGHUMIYAO}' # 计算签名 calculated_sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper() diff --git a/utils/order_broadcast.py b/utils/order_broadcast.py index 45b93c6..5fe4328 100644 --- a/utils/order_broadcast.py +++ b/utils/order_broadcast.py @@ -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'): diff --git a/utils/page_ziyuan_service.py b/utils/page_ziyuan_service.py new file mode 100644 index 0000000..9e4ca04 --- /dev/null +++ b/utils/page_ziyuan_service.py @@ -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'), + } diff --git a/utils/weixin_broadcast.py b/utils/weixin_broadcast.py index 70be339..2d49059 100644 --- a/utils/weixin_broadcast.py +++ b/utils/weixin_broadcast.py @@ -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}') diff --git a/utils/xcx_sys_config.py b/utils/xcx_sys_config.py new file mode 100644 index 0000000..e82f029 --- /dev/null +++ b/utils/xcx_sys_config.py @@ -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() diff --git a/yonghu/paihang_views.py b/yonghu/paihang_views.py index 026960a..af3b765 100644 --- a/yonghu/paihang_views.py +++ b/yonghu/paihang_views.py @@ -51,7 +51,7 @@ ROLE_CONFIG = { 'shangjia': { 'model': ShangjiaRiTongji, 'id_field': 'yonghuid', - 'sort_field': 'jiesuan_jine', + 'sort_field': 'paifa_jine', 'fields': ('paifa_dingdan_shu', 'paifa_jine', 'jiesuan_dingdan_shu', 'jiesuan_jine'), 'int_fields': frozenset({'paifa_dingdan_shu', 'jiesuan_dingdan_shu'}), }, @@ -70,12 +70,9 @@ TODAY_PROFILE_CONFIG = { }, 'guanshi': { 'profile_model': UserGuanshi, - 'profile_sort_field': 'jinrichongzhi', - 'field_map': { - 'yaoqing_dashou_shu': None, - 'chongzhi_dashou_shu': 'jinrichongzhi', - 'shouru_zonge': None, - }, + 'profile_sort_field': None, + 'field_map': {}, + 'skip_profile_fallback': True, }, 'zuzhang': { 'profile_model': UserZuzhang, @@ -228,6 +225,9 @@ class PhbHqsjView(APIView): return rows today = date.today() if start_date == today: + today_cfg = TODAY_PROFILE_CONFIG.get(shenfen, {}) + if today_cfg.get('skip_profile_fallback'): + return rows return self._query_today_from_profiles(shenfen) return rows @@ -243,8 +243,12 @@ class PhbHqsjView(APIView): def _query_today_from_profiles(self, shenfen): cfg = ROLE_CONFIG[shenfen] today_cfg = TODAY_PROFILE_CONFIG[shenfen] + if today_cfg.get('skip_profile_fallback'): + return [] profile_model = today_cfg['profile_model'] profile_sort = today_cfg['profile_sort_field'] + if not profile_sort: + return [] field_map = today_cfg['field_map'] id_field = cfg['id_field'] fields = cfg['fields'] diff --git a/yonghu/views.py b/yonghu/views.py index e0efca1..ab8ba7c 100644 --- a/yonghu/views.py +++ b/yonghu/views.py @@ -1,10 +1,11 @@ -# yonghu/views.py +# yonghu/views.py import time import random import hashlib import requests from django.db import transaction, IntegrityError from django.conf import settings +from utils.xcx_sys_config import wx_cfg from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import status @@ -23,6 +24,7 @@ from rest_framework.permissions import AllowAny # 新增这行 from django.conf import settings +from utils.xcx_sys_config import wx_cfg from django.utils import timezone from django.db import transaction, IntegrityError from django.core.exceptions import ObjectDoesNotExist @@ -255,8 +257,8 @@ class WechatMiniProgramLoginView(APIView): 调用微信接口获取openid和unionid """ try: - appid = getattr(settings, 'WEIXIN_APPID', '') - secret = getattr(settings, 'WEIXIN_SECRET', '') + appid = wx_cfg.WEIXIN_APPID + secret = wx_cfg.WEIXIN_SECRET if not appid or not secret: raise ValueError('微信配置未设置') @@ -386,6 +388,7 @@ from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import status from django.conf import settings +from utils.xcx_sys_config import wx_cfg from .models import OfficialAccountUser logger = logging.getLogger(__name__) @@ -410,7 +413,7 @@ class WeixinOfficialCallbackView(APIView): nonce = request.GET.get('nonce', '') echostr = request.GET.get('echostr', '') - token = getattr(settings, 'WEIXIN_OFFICIAL_TOKEN', '') + token = wx_cfg.WEIXIN_OFFICIAL_TOKEN if not token: logger.error("服务号Token未配置") @@ -755,8 +758,8 @@ class UserInfoUpdateView(APIView): return token # 从 settings 读取 AppID 和 AppSecret - appid = getattr(settings, 'WEIXIN_APPID', None) - secret = getattr(settings, 'WEIXIN_SECRET', None) + appid = wx_cfg.WEIXIN_APPID or None + secret = wx_cfg.WEIXIN_SECRET or None if not appid or not secret: raise ValueError( 'settings.py 中缺少 WECHAT_APPID 或 WECHAT_APPSECRET,' @@ -4684,6 +4687,7 @@ class AdcjxgView(APIView): # 🔒 判断当前管理员是否有增加权限 from django.conf import settings +from utils.xcx_sys_config import wx_cfg allow_increase = zhanghao in getattr(settings, 'ALLOW_INCREASE_ADMINS', []) if user_type == 1: # 老板 - 不允许修改 @@ -6728,8 +6732,8 @@ class WechatLoginAndDashouRegisterView(APIView): 调用微信接口获取openid """ try: - appid = getattr(settings, 'WEIXIN_APPID', '') - secret = getattr(settings, 'WEIXIN_SECRET', '') + appid = wx_cfg.WEIXIN_APPID + secret = wx_cfg.WEIXIN_SECRET if not appid or not secret: raise ValueError('微信配置未设置') @@ -9080,6 +9084,7 @@ import time from decimal import Decimal from django.db import transaction, IntegrityError from django.conf import settings +from utils.xcx_sys_config import wx_cfg from django.core.exceptions import ObjectDoesNotExist from rest_framework.views import APIView from rest_framework.response import Response @@ -9386,8 +9391,8 @@ class KefuPlatformRefundView(APIView): "https://api.weixin.qq.com/cgi-bin/token", params={ "grant_type": "client_credential", - "appid": settings.WEIXIN_APPID, - "secret": settings.WEIXIN_SECRET, + "appid": wx_cfg.WEIXIN_APPID, + "secret": wx_cfg.WEIXIN_SECRET, }, timeout=10, ) @@ -9551,11 +9556,11 @@ class KefuPlatformRefundView(APIView): :return: {'code': 0, 'msg': '成功', 'refund_id': 'xxx'} 或 {'code': 非0, 'msg': '错误信息'} """ # 获取配置 - appid = getattr(settings, 'WEIXIN_APPID', '') - mch_id = getattr(settings, 'WEIXIN_MCHID', '') - key = getattr(settings, 'WEIXIN_SHANGHUMIYAO', '') - cert_path = getattr(settings, 'WEIXIN_CERT_PATH', '') - key_path = getattr(settings, 'WEIXIN_KEY_PATH', '') + appid = wx_cfg.WEIXIN_APPID + mch_id = wx_cfg.WEIXIN_MCHID + key = wx_cfg.WEIXIN_SHANGHUMIYAO + cert_path = wx_cfg.WEIXIN_CERT_PATH + key_path = wx_cfg.WEIXIN_KEY_PATH if not all([appid, mch_id, key, cert_path, key_path]): missing = [k for k, v in {'appid': appid, 'mch_id': mch_id, 'key': key, 'cert_path': cert_path, 'key_path': key_path}.items() if not v] @@ -12651,6 +12656,7 @@ from datetime import date import requests from django.db import transaction, IntegrityError from django.conf import settings +from utils.xcx_sys_config import wx_cfg from rest_framework.views import APIView from rest_framework import permissions, status from rest_framework.response import Response @@ -12679,6 +12685,7 @@ import logging import requests from django.db import transaction, IntegrityError from django.conf import settings +from utils.xcx_sys_config import wx_cfg from django.core.cache import cache from django.utils import timezone from rest_framework.views import APIView @@ -12710,6 +12717,7 @@ import logging import requests from django.db import transaction, IntegrityError from django.conf import settings +from utils.xcx_sys_config import wx_cfg from django.core.cache import cache from django.utils import timezone from rest_framework.views import APIView @@ -13605,8 +13613,8 @@ class WechatLoginAndGuanshiRegisterView(APIView): def _get_wechat_openid(self, code): try: - appid = getattr(settings, 'WEIXIN_APPID', '') - secret = getattr(settings, 'WEIXIN_SECRET', '') + appid = wx_cfg.WEIXIN_APPID + secret = wx_cfg.WEIXIN_SECRET if not appid or not secret: raise ValueError('微信配置未设置') url = 'https://api.weixin.qq.com/sns/jscode2session' @@ -14665,9 +14673,9 @@ class FaKuanPayView(APIView): }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) def generate_wechat_pay_params(self, dingdanid, jine, openid, pay_type): - APPID = settings.WEIXIN_APPID - MCHID = settings.WEIXIN_MCHID - KEY = settings.WEIXIN_SHANGHUMIYAO + APPID = wx_cfg.WEIXIN_APPID + MCHID = wx_cfg.WEIXIN_MCHID + KEY = wx_cfg.WEIXIN_SHANGHUMIYAO if pay_type == 'fakuan': PAY_DESCRIPTION = settings.FAKUAN_PAY_DESCRIPTION @@ -14927,7 +14935,7 @@ class FaKuanHuitiaoView(View): # 拼接字符串 sign_string = '&'.join([f"{key}={value}" for key, value in sorted_params if value]) - sign_string += f'&key={settings.WEIXIN_SHANGHUMIYAO}' + sign_string += f'&key={wx_cfg.WEIXIN_SHANGHUMIYAO}' # 计算签名 calculated_sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper() @@ -15188,9 +15196,9 @@ class KaohePayView(APIView): def generate_wechat_pay_params(self, dingdanid, jine, openid, attach, pay_type): """生成微信JSAPI支付参数""" - APPID = settings.WEIXIN_APPID - MCHID = settings.WEIXIN_MCHID - KEY = settings.WEIXIN_SHANGHUMIYAO + APPID = wx_cfg.WEIXIN_APPID + MCHID = wx_cfg.WEIXIN_MCHID + KEY = wx_cfg.WEIXIN_SHANGHUMIYAO if pay_type == 'kaohe': PAY_DESCRIPTION = settings.KAOHE_PAY_DESCRIPTION @@ -15508,7 +15516,7 @@ class KaohePayCallbackView(View): sorted_params = sorted(params.items(), key=lambda x: x[0]) sign_string = '&'.join([f"{key}={value}" for key, value in sorted_params if value]) - sign_string += f'&key={settings.WEIXIN_SHANGHUMIYAO}' + sign_string += f'&key={wx_cfg.WEIXIN_SHANGHUMIYAO}' calculated_sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper() result = sign == calculated_sign