小程序在9月27日发布了正式版
This commit is contained in:
@@ -324,7 +324,7 @@ def mesum_list(tenant_id):
|
||||
# model_type = request.args.get("type")
|
||||
try:
|
||||
res = []
|
||||
overviews=MesumOverviewService.get_all()
|
||||
overviews=MesumOverviewService.get_all(reverse=False,order_by ='sort_order')
|
||||
for o in overviews:
|
||||
res.append(o.to_dict())
|
||||
return get_result(data=res)
|
||||
|
||||
@@ -991,6 +991,7 @@ class CanvasTemplate(DataBaseModel):
|
||||
|
||||
# ------------added by cyx for mesum overview
|
||||
class MesumOverview(DataBaseModel):
|
||||
id = BigIntegerField(primary_key=True,null=False) # 非自增主键
|
||||
name = CharField(
|
||||
max_length=128,
|
||||
null=False,
|
||||
@@ -1043,6 +1044,14 @@ class MesumOverview(DataBaseModel):
|
||||
|
||||
free = IntegerField(default=0, index=False)
|
||||
hotspot_rank = IntegerField(default=50, index=False)
|
||||
|
||||
functions = CharField(
|
||||
max_length=300,
|
||||
null=False,
|
||||
help_text="此场所软件开放的功能",
|
||||
index=False)
|
||||
sort_order = IntegerField(default=0, null=False) #
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@@ -1063,6 +1072,8 @@ class MesumAntique(DataBaseModel):
|
||||
ttsUrl_child = CharField(max_length=256, null=True)
|
||||
photo_url = CharField(max_length=256, null=True)
|
||||
orgin_text = TextField(null=True)
|
||||
tags = CharField(max_length=200, null=True)
|
||||
sort_order = IntegerField(default=0) # 添加sort_order字段
|
||||
|
||||
class Meta:
|
||||
db_table = 'mesum_antique'
|
||||
|
||||
@@ -25,7 +25,7 @@ from api.db.services.common_service import CommonService
|
||||
from api.db.services.brief_service import MesumOverviewService
|
||||
from api.utils import get_uuid, get_format_time, current_timestamp, datetime_format
|
||||
from api.db import StatusEnum
|
||||
import logging
|
||||
import logging,json
|
||||
|
||||
class MesumAntiqueService(CommonService):
|
||||
model = MesumAntique
|
||||
@@ -33,8 +33,25 @@ class MesumAntiqueService(CommonService):
|
||||
@classmethod
|
||||
@DB.connection_context()
|
||||
def get_by_mesum_id(cls, mesum_id):
|
||||
objs = cls.query(mesum_id=mesum_id)
|
||||
return objs
|
||||
# objs = cls.query(mesum_id=mesum_id)
|
||||
# return objs
|
||||
"""
|
||||
根据mesum_id查询记录,并按category和sort_order排序
|
||||
"""
|
||||
try:
|
||||
objs = (cls.model
|
||||
.select()
|
||||
.where(cls.model.mesum_id == mesum_id)
|
||||
.order_by(
|
||||
cls.model.category, # 一级排序:按category
|
||||
cls.model.sort_order.asc() # 二级排序:sort_order升序
|
||||
)
|
||||
.execute())
|
||||
return list(objs)
|
||||
|
||||
except Exception as e:
|
||||
print(f"根据mesum_id查询失败: {e}")
|
||||
return []
|
||||
|
||||
@classmethod
|
||||
@DB.connection_context()
|
||||
@@ -44,6 +61,15 @@ class MesumAntiqueService(CommonService):
|
||||
try:
|
||||
mesum_brief = MesumOverviewService.query(id=mesum_id)
|
||||
if mesum_brief:
|
||||
|
||||
"""
|
||||
20250922 数据库中category 改为如下形式
|
||||
[{"name":"第一展厅(新石器-唐、战国、秦汉)","audio":"xxxx","photo":"xxx"},
|
||||
{"name":"第二展厅 (辽金)","audio":"xxx","photo":""},
|
||||
{"name":"第三展厅(元)","audio":"xxx","photo":""},
|
||||
{"name":"第四展厅(明清-至今)","audio":"xxx","photo":""}]
|
||||
"""
|
||||
"""
|
||||
categories_text= mesum_brief[0].category
|
||||
# 统一替换中文分号为英文分号,并去除末尾分号
|
||||
if categories_text:
|
||||
@@ -51,6 +77,9 @@ class MesumAntiqueService(CommonService):
|
||||
# 分割并清理空格/空值
|
||||
mesum_antique_categories = [dynasty.strip() for dynasty in categories_text.split(";") if dynasty.strip()]
|
||||
|
||||
"""
|
||||
mesum_category_json = json.loads(mesum_brief[0].category.strip())
|
||||
mesum_antique_categories = [item["name"] for item in mesum_category_json]
|
||||
finally:
|
||||
pass
|
||||
categories = [category.category
|
||||
@@ -82,13 +111,61 @@ class MesumAntiqueService(CommonService):
|
||||
|
||||
@classmethod
|
||||
@DB.connection_context()
|
||||
def get_labels_ext(cls, mesum_id):
|
||||
def get_labels_ext(cls, mesum_id,tags = None):
|
||||
# 根据mesum_id过滤,并排除空的category
|
||||
query = cls.model.select().where(
|
||||
"""query = cls.model.select().where(
|
||||
(cls.model.mesum_id == mesum_id) &
|
||||
(cls.model.category != "")
|
||||
).order_by(cls.model.category)
|
||||
).order_by(
|
||||
cls.model.category, # 一级排序:按category
|
||||
cls.model.sort_order.asc() # 二级排序:sort_order升序
|
||||
)
|
||||
"""
|
||||
# 基础查询条件
|
||||
conditions = [
|
||||
(cls.model.mesum_id == mesum_id),
|
||||
(cls.model.category != ""),
|
||||
(cls.model.category.is_null(False)) # 确保category不为NULL
|
||||
]
|
||||
# 添加tags条件
|
||||
if tags is not None:
|
||||
# 如果tags是字符串,按逗号分割处理
|
||||
if isinstance(tags, str):
|
||||
tags_list = [tag.strip() for tag in tags.split(',') if tag.strip()]
|
||||
elif isinstance(tags, list):
|
||||
tags_list = tags
|
||||
else:
|
||||
tags_list = []
|
||||
|
||||
if tags_list:
|
||||
# 构建tags匹配条件:tags包含任意一个指定的标签
|
||||
tag_conditions = []
|
||||
for tag in tags_list:
|
||||
# 匹配tags字段包含该标签(考虑逗号分隔的情况)
|
||||
tag_conditions.append(
|
||||
(cls.model.tags.contains(tag)) |
|
||||
(cls.model.tags.startswith(f"{tag},")) |
|
||||
(cls.model.tags.endswith(f",{tag}")) |
|
||||
(cls.model.tags == tag)
|
||||
)
|
||||
|
||||
# 将所有tag条件用OR连接
|
||||
tags_condition = reduce(lambda a, b: a | b, tag_conditions)
|
||||
conditions.append(tags_condition)
|
||||
else:
|
||||
# 如果tags为空但传入了参数,确保tags不为空
|
||||
conditions.append(
|
||||
(cls.model.tags != "") &
|
||||
(cls.model.tags.is_null(False))
|
||||
)
|
||||
else:
|
||||
# 如果未传入tags参数,包含所有tags记录(包括空值)
|
||||
pass
|
||||
# 执行查询,添加二级排序
|
||||
query = cls.model.select().where(*conditions).order_by(
|
||||
cls.model.category, # 一级排序:按category
|
||||
cls.model.sort_order.asc() # 二级排序:按sort_order升序
|
||||
)
|
||||
# 按category分组并存储结果
|
||||
grouped_data = {}
|
||||
for obj in query.dicts().execute():
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
# limitations under the License.
|
||||
#
|
||||
from datetime import datetime
|
||||
|
||||
import logging
|
||||
import peewee
|
||||
|
||||
from api.db.db_models import DB
|
||||
@@ -38,7 +38,7 @@ class CommonService:
|
||||
else:
|
||||
query_records = cls.model.select()
|
||||
if reverse is not None:
|
||||
if not order_by or not hasattr(cls, order_by):
|
||||
if not order_by or not hasattr(cls.model, order_by): # 20250923 这里之前使用cls ,而没有model是错误的
|
||||
order_by = "create_time"
|
||||
if reverse is True:
|
||||
query_records = query_records.order_by(
|
||||
|
||||
31088
asr-monitor-test/app.log
31088
asr-monitor-test/app.log
File diff suppressed because it is too large
Load Diff
@@ -147,7 +147,7 @@ def create_museum(data: dict):
|
||||
return execute_query(sql, params)
|
||||
|
||||
|
||||
def get_museums(search: str = None, free: int = None):
|
||||
def get_museums(search: str = None, free: int = None, id_list: Union[None, int, List[int]] = None):
|
||||
base_sql = "SELECT * FROM mesum_overview WHERE 1=1"
|
||||
params = []
|
||||
|
||||
@@ -159,11 +159,30 @@ def get_museums(search: str = None, free: int = None):
|
||||
base_sql += " AND free = %s"
|
||||
params.append(free)
|
||||
|
||||
# 处理 id_list 参数
|
||||
if id_list is not None:
|
||||
# 如果 id_list 是单个整数,转换为列表
|
||||
if isinstance(id_list, int):
|
||||
id_list = [id_list]
|
||||
|
||||
# 如果 id_list 是列表且不为空
|
||||
if isinstance(id_list, list) and len(id_list) > 0:
|
||||
# 创建占位符字符串,例如 "%s, %s, %s"
|
||||
placeholders = ", ".join(["%s"] * len(id_list))
|
||||
base_sql += f" AND id IN ({placeholders})"
|
||||
params.extend(id_list)
|
||||
|
||||
if isinstance(id_list, str):
|
||||
id_list = [int(x) for x in id_list.split(',') if x.strip().isdigit()]
|
||||
# 创建占位符字符串,例如 "%s, %s, %s"
|
||||
placeholders = ", ".join(["%s"] * len(id_list))
|
||||
base_sql += f" AND id IN ({placeholders})"
|
||||
params.extend(id_list)
|
||||
|
||||
base_sql += " ORDER BY create_time DESC"
|
||||
|
||||
return execute_query(base_sql, tuple(params))
|
||||
|
||||
|
||||
def update_museum(museum_id: int, data: dict):
|
||||
"""动态更新博物馆信息(仅更新data中包含的字段)"""
|
||||
allowed_fields = {
|
||||
@@ -940,7 +959,7 @@ def get_subscription_template_by_id(template_id: int) -> dict:
|
||||
return result[0] if result else None
|
||||
|
||||
|
||||
def get_active_user_subscription(user_id: str, museum_id: int) -> dict:
|
||||
def get_user_subscription(user_id: str, museum_id: int) -> dict:
|
||||
"""
|
||||
获取用户在指定博物馆的有效订阅
|
||||
|
||||
@@ -975,6 +994,82 @@ def get_active_user_subscription(user_id: str, museum_id: int) -> dict:
|
||||
return result[0] if result else None
|
||||
|
||||
|
||||
def get_all_users_subscriptions_paginated(
|
||||
museum_id: Optional[int] = None,
|
||||
page: int = 1,
|
||||
page_size: int = 50
|
||||
) -> Dict[str, any]:
|
||||
"""
|
||||
获取所有用户的有效订阅(分页版)
|
||||
|
||||
参数说明:
|
||||
- museum_id: 博物馆ID,如果为None则查询所有博物馆
|
||||
- page: 页码,从1开始
|
||||
- page_size: 每页记录数
|
||||
|
||||
返回:
|
||||
- 包含订阅列表和分页信息的字典
|
||||
"""
|
||||
offset = (page - 1) * page_size
|
||||
|
||||
# 基础查询条件
|
||||
#where_conditions = ["us.is_active = 1", "us.end_date > NOW()"]
|
||||
where_conditions = []
|
||||
params = []
|
||||
|
||||
if museum_id is not None:
|
||||
where_conditions.append("ms.museum_id = %s")
|
||||
params.append(museum_id)
|
||||
|
||||
where_clause = " AND ".join(where_conditions)
|
||||
|
||||
# 获取总数
|
||||
count_sql = f"""
|
||||
SELECT COUNT(*) as total
|
||||
FROM user_subscriptions us
|
||||
JOIN museum_subscriptions ms ON us.museum_subscription_id = ms.sub_id
|
||||
WHERE {where_clause}
|
||||
"""
|
||||
|
||||
total_result = execute_query(count_sql, params)
|
||||
total = total_result[0]['total'] if total_result else 0
|
||||
# 获取分页数据
|
||||
data_sql = f"""
|
||||
SELECT
|
||||
us.*,
|
||||
st.name as template_name,
|
||||
st.description as template_description,
|
||||
st.validity_type as template_validity_type,
|
||||
ui.openid as openid,
|
||||
ui.phone as phone,
|
||||
ms.museum_id,
|
||||
ms.price as price,
|
||||
m.name as museum_name
|
||||
FROM user_subscriptions us
|
||||
JOIN museum_subscriptions ms ON us.museum_subscription_id = ms.sub_id
|
||||
JOIN subscription_templates st ON ms.template_id = st.id
|
||||
JOIN users_info ui ON us.user_id = ui.user_id
|
||||
LEFT JOIN mesum_overview m ON ms.museum_id = m.id
|
||||
WHERE {where_clause}
|
||||
ORDER BY us.create_date, ms.museum_id, us.user_id ASC
|
||||
LIMIT %s OFFSET %s
|
||||
"""
|
||||
|
||||
# 添加分页参数
|
||||
data_params = params + [page_size, offset]
|
||||
|
||||
data_result = execute_query(data_sql, data_params)
|
||||
|
||||
return {
|
||||
"data": data_result if data_result else [],
|
||||
"pagination": {
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"total": total,
|
||||
"total_pages": (total + page_size - 1) // page_size if page_size > 0 else 0
|
||||
}
|
||||
}
|
||||
|
||||
def get_user_subscription_history(user_id: str) -> list:
|
||||
"""
|
||||
获取用户的订阅历史记录
|
||||
@@ -1191,7 +1286,7 @@ def check_user_subscription(user_id: str, museum_id: int) -> dict:
|
||||
- 如果没有,检查免费订阅是否可用
|
||||
"""
|
||||
# 1. 检查当前有效订阅
|
||||
active_sub = get_active_user_subscription(user_id, museum_id)
|
||||
active_sub = get_user_subscription(user_id, museum_id)
|
||||
if active_sub:
|
||||
return active_sub
|
||||
|
||||
@@ -1288,6 +1383,15 @@ def calculate_subscription_expiry(start_date: datetime, validity_type: str) -> d
|
||||
elif validity_type == "1month":
|
||||
# 下个月的同一天(自动处理月末情况)
|
||||
return start_date + relativedelta(months=1)
|
||||
elif validity_type == "2month":
|
||||
# 下个月的同一天(自动处理月末情况)
|
||||
return start_date + relativedelta(months=2)
|
||||
elif validity_type == "3month":
|
||||
# 下个月的同一天(自动处理月末情况)
|
||||
return start_date + relativedelta(months=3)
|
||||
elif validity_type == "6month":
|
||||
# 下个月的同一天(自动处理月末情况)
|
||||
return start_date + relativedelta(months=6)
|
||||
elif validity_type == "1year":
|
||||
# 下一年的同一天
|
||||
return start_date + relativedelta(years=1)
|
||||
@@ -1359,3 +1463,26 @@ def is_subscription_valid(subscription: dict) -> bool:
|
||||
pass
|
||||
|
||||
return True
|
||||
|
||||
|
||||
# 查询用户(多条件)
|
||||
def get_admin_account_info(status: int = None, email: str = None, phone: str = None):
|
||||
base_sql = "SELECT * FROM rag_flow.admin_account WHERE 1=1"
|
||||
params = []
|
||||
|
||||
if status is not None:
|
||||
base_sql += " AND status = %s"
|
||||
params.append(status)
|
||||
|
||||
if email:
|
||||
base_sql += " AND email = %s"
|
||||
params.append(email)
|
||||
|
||||
if phone:
|
||||
base_sql += " AND phone = %s"
|
||||
params.append(phone)
|
||||
|
||||
|
||||
base_sql += " ORDER BY create_time DESC"
|
||||
|
||||
return execute_query(base_sql, tuple(params))
|
||||
@@ -241,6 +241,8 @@ fake_phone_number = {
|
||||
'hxbtest004':'19912345634',
|
||||
'hxbtest005':'19912345635',
|
||||
}
|
||||
|
||||
|
||||
@login_router.post("/testAccountLogin")
|
||||
async def test_account_login(request: Request):
|
||||
# 获取原始请求数据
|
||||
@@ -407,6 +409,14 @@ async def verify_token(user: dict = Depends(optional_current_user)):
|
||||
"""
|
||||
logging.info(f"verify_token user={user}")
|
||||
if user:
|
||||
try:
|
||||
update_data = {
|
||||
"last_login_time": int(datetime.now().timestamp())
|
||||
}
|
||||
updated_user = update_user(user["user_id"], update_data)
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
return JSONResponse({
|
||||
"valid": True,
|
||||
"user": {
|
||||
|
||||
@@ -20,6 +20,7 @@ from app.tts_service import tts_router,tts_lifespan
|
||||
from app.login_service import login_router
|
||||
from app.chat_service import chat_router
|
||||
from app.payment_service import payment_router
|
||||
from app.system_admin import system_admin_router
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
@@ -60,6 +61,7 @@ app.include_router(tts_router, prefix="/tts")
|
||||
app.include_router(login_router, prefix="/auth")
|
||||
app.include_router(chat_router, prefix="/chat")
|
||||
app.include_router(payment_router, prefix="/payment")
|
||||
app.include_router(system_admin_router,prefix="/system_admin")
|
||||
|
||||
# 挂载静态文件(可选)
|
||||
# app.mount("/static", StaticFiles(directory="static"), name="static")
|
||||
|
||||
@@ -8,6 +8,7 @@ import xml.etree.ElementTree as ET
|
||||
import requests
|
||||
import time
|
||||
from datetime import datetime, timedelta, date
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from decimal import Decimal
|
||||
from uuid import UUID
|
||||
import json
|
||||
@@ -553,6 +554,7 @@ async def get_order_detial(
|
||||
"msg": "success",
|
||||
"data": result})
|
||||
|
||||
# 用户是否有权访问这个博物馆
|
||||
@payment_router.post("/get_user_museum_subscriptions")
|
||||
async def get_user_museum_subscriptions(
|
||||
request: Request,
|
||||
@@ -959,41 +961,6 @@ async def deliver_virtual_goods(order: dict) -> bool:
|
||||
logger.exception(f"发货处理异常: {str(e)}")
|
||||
|
||||
return False
|
||||
# 套餐激活函数
|
||||
def activate_user_product(user_id: str, product_id: int, museum_id: int, order_id: str):
|
||||
product = get_product_by_id(product_id)
|
||||
if not product:
|
||||
return
|
||||
|
||||
# 计算有效期
|
||||
start_time = datetime.now()
|
||||
end_time = calculate_expiry(start_time, product["validity_type"])
|
||||
|
||||
# 创建用户套餐记录
|
||||
create_user_product({
|
||||
"user_id": user_id,
|
||||
"product_id": product_id,
|
||||
"museum_id": museum_id,
|
||||
"order_id": order_id,
|
||||
"start_time": start_time,
|
||||
"end_time": end_time,
|
||||
"is_active": True
|
||||
})
|
||||
|
||||
# 禁用同一博物馆的旧套餐
|
||||
deactivate_previous_products(user_id, museum_id)
|
||||
|
||||
|
||||
def calculate_expiry(start_time: datetime, validity_type: str) -> datetime:
|
||||
if validity_type == "free":
|
||||
return start_time + timedelta(days=7) # 免费套餐7天
|
||||
elif validity_type == "1month":
|
||||
return start_time + timedelta(days=30)
|
||||
elif validity_type == "1year":
|
||||
return start_time + timedelta(days=365)
|
||||
else: # permanent
|
||||
return datetime(2999, 12, 31)
|
||||
|
||||
|
||||
async def generate_wx_prepay_params_v3(order_id: str, total_fee: int, openid: str, body: str):
|
||||
"""微信支付v3统一下单"""
|
||||
@@ -1194,6 +1161,128 @@ async def query_order_by_out_trade_no(out_trade_no: str):
|
||||
except Exception as e:
|
||||
logging.error(f"订单查询异常: {str(e)}", exc_info=True)
|
||||
return None
|
||||
|
||||
"""
|
||||
20250929 临时增加生成1个临时的收款二维码,用于测试学校智能水表收费
|
||||
"""
|
||||
|
||||
|
||||
@payment_router.post("/create_temp_qrcode_simple")
|
||||
async def create_temp_qrcode_simple(
|
||||
request: Request
|
||||
):
|
||||
"""
|
||||
生成临时收款二维码(简化版,不创建订单)
|
||||
请求参数: {"amount": 金额(元), "description": "商品描述"}
|
||||
返回: 二维码URL
|
||||
"""
|
||||
try:
|
||||
data = await request.json()
|
||||
amount = data.get("amount")
|
||||
description = data.get("description", "临时收款")
|
||||
|
||||
if not amount or float(amount) <= 0:
|
||||
raise HTTPException(status_code=400, detail="金额必须大于0")
|
||||
|
||||
# 生成临时订单ID(仅用于支付,不存储)
|
||||
order_id = f"SMART_WATER_{int(time.time())}{random.randint(1000, 9999)}"
|
||||
amount_float = float(amount)
|
||||
amount_in_cents = int(amount_float * 100) # 转换为分
|
||||
|
||||
# 生成Native支付二维码
|
||||
try:
|
||||
qr_code_url = await generate_simple_native_qrcode(
|
||||
order_id=order_id,
|
||||
total_fee=amount_in_cents,
|
||||
body=description
|
||||
)
|
||||
except Exception as error:
|
||||
logger.info(f"生成支付二维码失败{error}")
|
||||
if not qr_code_url:
|
||||
raise HTTPException(status_code=500, detail="生成支付二维码失败")
|
||||
|
||||
return JSONResponse({
|
||||
"code": 0,
|
||||
"msg": "success",
|
||||
"data": {
|
||||
"order_id": order_id, # 返回订单ID用于测试查询
|
||||
"amount": amount_float,
|
||||
"description": description,
|
||||
"qr_code_url": qr_code_url,
|
||||
"expire_time": (datetime.now() + timedelta(hours=4)).isoformat(),
|
||||
"note": "此为测试二维码,支付成功后不会激活任何服务"
|
||||
}
|
||||
})
|
||||
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="金额格式错误")
|
||||
except Exception as e:
|
||||
logger.error(f"生成临时收款二维码失败: {str(e)}", exc_info=True)
|
||||
raise HTTPException(status_code=500, detail=f"生成失败: {str(e)}")
|
||||
|
||||
|
||||
async def generate_simple_native_qrcode(order_id: str, total_fee: int, body: str):
|
||||
"""生成简化的Native支付二维码"""
|
||||
try:
|
||||
nonce_str = generate_nonce_str(32)
|
||||
params = {
|
||||
"appid": WX_APPID,
|
||||
"mch_id": WX_MCH_ID,
|
||||
"nonce_str": nonce_str,
|
||||
"body": f"测试-{body}"[:128], # 添加测试前缀
|
||||
"out_trade_no": order_id,
|
||||
"total_fee": str(total_fee),
|
||||
"spbill_create_ip": "127.0.0.1",
|
||||
"notify_url": WX_PAY_NOTIFY_URL, # 使用相同的回调接口
|
||||
"trade_type": "NATIVE",
|
||||
"time_expire": (datetime.now() + timedelta(hours=4)).strftime('%Y%m%d%H%M%S') # 4小时过期
|
||||
}
|
||||
|
||||
# 生成签名
|
||||
params["sign"] = generate_sign_v2(params, WX_PAY_KEY)
|
||||
|
||||
# 转换为XML
|
||||
xml_data = dict_to_xml(params)
|
||||
try:
|
||||
# 调用微信统一下单接口
|
||||
response = requests.post(
|
||||
"https://api.mch.weixin.qq.com/pay/unifiedorder",
|
||||
data=xml_data.encode('utf-8'),
|
||||
headers={"Content-Type": "application/xml"},
|
||||
timeout=10
|
||||
)
|
||||
except Exception as error:
|
||||
logging.info(f"调用生成支持二维码失败:{error}")
|
||||
logging.info(f"res={response}")
|
||||
if response.status_code != 200:
|
||||
logger.error(f"Native支付接口错误: {response.status_code}")
|
||||
return None
|
||||
|
||||
response_data = xml_to_dict(response.text)
|
||||
|
||||
if response_data.get("return_code") != "SUCCESS":
|
||||
error_msg = response_data.get("return_msg", "未知错误")
|
||||
logger.error(f"Native支付下单失败: {error_msg}")
|
||||
return None
|
||||
|
||||
if response_data.get("result_code") != "SUCCESS":
|
||||
error_code = response_data.get("err_code", "")
|
||||
error_msg = response_data.get("err_code_des", "未知错误")
|
||||
logger.error(f"Native支付业务失败: [{error_code}] {error_msg}")
|
||||
return None
|
||||
|
||||
# 返回二维码链接
|
||||
code_url = response_data.get("code_url")
|
||||
if code_url:
|
||||
logger.info(f"生成测试二维码成功: {order_id}, 金额: {total_fee}分")
|
||||
return code_url
|
||||
else:
|
||||
logger.error("Native支付返回缺少code_url")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"生成测试二维码异常: {str(e)}", exc_info=True)
|
||||
return None
|
||||
"""
|
||||
需要在 database.py 中实现以下函数:
|
||||
|
||||
|
||||
231
asr-monitor-test/app/system_admin.py
Normal file
231
asr-monitor-test/app/system_admin.py
Normal file
@@ -0,0 +1,231 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Request, Response,Query
|
||||
from fastapi.responses import StreamingResponse, JSONResponse
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
import hashlib
|
||||
import random
|
||||
import string
|
||||
import xml.etree.ElementTree as ET
|
||||
import requests
|
||||
import time
|
||||
from datetime import datetime, timedelta, date
|
||||
from decimal import Decimal
|
||||
from uuid import UUID
|
||||
import json
|
||||
import logging
|
||||
from app.database import *
|
||||
from jose import JWTError, jwt
|
||||
import base64
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import padding
|
||||
from cryptography.hazmat.primitives.hashes import SHA256
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
import httpx,threading,asyncio
|
||||
|
||||
class CustomJSONResponse(JSONResponse):
|
||||
"""
|
||||
自定义 JSON 响应类,处理特殊类型:
|
||||
- datetime: 转换为 ISO 8601 字符串
|
||||
- date: 转换为 ISO 8601 字符串
|
||||
- Decimal: 转换为 float
|
||||
"""
|
||||
|
||||
def render(self, content: any) -> bytes:
|
||||
"""
|
||||
重写渲染方法,使用自定义编码器
|
||||
"""
|
||||
class EnhancedJSONEncoder(json.JSONEncoder):
|
||||
def default(self, obj):
|
||||
"""
|
||||
增强型 JSON 编码器,处理多种特殊类型:
|
||||
- datetime: 转换为 ISO 8601 字符串
|
||||
- date: 转换为 ISO 8601 字符串
|
||||
- time: 转换为 ISO 8601 字符串
|
||||
- Decimal: 转换为 float
|
||||
- UUID: 转换为字符串
|
||||
- numpy 类型: 转换为 Python 原生类型
|
||||
"""
|
||||
# 处理日期时间类型
|
||||
if isinstance(obj, datetime):
|
||||
return obj.isoformat()
|
||||
|
||||
if isinstance(obj, date):
|
||||
return obj.isoformat()
|
||||
|
||||
# 处理 Decimal 类型
|
||||
if isinstance(obj, Decimal):
|
||||
return float(obj)
|
||||
|
||||
# 处理 UUID 类型
|
||||
if isinstance(obj, UUID):
|
||||
return str(obj)
|
||||
"""
|
||||
# 处理 numpy 类型
|
||||
if isinstance(obj, np.integer):
|
||||
return int(obj)
|
||||
if isinstance(obj, np.floating):
|
||||
return float(obj)
|
||||
if isinstance(obj, np.ndarray):
|
||||
return obj.tolist()
|
||||
"""
|
||||
# 处理其他自定义类型
|
||||
if hasattr(obj, '__json__'):
|
||||
return obj.__json__()
|
||||
|
||||
# 默认处理
|
||||
return super().default(obj)
|
||||
|
||||
return json.dumps(
|
||||
content,
|
||||
ensure_ascii=False,
|
||||
allow_nan=False,
|
||||
indent=None,
|
||||
separators=(",", ":"),
|
||||
cls=EnhancedJSONEncoder
|
||||
).encode("utf-8")
|
||||
|
||||
|
||||
system_admin_router = APIRouter()
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
|
||||
logger = logging.getLogger("system_admin")
|
||||
|
||||
JWT_SECRET_KEY = "3e5b8d7f1a9c2b6d4e0f1a9c2b6d4e0f1a9c2b6d4e0f1a9c2b6d4e0f1a9c2b6d"
|
||||
ALGORITHM = "HS256"
|
||||
# JWT工具函数
|
||||
def create_jwt(user_id: str) -> str:
|
||||
payload = {
|
||||
"sub": user_id,
|
||||
"exp": datetime.utcnow() + timedelta(days=7)
|
||||
}
|
||||
return jwt.encode(payload, JWT_SECRET_KEY, algorithm=ALGORITHM)
|
||||
|
||||
async def get_current_user(token: str = Depends(oauth2_scheme)):
|
||||
"""
|
||||
可选用户依赖项(不抛出401错误)
|
||||
返回: 用户对象 或 None
|
||||
"""
|
||||
try:
|
||||
payload = jwt.decode(token, JWT_SECRET_KEY, algorithms=[ALGORITHM])
|
||||
user_id = payload.get("sub")
|
||||
return get_user_by_id(user_id)
|
||||
except (JWTError, StopIteration):
|
||||
return None
|
||||
|
||||
|
||||
|
||||
@system_admin_router.get("/get_subscriptions")
|
||||
async def get_museum_subscriptions(
|
||||
museum_id: Optional[int] = Query(None, description="博物馆ID,不提供则返回所有博物馆"),
|
||||
page: int = Query(1, ge=1, description="页码,从1开始"),
|
||||
page_size: int = Query(50, ge=1, le=100, description="每页记录数,最大100"),
|
||||
#current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
# 直接使用参数,FastAPI 会自动处理类型转换和验证
|
||||
result = get_all_users_subscriptions_paginated(museum_id, page, page_size)
|
||||
return CustomJSONResponse({
|
||||
"code": 0,
|
||||
"status": "success",
|
||||
"data": result.get('data'),
|
||||
"pagination":result.get("pagination")
|
||||
})
|
||||
|
||||
|
||||
|
||||
def generate_json_response(code: str, message: str) -> JSONResponse:
|
||||
return JSONResponse(
|
||||
content={"code": code, "message": message},
|
||||
status_code=200
|
||||
)
|
||||
|
||||
@system_admin_router.post("/login")
|
||||
async def test_account_login(request: Request):
|
||||
# 获取原始请求数据
|
||||
try:
|
||||
data = await request.json()
|
||||
except json.JSONDecodeError:
|
||||
raise HTTPException(400, "Invalid JSON")
|
||||
# 校验必要参数
|
||||
required_fields = ["user","password"]
|
||||
if not all(k in data for k in required_fields):
|
||||
raise HTTPException(400, "Missing required fields")
|
||||
user = data.get('user')
|
||||
password = data.get('password')
|
||||
account_info=get_admin_account_info(phone=user)
|
||||
if account_info and len(account_info)>0:
|
||||
account_info = account_info[0]
|
||||
else:
|
||||
return JSONResponse({
|
||||
"code": 0,
|
||||
"status": "error",
|
||||
"data": {
|
||||
"status":"error",
|
||||
"msg": "账户不存在",
|
||||
"token": create_jwt(user),
|
||||
"user_info": {
|
||||
|
||||
},
|
||||
"menu_authed": [],
|
||||
"museum_authed": []
|
||||
}
|
||||
})
|
||||
if account_info['password'] != password:
|
||||
return JSONResponse({
|
||||
"code": 0,
|
||||
"status": "error",
|
||||
"data": {
|
||||
"status":"error",
|
||||
"msg": "密码不正确",
|
||||
"token": create_jwt(user),
|
||||
"user_info": {
|
||||
|
||||
},
|
||||
"menu_authed": [],
|
||||
"museum_authed": []
|
||||
}
|
||||
})
|
||||
museum_authed_str = account_info.get("museum_authed",None)
|
||||
menu_authed_str = account_info.get("menu_authed","")
|
||||
logging.info(f"account_info={account_info} {type(account_info)} {museum_authed_str}")
|
||||
if not museum_authed_str: # 使用not 可以同步判断为空字符串
|
||||
all_museums = get_museums()
|
||||
logger.info(f"all {all_museums}")
|
||||
museum_authed_list = [
|
||||
{"id": int(item.id), "name": item.name}
|
||||
for item in all_museums
|
||||
if hasattr(item, 'id') and hasattr(item, 'name') and item.id is not None and item.name is not None
|
||||
]
|
||||
else:
|
||||
museum_records = get_museums(id_list = museum_authed_str)
|
||||
museum_authed_list = [
|
||||
{"id": int(item['id']), "name": item['name']}
|
||||
for item in museum_records
|
||||
if isinstance(item, dict) and 'id' in item and 'name' in item and item['id'] is not None and item[
|
||||
'name'] is not None
|
||||
]
|
||||
#museum_authed_list = [int(x) for x in museum_authed_str.split(',') if x.strip().isdigit()]
|
||||
menu_authed_list = [x for x in menu_authed_str.split(',') ]
|
||||
|
||||
|
||||
# ========== 数据库操作开始 ==========
|
||||
# 使用数据库查询替代内存查询
|
||||
#db_users = get_users(openid=user)
|
||||
#user = db_users[0] if db_users else None
|
||||
|
||||
# ========== 数据库操作结束 ==========
|
||||
|
||||
logging.info(f"system account login return {user} {password}")
|
||||
# 生成token
|
||||
|
||||
return JSONResponse({
|
||||
"code": 0,
|
||||
"status": "success",
|
||||
"data": {
|
||||
"status":"success",
|
||||
"token": create_jwt(user),
|
||||
"user_info": {
|
||||
|
||||
},
|
||||
"menu_authed":menu_authed_list,
|
||||
"museum_authed":museum_authed_list
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1091,6 +1091,7 @@ class QwenTTS:
|
||||
self.synthesizer = None
|
||||
self.callback = None
|
||||
self.is_cosyvoice = False
|
||||
self.is_cosyvoice_v2 = False
|
||||
self.cosyvoice = ""
|
||||
self.voice = ""
|
||||
self.format = format
|
||||
@@ -1103,6 +1104,9 @@ class QwenTTS:
|
||||
self.is_cosyvoice = True
|
||||
self.cosyvoice = parts[0]
|
||||
self.voice = parts[1]
|
||||
if parts[0] == 'cosyvoice-v2':
|
||||
self.is_cosyvoice_v2 = True
|
||||
|
||||
logging.info(f"---begin--init QwenTTS-- {format} {sample_rate} {model_name} {self.cosyvoice} {self.voice}") # cyx
|
||||
self.completion_event = None # 新增:用于通知任务完成
|
||||
# 特殊字符及其拼音映射
|
||||
@@ -1292,6 +1296,12 @@ class QwenTTS:
|
||||
"""
|
||||
在文本中查找特殊字符并用<phoneme>标签包裹它们
|
||||
"""
|
||||
"""
|
||||
SSML(Speech Synthesis Markup Language) 是一种基于 XML 的语音合成标记语言。
|
||||
它不仅能让语音合成大模型读出更丰富的文本内容,还支持对语速、语调、停顿、音量等语音特征进行精细控制,
|
||||
甚至可以添加背景音乐,带来更具表现力的语音效果。本文介绍CosyVoice的SSML功能及使用。
|
||||
仅限cosyvoice-v2模型
|
||||
"""
|
||||
# 如果文本已经是SSML格式,直接返回
|
||||
if text.strip().startswith("<speak>") and text.strip().endswith("</speak>"):
|
||||
return text
|
||||
@@ -1311,7 +1321,9 @@ class QwenTTS:
|
||||
return f"<speak>{text}</speak>"
|
||||
|
||||
def text_tts_call(self, text):
|
||||
if self.special_characters :
|
||||
# SSML(Speech Synthesis Markup Language) 是一种基于 XML 的语音合成标记语言。
|
||||
# 仅限cosyvoice-v2模型
|
||||
if self.special_characters and self.is_cosyvoice_v2 :
|
||||
text = self.apply_phoneme_tags(text)
|
||||
#logging.info(f"Applied SSML phoneme tags to text: {text}")
|
||||
volume = 50
|
||||
|
||||
@@ -45,3 +45,5 @@
|
||||
[DEBUG] 错误详情: 可能原因 -
|
||||
通用错误
|
||||
[SUCCESS] Sun Aug 17 03:55:56 PM CST 2025 同步成功 | 大小: 18M
|
||||
[SUCCESS] Mon Sep 8 09:20:51 AM CST 2025 同步成功 | 大小: 19M
|
||||
[SUCCESS] Tue Sep 23 12:10:14 PM CST 2025 同步成功 | 大小: 19M
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -120,7 +120,7 @@ pyicu = "^2.13.1"
|
||||
flasgger = "^0.9.7.1"
|
||||
polars = { version = "^1.9.0", markers = "platform_machine == 'x86_64'" }
|
||||
polars-lts-cpu = { version = "^1.9.0", markers = "platform_machine == 'arm64'" }
|
||||
|
||||
python-dateutil="^2.8.2"
|
||||
|
||||
[tool.poetry.group.full]
|
||||
optional = true
|
||||
|
||||
Reference in New Issue
Block a user