upload project source code
This commit is contained in:
@@ -0,0 +1 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
@@ -0,0 +1 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
@@ -0,0 +1,137 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from fastapi import APIRouter, Depends, UploadFile, Body, Path, Query
|
||||
from fastapi.responses import StreamingResponse, JSONResponse
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.common.response import SuccessResponse, StreamResponse
|
||||
from app.core.dependencies import AuthPermission, db_getter
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from app.core.base_params import PaginationQueryParam
|
||||
from app.utils.common_util import bytes2file_response
|
||||
from app.core.logger import log
|
||||
from app.core.base_schema import BatchSetAvailable
|
||||
|
||||
from .service import YifanAboutUsService
|
||||
from .schema import YifanAboutUsCreateSchema, YifanAboutUsUpdateSchema, YifanAboutUsQueryParam
|
||||
|
||||
YifanAboutUsRouter = APIRouter(prefix='/yifan_about_us', tags=["品牌介绍模块"])
|
||||
|
||||
@YifanAboutUsRouter.get("/miniapp/info", summary="小程序获取品牌介绍", description="小程序获取品牌介绍(获取启用的第一条)")
|
||||
async def get_miniapp_about_us_controller(
|
||||
db: AsyncSession = Depends(db_getter)
|
||||
) -> JSONResponse:
|
||||
"""小程序获取品牌介绍接口"""
|
||||
auth = AuthSchema(db=db)
|
||||
result_dict = await YifanAboutUsService.get_miniapp_about_us_service(auth=auth)
|
||||
log.info("小程序获取品牌介绍成功")
|
||||
return SuccessResponse(data=result_dict, msg="获取品牌介绍成功")
|
||||
|
||||
@YifanAboutUsRouter.get("/detail/{id}", summary="获取品牌介绍详情", description="获取品牌介绍详情")
|
||||
async def get_yifan_about_us_detail_controller(
|
||||
id: int = Path(..., description="ID"),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_about_us:query"]))
|
||||
) -> JSONResponse:
|
||||
"""获取品牌介绍详情接口"""
|
||||
result_dict = await YifanAboutUsService.detail_yifan_about_us_service(auth=auth, id=id)
|
||||
log.info(f"获取品牌介绍详情成功 {id}")
|
||||
return SuccessResponse(data=result_dict, msg="获取品牌介绍详情成功")
|
||||
|
||||
@YifanAboutUsRouter.get("/list", summary="查询品牌介绍列表", description="查询品牌介绍列表")
|
||||
async def get_yifan_about_us_list_controller(
|
||||
page: PaginationQueryParam = Depends(),
|
||||
search: YifanAboutUsQueryParam = Depends(),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_about_us:query"]))
|
||||
) -> JSONResponse:
|
||||
"""查询品牌介绍列表接口(数据库分页)"""
|
||||
result_dict = await YifanAboutUsService.page_yifan_about_us_service(
|
||||
auth=auth,
|
||||
page_no=page.page_no if page.page_no is not None else 1,
|
||||
page_size=page.page_size if page.page_size is not None else 10,
|
||||
search=search,
|
||||
order_by=page.order_by
|
||||
)
|
||||
log.info("查询品牌介绍列表成功")
|
||||
return SuccessResponse(data=result_dict, msg="查询品牌介绍列表成功")
|
||||
|
||||
@YifanAboutUsRouter.post("/create", summary="创建品牌介绍", description="创建品牌介绍")
|
||||
async def create_yifan_about_us_controller(
|
||||
data: YifanAboutUsCreateSchema,
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_about_us:create"]))
|
||||
) -> JSONResponse:
|
||||
"""创建品牌介绍接口"""
|
||||
result_dict = await YifanAboutUsService.create_yifan_about_us_service(auth=auth, data=data)
|
||||
log.info("创建品牌介绍成功")
|
||||
return SuccessResponse(data=result_dict, msg="创建品牌介绍成功")
|
||||
|
||||
@YifanAboutUsRouter.put("/update/{id}", summary="修改品牌介绍", description="修改品牌介绍")
|
||||
async def update_yifan_about_us_controller(
|
||||
data: YifanAboutUsUpdateSchema,
|
||||
id: int = Path(..., description="ID"),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_about_us:update"]))
|
||||
) -> JSONResponse:
|
||||
"""修改品牌介绍接口"""
|
||||
result_dict = await YifanAboutUsService.update_yifan_about_us_service(auth=auth, id=id, data=data)
|
||||
log.info("修改品牌介绍成功")
|
||||
return SuccessResponse(data=result_dict, msg="修改品牌介绍成功")
|
||||
|
||||
@YifanAboutUsRouter.delete("/delete", summary="删除品牌介绍", description="删除品牌介绍")
|
||||
async def delete_yifan_about_us_controller(
|
||||
ids: list[int] = Body(..., description="ID列表"),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_about_us:delete"]))
|
||||
) -> JSONResponse:
|
||||
"""删除品牌介绍接口"""
|
||||
await YifanAboutUsService.delete_yifan_about_us_service(auth=auth, ids=ids)
|
||||
log.info(f"删除品牌介绍成功: {ids}")
|
||||
return SuccessResponse(msg="删除品牌介绍成功")
|
||||
|
||||
@YifanAboutUsRouter.patch("/available/setting", summary="批量修改品牌介绍状态", description="批量修改品牌介绍状态")
|
||||
async def batch_set_available_yifan_about_us_controller(
|
||||
data: BatchSetAvailable,
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_about_us:patch"]))
|
||||
) -> JSONResponse:
|
||||
"""批量修改品牌介绍状态接口"""
|
||||
await YifanAboutUsService.set_available_yifan_about_us_service(auth=auth, data=data)
|
||||
log.info(f"批量修改品牌介绍状态成功: {data.ids}")
|
||||
return SuccessResponse(msg="批量修改品牌介绍状态成功")
|
||||
|
||||
@YifanAboutUsRouter.post('/export', summary="导出品牌介绍", description="导出品牌介绍")
|
||||
async def export_yifan_about_us_list_controller(
|
||||
search: YifanAboutUsQueryParam = Depends(),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_about_us:export"]))
|
||||
) -> StreamingResponse:
|
||||
"""导出品牌介绍接口"""
|
||||
result_dict_list = await YifanAboutUsService.list_yifan_about_us_service(search=search, auth=auth)
|
||||
export_result = await YifanAboutUsService.batch_export_yifan_about_us_service(obj_list=result_dict_list)
|
||||
log.info('导出品牌介绍成功')
|
||||
|
||||
return StreamResponse(
|
||||
data=bytes2file_response(export_result),
|
||||
media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
headers={
|
||||
'Content-Disposition': 'attachment; filename=yifan_about_us.xlsx'
|
||||
}
|
||||
)
|
||||
|
||||
@YifanAboutUsRouter.post('/import', summary="导入品牌介绍", description="导入品牌介绍")
|
||||
async def import_yifan_about_us_list_controller(
|
||||
file: UploadFile,
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_about_us:import"]))
|
||||
) -> JSONResponse:
|
||||
"""导入品牌介绍接口"""
|
||||
batch_import_result = await YifanAboutUsService.batch_import_yifan_about_us_service(file=file, auth=auth, update_support=True)
|
||||
log.info("导入品牌介绍成功")
|
||||
|
||||
return SuccessResponse(data=batch_import_result, msg="导入品牌介绍成功")
|
||||
|
||||
@YifanAboutUsRouter.post('/download/template', summary="获取品牌介绍导入模板", description="获取品牌介绍导入模板", dependencies=[Depends(AuthPermission(["module_yifan:yifan_about_us:download"]))])
|
||||
async def export_yifan_about_us_template_controller() -> StreamingResponse:
|
||||
"""获取品牌介绍导入模板接口"""
|
||||
import_template_result = await YifanAboutUsService.import_template_download_yifan_about_us_service()
|
||||
log.info('获取品牌介绍导入模板成功')
|
||||
|
||||
return StreamResponse(
|
||||
data=bytes2file_response(import_template_result),
|
||||
media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
headers={'Content-Disposition': 'attachment; filename=yifan_about_us_template.xlsx'}
|
||||
)
|
||||
@@ -0,0 +1,133 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from typing import Sequence
|
||||
|
||||
from app.core.base_crud import CRUDBase
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from .model import YifanAboutUsModel
|
||||
from .schema import YifanAboutUsCreateSchema, YifanAboutUsUpdateSchema, YifanAboutUsOutSchema
|
||||
|
||||
|
||||
class YifanAboutUsCRUD(CRUDBase[YifanAboutUsModel, YifanAboutUsCreateSchema, YifanAboutUsUpdateSchema]):
|
||||
"""品牌介绍数据层"""
|
||||
|
||||
def __init__(self, auth: AuthSchema) -> None:
|
||||
"""
|
||||
初始化CRUD数据层
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
"""
|
||||
super().__init__(model=YifanAboutUsModel, auth=auth)
|
||||
|
||||
async def get_by_id_yifan_about_us_crud(self, id: int, preload: list | None = None) -> YifanAboutUsModel | None:
|
||||
"""
|
||||
详情
|
||||
|
||||
参数:
|
||||
- id (int): 对象ID
|
||||
- preload (list | None): 预加载关系,未提供时使用模型默认项
|
||||
|
||||
返回:
|
||||
- YifanAboutUsModel | None: 模型实例或None
|
||||
"""
|
||||
return await self.get(id=id, preload=preload)
|
||||
|
||||
async def list_yifan_about_us_crud(self, search: dict | None = None, order_by: list[dict] | None = None, preload: list | None = None) -> Sequence[YifanAboutUsModel]:
|
||||
"""
|
||||
列表查询
|
||||
|
||||
参数:
|
||||
- search (dict | None): 查询参数
|
||||
- order_by (list[dict] | None): 排序参数,未提供时使用模型默认项
|
||||
- preload (list | None): 预加载关系,未提供时使用模型默认项
|
||||
|
||||
返回:
|
||||
- Sequence[YifanAboutUsModel]: 模型实例序列
|
||||
"""
|
||||
return await self.list(search=search, order_by=order_by, preload=preload)
|
||||
|
||||
async def create_yifan_about_us_crud(self, data: YifanAboutUsCreateSchema) -> YifanAboutUsModel | None:
|
||||
"""
|
||||
创建
|
||||
|
||||
参数:
|
||||
- data (YifanAboutUsCreateSchema): 创建模型
|
||||
|
||||
返回:
|
||||
- YifanAboutUsModel | None: 模型实例或None
|
||||
"""
|
||||
return await self.create(data=data)
|
||||
|
||||
async def update_yifan_about_us_crud(self, id: int, data: YifanAboutUsUpdateSchema) -> YifanAboutUsModel | None:
|
||||
"""
|
||||
更新
|
||||
|
||||
参数:
|
||||
- id (int): 对象ID
|
||||
- data (YifanAboutUsUpdateSchema): 更新模型
|
||||
|
||||
返回:
|
||||
- YifanAboutUsModel | None: 模型实例或None
|
||||
"""
|
||||
return await self.update(id=id, data=data)
|
||||
|
||||
async def delete_yifan_about_us_crud(self, ids: list[int]) -> None:
|
||||
"""
|
||||
批量删除
|
||||
|
||||
参数:
|
||||
- ids (list[int]): 对象ID列表
|
||||
|
||||
返回:
|
||||
- None
|
||||
"""
|
||||
return await self.delete(ids=ids)
|
||||
|
||||
async def set_available_yifan_about_us_crud(self, ids: list[int], status: str) -> None:
|
||||
"""
|
||||
批量设置可用状态
|
||||
|
||||
参数:
|
||||
- ids (list[int]): 对象ID列表
|
||||
- status (str): 可用状态
|
||||
|
||||
返回:
|
||||
- None
|
||||
"""
|
||||
return await self.set(ids=ids, status=status)
|
||||
|
||||
async def page_yifan_about_us_crud(self, offset: int, limit: int, order_by: list[dict] | None = None, search: dict | None = None, preload: list | None = None) -> dict:
|
||||
"""
|
||||
分页查询
|
||||
|
||||
参数:
|
||||
- offset (int): 偏移量
|
||||
- limit (int): 每页数量
|
||||
- order_by (list[dict] | None): 排序参数,未提供时使用模型默认项
|
||||
- search (dict | None): 查询参数,未提供时查询所有
|
||||
- preload (list | None): 预加载关系,未提供时使用模型默认项
|
||||
|
||||
返回:
|
||||
- Dict: 分页数据
|
||||
"""
|
||||
order_by_list = order_by or [{'id': 'asc'}]
|
||||
search_dict = search or {}
|
||||
return await self.page(
|
||||
offset=offset,
|
||||
limit=limit,
|
||||
order_by=order_by_list,
|
||||
search=search_dict,
|
||||
out_schema=YifanAboutUsOutSchema,
|
||||
preload=preload
|
||||
)
|
||||
|
||||
async def get_first_available_yifan_about_us_crud(self) -> YifanAboutUsModel | None:
|
||||
"""
|
||||
获取第一条启用的品牌介绍(小程序用)
|
||||
|
||||
返回:
|
||||
- YifanAboutUsModel | None: 模型实例或None
|
||||
"""
|
||||
result = await self.list(search={'status': '0'}, order_by=[{'id': 'asc'}])
|
||||
return result[0] if result else None
|
||||
@@ -0,0 +1,18 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from sqlalchemy import Text, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.base_model import ModelMixin, UserMixin
|
||||
|
||||
|
||||
class YifanAboutUsModel(ModelMixin, UserMixin):
|
||||
"""
|
||||
品牌介绍表
|
||||
"""
|
||||
__tablename__: str = 'yifan_about_us'
|
||||
__table_args__: dict[str, str] = {'comment': '品牌介绍'}
|
||||
__loader_options__: list[str] = ["created_by", "updated_by"]
|
||||
|
||||
title: Mapped[str | None] = mapped_column(String(100), nullable=True, comment='标题')
|
||||
content: Mapped[str | None] = mapped_column(Text, nullable=True, comment='内容')
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from fastapi import Query
|
||||
|
||||
from app.core.validator import DateTimeStr
|
||||
from app.core.base_schema import BaseSchema, UserBySchema
|
||||
|
||||
class YifanAboutUsCreateSchema(BaseModel):
|
||||
"""
|
||||
品牌介绍新增模型
|
||||
"""
|
||||
title: str | None = Field(default=None, description='标题')
|
||||
content: str | None = Field(default=None, description='内容')
|
||||
|
||||
|
||||
class YifanAboutUsUpdateSchema(YifanAboutUsCreateSchema):
|
||||
"""
|
||||
品牌介绍更新模型
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
class YifanAboutUsOutSchema(YifanAboutUsCreateSchema, BaseSchema, UserBySchema):
|
||||
"""
|
||||
品牌介绍响应模型
|
||||
"""
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class YifanAboutUsQueryParam:
|
||||
"""品牌介绍查询参数"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
title: str | None = Query(None, description="标题"),
|
||||
content: str | None = Query(None, description="内容"),
|
||||
created_id: int | None = Query(None, description="创建人ID"),
|
||||
updated_id: int | None = Query(None, description="更新人ID"),
|
||||
created_time: list[DateTimeStr] | None = Query(None, description="创建时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]),
|
||||
updated_time: list[DateTimeStr] | None = Query(None, description="更新时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]),
|
||||
|
||||
) -> None:
|
||||
|
||||
# 模糊查询字段
|
||||
self.title = ("like", title)
|
||||
# 模糊查询字段
|
||||
self.content = ("like", content)
|
||||
# 精确查询字段
|
||||
self.created_id = created_id
|
||||
# 精确查询字段
|
||||
self.updated_id = updated_id
|
||||
# 时间范围查询
|
||||
if created_time and len(created_time) == 2:
|
||||
self.created_time = ("between", (created_time[0], created_time[1]))
|
||||
if updated_time and len(updated_time) == 2:
|
||||
self.updated_time = ("between", (updated_time[0], updated_time[1]))
|
||||
@@ -0,0 +1,199 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import io
|
||||
from fastapi import UploadFile
|
||||
import pandas as pd
|
||||
|
||||
from app.core.base_schema import BatchSetAvailable
|
||||
from app.core.exceptions import CustomException
|
||||
from app.utils.excel_util import ExcelUtil
|
||||
from app.core.logger import log
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from .schema import YifanAboutUsCreateSchema, YifanAboutUsUpdateSchema, YifanAboutUsOutSchema, YifanAboutUsQueryParam
|
||||
from .crud import YifanAboutUsCRUD
|
||||
|
||||
|
||||
class YifanAboutUsService:
|
||||
"""
|
||||
品牌介绍服务层
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
async def detail_yifan_about_us_service(cls, auth: AuthSchema, id: int) -> dict:
|
||||
"""详情"""
|
||||
obj = await YifanAboutUsCRUD(auth).get_by_id_yifan_about_us_crud(id=id)
|
||||
if not obj:
|
||||
raise CustomException(msg="该数据不存在")
|
||||
return YifanAboutUsOutSchema.model_validate(obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def get_miniapp_about_us_service(cls, auth: AuthSchema) -> dict | None:
|
||||
"""小程序获取品牌介绍(获取第一条启用的数据)"""
|
||||
obj = await YifanAboutUsCRUD(auth).get_first_available_yifan_about_us_crud()
|
||||
if not obj:
|
||||
return None
|
||||
return YifanAboutUsOutSchema.model_validate(obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def list_yifan_about_us_service(cls, auth: AuthSchema, search: YifanAboutUsQueryParam | None = None, order_by: list[dict] | None = None) -> list[dict]:
|
||||
"""列表查询"""
|
||||
search_dict = search.__dict__ if search else None
|
||||
obj_list = await YifanAboutUsCRUD(auth).list_yifan_about_us_crud(search=search_dict, order_by=order_by)
|
||||
return [YifanAboutUsOutSchema.model_validate(obj).model_dump() for obj in obj_list]
|
||||
|
||||
@classmethod
|
||||
async def page_yifan_about_us_service(cls, auth: AuthSchema, page_no: int, page_size: int, search: YifanAboutUsQueryParam | None = None, order_by: list[dict] | None = None) -> dict:
|
||||
"""分页查询(数据库分页)"""
|
||||
search_dict = search.__dict__ if search else {}
|
||||
order_by_list = order_by or [{'id': 'asc'}]
|
||||
offset = (page_no - 1) * page_size
|
||||
result = await YifanAboutUsCRUD(auth).page_yifan_about_us_crud(
|
||||
offset=offset,
|
||||
limit=page_size,
|
||||
order_by=order_by_list,
|
||||
search=search_dict
|
||||
)
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
async def create_yifan_about_us_service(cls, auth: AuthSchema, data: YifanAboutUsCreateSchema) -> dict:
|
||||
"""创建"""
|
||||
# 检查唯一性约束
|
||||
obj = await YifanAboutUsCRUD(auth).create_yifan_about_us_crud(data=data)
|
||||
return YifanAboutUsOutSchema.model_validate(obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def update_yifan_about_us_service(cls, auth: AuthSchema, id: int, data: YifanAboutUsUpdateSchema) -> dict:
|
||||
"""更新"""
|
||||
# 检查数据是否存在
|
||||
obj = await YifanAboutUsCRUD(auth).get_by_id_yifan_about_us_crud(id=id)
|
||||
if not obj:
|
||||
raise CustomException(msg='更新失败,该数据不存在')
|
||||
|
||||
# 检查唯一性约束
|
||||
|
||||
obj = await YifanAboutUsCRUD(auth).update_yifan_about_us_crud(id=id, data=data)
|
||||
return YifanAboutUsOutSchema.model_validate(obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def delete_yifan_about_us_service(cls, auth: AuthSchema, ids: list[int]) -> None:
|
||||
"""删除"""
|
||||
if len(ids) < 1:
|
||||
raise CustomException(msg='删除失败,删除对象不能为空')
|
||||
for id in ids:
|
||||
obj = await YifanAboutUsCRUD(auth).get_by_id_yifan_about_us_crud(id=id)
|
||||
if not obj:
|
||||
raise CustomException(msg=f'删除失败,ID为{id}的数据不存在')
|
||||
await YifanAboutUsCRUD(auth).delete_yifan_about_us_crud(ids=ids)
|
||||
|
||||
@classmethod
|
||||
async def set_available_yifan_about_us_service(cls, auth: AuthSchema, data: BatchSetAvailable) -> None:
|
||||
"""批量设置状态"""
|
||||
await YifanAboutUsCRUD(auth).set_available_yifan_about_us_crud(ids=data.ids, status=data.status)
|
||||
|
||||
@classmethod
|
||||
async def batch_export_yifan_about_us_service(cls, obj_list: list[dict]) -> bytes:
|
||||
"""批量导出"""
|
||||
mapping_dict = {
|
||||
'id': '主键ID',
|
||||
'title': '标题',
|
||||
'content': '内容',
|
||||
'created_id': '创建人ID',
|
||||
'updated_id': '更新人ID',
|
||||
}
|
||||
|
||||
data = obj_list.copy()
|
||||
for item in data:
|
||||
# 状态转换
|
||||
if 'status' in item:
|
||||
item['status'] = '启用' if item.get('status') == '0' else '停用'
|
||||
# 创建者转换
|
||||
creator_info = item.get('creator')
|
||||
if isinstance(creator_info, dict):
|
||||
item['creator'] = creator_info.get('name', '未知')
|
||||
elif creator_info is None:
|
||||
item['creator'] = '未知'
|
||||
|
||||
return ExcelUtil.export_list2excel(list_data=data, mapping_dict=mapping_dict)
|
||||
|
||||
@classmethod
|
||||
async def batch_import_yifan_about_us_service(cls, auth: AuthSchema, file: UploadFile, update_support: bool = False) -> str:
|
||||
"""批量导入"""
|
||||
header_dict = {
|
||||
'主键ID': 'id',
|
||||
'标题': 'title',
|
||||
'内容': 'content',
|
||||
'创建人ID': 'created_id',
|
||||
'更新人ID': 'updated_id',
|
||||
}
|
||||
|
||||
try:
|
||||
contents = await file.read()
|
||||
df = pd.read_excel(io.BytesIO(contents))
|
||||
await file.close()
|
||||
|
||||
if df.empty:
|
||||
raise CustomException(msg="导入文件为空")
|
||||
|
||||
missing_headers = [header for header in header_dict.keys() if header not in df.columns]
|
||||
if missing_headers:
|
||||
raise CustomException(msg=f"导入文件缺少必要的列: {', '.join(missing_headers)}")
|
||||
|
||||
df.rename(columns=header_dict, inplace=True)
|
||||
|
||||
# 验证必填字段
|
||||
|
||||
error_msgs = []
|
||||
success_count = 0
|
||||
count = 0
|
||||
|
||||
for index, row in df.iterrows():
|
||||
count += 1
|
||||
try:
|
||||
data = {
|
||||
"id": row['id'],
|
||||
"title": row['title'],
|
||||
"content": row['content'],
|
||||
"created_id": row['created_id'],
|
||||
"updated_id": row['updated_id'],
|
||||
}
|
||||
# 使用CreateSchema做校验后入库
|
||||
create_schema = YifanAboutUsCreateSchema.model_validate(data)
|
||||
|
||||
# 检查唯一性约束
|
||||
|
||||
await YifanAboutUsCRUD(auth).create_yifan_about_us_crud(data=create_schema)
|
||||
success_count += 1
|
||||
except Exception as e:
|
||||
error_msgs.append(f"第{count}行: {str(e)}")
|
||||
continue
|
||||
|
||||
result = f"成功导入 {success_count} 条数据"
|
||||
if error_msgs:
|
||||
result += "\n错误信息:\n" + "\n".join(error_msgs)
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
log.error(f"批量导入失败: {str(e)}")
|
||||
raise CustomException(msg=f"导入失败: {str(e)}")
|
||||
|
||||
@classmethod
|
||||
async def import_template_download_yifan_about_us_service(cls) -> bytes:
|
||||
"""下载导入模板"""
|
||||
header_list = [
|
||||
'主键ID',
|
||||
'标题',
|
||||
'内容',
|
||||
'创建人ID',
|
||||
'更新人ID',
|
||||
]
|
||||
selector_header_list = []
|
||||
option_list = []
|
||||
|
||||
# 添加下拉选项
|
||||
|
||||
return ExcelUtil.get_excel_template(
|
||||
header_list=header_list,
|
||||
selector_header_list=selector_header_list,
|
||||
option_list=option_list
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
@@ -0,0 +1,137 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from fastapi import APIRouter, Depends, UploadFile, Body, Path, Query
|
||||
from fastapi.responses import StreamingResponse, JSONResponse
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.common.response import SuccessResponse, StreamResponse
|
||||
from app.core.dependencies import AuthPermission, db_getter
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from app.core.base_params import PaginationQueryParam
|
||||
from app.utils.common_util import bytes2file_response
|
||||
from app.core.logger import log
|
||||
from app.core.base_schema import BatchSetAvailable
|
||||
|
||||
from .service import YifanAboutVideoService
|
||||
from .schema import YifanAboutVideoCreateSchema, YifanAboutVideoUpdateSchema, YifanAboutVideoQueryParam
|
||||
|
||||
YifanAboutVideoRouter = APIRouter(prefix='/yifan_about_video', tags=["视频内容模块"])
|
||||
|
||||
@YifanAboutVideoRouter.get("/miniapp/info", summary="小程序获取视频内容", description="小程序获取视频内容(获取启用的第一条)")
|
||||
async def get_miniapp_about_video_controller(
|
||||
db: AsyncSession = Depends(db_getter)
|
||||
) -> JSONResponse:
|
||||
"""小程序获取视频内容接口"""
|
||||
auth = AuthSchema(db=db)
|
||||
result_dict = await YifanAboutVideoService.get_miniapp_about_video_service(auth=auth)
|
||||
log.info("小程序获取视频内容成功")
|
||||
return SuccessResponse(data=result_dict, msg="获取视频内容成功")
|
||||
|
||||
@YifanAboutVideoRouter.get("/detail/{id}", summary="获取视频内容详情", description="获取视频内容详情")
|
||||
async def get_yifan_about_video_detail_controller(
|
||||
id: int = Path(..., description="ID"),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_about_video:query"]))
|
||||
) -> JSONResponse:
|
||||
"""获取视频内容详情接口"""
|
||||
result_dict = await YifanAboutVideoService.detail_yifan_about_video_service(auth=auth, id=id)
|
||||
log.info(f"获取视频内容详情成功 {id}")
|
||||
return SuccessResponse(data=result_dict, msg="获取视频内容详情成功")
|
||||
|
||||
@YifanAboutVideoRouter.get("/list", summary="查询视频内容列表", description="查询视频内容列表")
|
||||
async def get_yifan_about_video_list_controller(
|
||||
page: PaginationQueryParam = Depends(),
|
||||
search: YifanAboutVideoQueryParam = Depends(),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_about_video:query"]))
|
||||
) -> JSONResponse:
|
||||
"""查询视频内容列表接口(数据库分页)"""
|
||||
result_dict = await YifanAboutVideoService.page_yifan_about_video_service(
|
||||
auth=auth,
|
||||
page_no=page.page_no if page.page_no is not None else 1,
|
||||
page_size=page.page_size if page.page_size is not None else 10,
|
||||
search=search,
|
||||
order_by=page.order_by
|
||||
)
|
||||
log.info("查询视频内容列表成功")
|
||||
return SuccessResponse(data=result_dict, msg="查询视频内容列表成功")
|
||||
|
||||
@YifanAboutVideoRouter.post("/create", summary="创建视频内容", description="创建视频内容")
|
||||
async def create_yifan_about_video_controller(
|
||||
data: YifanAboutVideoCreateSchema,
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_about_video:create"]))
|
||||
) -> JSONResponse:
|
||||
"""创建视频内容接口"""
|
||||
result_dict = await YifanAboutVideoService.create_yifan_about_video_service(auth=auth, data=data)
|
||||
log.info("创建视频内容成功")
|
||||
return SuccessResponse(data=result_dict, msg="创建视频内容成功")
|
||||
|
||||
@YifanAboutVideoRouter.put("/update/{id}", summary="修改视频内容", description="修改视频内容")
|
||||
async def update_yifan_about_video_controller(
|
||||
data: YifanAboutVideoUpdateSchema,
|
||||
id: int = Path(..., description="ID"),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_about_video:update"]))
|
||||
) -> JSONResponse:
|
||||
"""修改视频内容接口"""
|
||||
result_dict = await YifanAboutVideoService.update_yifan_about_video_service(auth=auth, id=id, data=data)
|
||||
log.info("修改视频内容成功")
|
||||
return SuccessResponse(data=result_dict, msg="修改视频内容成功")
|
||||
|
||||
@YifanAboutVideoRouter.delete("/delete", summary="删除视频内容", description="删除视频内容")
|
||||
async def delete_yifan_about_video_controller(
|
||||
ids: list[int] = Body(..., description="ID列表"),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_about_video:delete"]))
|
||||
) -> JSONResponse:
|
||||
"""删除视频内容接口"""
|
||||
await YifanAboutVideoService.delete_yifan_about_video_service(auth=auth, ids=ids)
|
||||
log.info(f"删除视频内容成功: {ids}")
|
||||
return SuccessResponse(msg="删除视频内容成功")
|
||||
|
||||
@YifanAboutVideoRouter.patch("/available/setting", summary="批量修改视频内容状态", description="批量修改视频内容状态")
|
||||
async def batch_set_available_yifan_about_video_controller(
|
||||
data: BatchSetAvailable,
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_about_video:patch"]))
|
||||
) -> JSONResponse:
|
||||
"""批量修改视频内容状态接口"""
|
||||
await YifanAboutVideoService.set_available_yifan_about_video_service(auth=auth, data=data)
|
||||
log.info(f"批量修改视频内容状态成功: {data.ids}")
|
||||
return SuccessResponse(msg="批量修改视频内容状态成功")
|
||||
|
||||
@YifanAboutVideoRouter.post('/export', summary="导出视频内容", description="导出视频内容")
|
||||
async def export_yifan_about_video_list_controller(
|
||||
search: YifanAboutVideoQueryParam = Depends(),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_about_video:export"]))
|
||||
) -> StreamingResponse:
|
||||
"""导出视频内容接口"""
|
||||
result_dict_list = await YifanAboutVideoService.list_yifan_about_video_service(search=search, auth=auth)
|
||||
export_result = await YifanAboutVideoService.batch_export_yifan_about_video_service(obj_list=result_dict_list)
|
||||
log.info('导出视频内容成功')
|
||||
|
||||
return StreamResponse(
|
||||
data=bytes2file_response(export_result),
|
||||
media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
headers={
|
||||
'Content-Disposition': 'attachment; filename=yifan_about_video.xlsx'
|
||||
}
|
||||
)
|
||||
|
||||
@YifanAboutVideoRouter.post('/import', summary="导入视频内容", description="导入视频内容")
|
||||
async def import_yifan_about_video_list_controller(
|
||||
file: UploadFile,
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_about_video:import"]))
|
||||
) -> JSONResponse:
|
||||
"""导入视频内容接口"""
|
||||
batch_import_result = await YifanAboutVideoService.batch_import_yifan_about_video_service(file=file, auth=auth, update_support=True)
|
||||
log.info("导入视频内容成功")
|
||||
|
||||
return SuccessResponse(data=batch_import_result, msg="导入视频内容成功")
|
||||
|
||||
@YifanAboutVideoRouter.post('/download/template', summary="获取视频内容导入模板", description="获取视频内容导入模板", dependencies=[Depends(AuthPermission(["module_yifan:yifan_about_video:download"]))])
|
||||
async def export_yifan_about_video_template_controller() -> StreamingResponse:
|
||||
"""获取视频内容导入模板接口"""
|
||||
import_template_result = await YifanAboutVideoService.import_template_download_yifan_about_video_service()
|
||||
log.info('获取视频内容导入模板成功')
|
||||
|
||||
return StreamResponse(
|
||||
data=bytes2file_response(import_template_result),
|
||||
media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
headers={'Content-Disposition': 'attachment; filename=yifan_about_video_template.xlsx'}
|
||||
)
|
||||
@@ -0,0 +1,133 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from typing import Sequence
|
||||
|
||||
from app.core.base_crud import CRUDBase
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from .model import YifanAboutVideoModel
|
||||
from .schema import YifanAboutVideoCreateSchema, YifanAboutVideoUpdateSchema, YifanAboutVideoOutSchema
|
||||
|
||||
|
||||
class YifanAboutVideoCRUD(CRUDBase[YifanAboutVideoModel, YifanAboutVideoCreateSchema, YifanAboutVideoUpdateSchema]):
|
||||
"""视频内容数据层"""
|
||||
|
||||
def __init__(self, auth: AuthSchema) -> None:
|
||||
"""
|
||||
初始化CRUD数据层
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
"""
|
||||
super().__init__(model=YifanAboutVideoModel, auth=auth)
|
||||
|
||||
async def get_by_id_yifan_about_video_crud(self, id: int, preload: list | None = None) -> YifanAboutVideoModel | None:
|
||||
"""
|
||||
详情
|
||||
|
||||
参数:
|
||||
- id (int): 对象ID
|
||||
- preload (list | None): 预加载关系,未提供时使用模型默认项
|
||||
|
||||
返回:
|
||||
- YifanAboutVideoModel | None: 模型实例或None
|
||||
"""
|
||||
return await self.get(id=id, preload=preload)
|
||||
|
||||
async def list_yifan_about_video_crud(self, search: dict | None = None, order_by: list[dict] | None = None, preload: list | None = None) -> Sequence[YifanAboutVideoModel]:
|
||||
"""
|
||||
列表查询
|
||||
|
||||
参数:
|
||||
- search (dict | None): 查询参数
|
||||
- order_by (list[dict] | None): 排序参数,未提供时使用模型默认项
|
||||
- preload (list | None): 预加载关系,未提供时使用模型默认项
|
||||
|
||||
返回:
|
||||
- Sequence[YifanAboutVideoModel]: 模型实例序列
|
||||
"""
|
||||
return await self.list(search=search, order_by=order_by, preload=preload)
|
||||
|
||||
async def create_yifan_about_video_crud(self, data: YifanAboutVideoCreateSchema) -> YifanAboutVideoModel | None:
|
||||
"""
|
||||
创建
|
||||
|
||||
参数:
|
||||
- data (YifanAboutVideoCreateSchema): 创建模型
|
||||
|
||||
返回:
|
||||
- YifanAboutVideoModel | None: 模型实例或None
|
||||
"""
|
||||
return await self.create(data=data)
|
||||
|
||||
async def update_yifan_about_video_crud(self, id: int, data: YifanAboutVideoUpdateSchema) -> YifanAboutVideoModel | None:
|
||||
"""
|
||||
更新
|
||||
|
||||
参数:
|
||||
- id (int): 对象ID
|
||||
- data (YifanAboutVideoUpdateSchema): 更新模型
|
||||
|
||||
返回:
|
||||
- YifanAboutVideoModel | None: 模型实例或None
|
||||
"""
|
||||
return await self.update(id=id, data=data)
|
||||
|
||||
async def delete_yifan_about_video_crud(self, ids: list[int]) -> None:
|
||||
"""
|
||||
批量删除
|
||||
|
||||
参数:
|
||||
- ids (list[int]): 对象ID列表
|
||||
|
||||
返回:
|
||||
- None
|
||||
"""
|
||||
return await self.delete(ids=ids)
|
||||
|
||||
async def set_available_yifan_about_video_crud(self, ids: list[int], status: str) -> None:
|
||||
"""
|
||||
批量设置可用状态
|
||||
|
||||
参数:
|
||||
- ids (list[int]): 对象ID列表
|
||||
- status (str): 可用状态
|
||||
|
||||
返回:
|
||||
- None
|
||||
"""
|
||||
return await self.set(ids=ids, status=status)
|
||||
|
||||
async def page_yifan_about_video_crud(self, offset: int, limit: int, order_by: list[dict] | None = None, search: dict | None = None, preload: list | None = None) -> dict:
|
||||
"""
|
||||
分页查询
|
||||
|
||||
参数:
|
||||
- offset (int): 偏移量
|
||||
- limit (int): 每页数量
|
||||
- order_by (list[dict] | None): 排序参数,未提供时使用模型默认项
|
||||
- search (dict | None): 查询参数,未提供时查询所有
|
||||
- preload (list | None): 预加载关系,未提供时使用模型默认项
|
||||
|
||||
返回:
|
||||
- Dict: 分页数据
|
||||
"""
|
||||
order_by_list = order_by or [{'id': 'asc'}]
|
||||
search_dict = search or {}
|
||||
return await self.page(
|
||||
offset=offset,
|
||||
limit=limit,
|
||||
order_by=order_by_list,
|
||||
search=search_dict,
|
||||
out_schema=YifanAboutVideoOutSchema,
|
||||
preload=preload
|
||||
)
|
||||
|
||||
async def get_first_available_yifan_about_video_crud(self) -> YifanAboutVideoModel | None:
|
||||
"""
|
||||
获取第一条启用的视频内容(小程序用)
|
||||
|
||||
返回:
|
||||
- YifanAboutVideoModel | None: 模型实例或None
|
||||
"""
|
||||
result = await self.list(search={'status': '0'}, order_by=[{'id': 'asc'}])
|
||||
return result[0] if result else None
|
||||
@@ -0,0 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from sqlalchemy import String
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.base_model import ModelMixin, UserMixin
|
||||
|
||||
|
||||
class YifanAboutVideoModel(ModelMixin, UserMixin):
|
||||
"""
|
||||
视频内容表
|
||||
"""
|
||||
__tablename__: str = 'yifan_about_video'
|
||||
__table_args__: dict[str, str] = {'comment': '视频内容'}
|
||||
__loader_options__: list[str] = ["created_by", "updated_by"]
|
||||
|
||||
title: Mapped[str | None] = mapped_column(String(100), nullable=True, comment='视频标题')
|
||||
subtitle: Mapped[str | None] = mapped_column(String(200), nullable=True, comment='副标题/期数')
|
||||
cover_url: Mapped[str | None] = mapped_column(String(500), nullable=True, comment='封面图URL')
|
||||
video_url: Mapped[str | None] = mapped_column(String(500), nullable=True, comment='视频URL')
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from fastapi import Query
|
||||
|
||||
from app.core.validator import DateTimeStr
|
||||
from app.core.base_schema import BaseSchema, UserBySchema
|
||||
|
||||
class YifanAboutVideoCreateSchema(BaseModel):
|
||||
"""
|
||||
视频内容新增模型
|
||||
"""
|
||||
title: str | None = Field(default=None, description='视频标题')
|
||||
subtitle: str | None = Field(default=None, description='副标题/期数')
|
||||
cover_url: str | None = Field(default=None, description='封面图URL')
|
||||
video_url: str | None = Field(default=None, description='视频URL')
|
||||
status: int = Field(default=0, description='状态:0禁用 1启用')
|
||||
|
||||
|
||||
class YifanAboutVideoUpdateSchema(YifanAboutVideoCreateSchema):
|
||||
"""
|
||||
视频内容更新模型
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
class YifanAboutVideoOutSchema(YifanAboutVideoCreateSchema, BaseSchema, UserBySchema):
|
||||
"""
|
||||
视频内容响应模型
|
||||
"""
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class YifanAboutVideoQueryParam:
|
||||
"""视频内容查询参数"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
title: str | None = Query(None, description="视频标题"),
|
||||
subtitle: str | None = Query(None, description="副标题/期数"),
|
||||
cover_url: str | None = Query(None, description="封面图URL"),
|
||||
video_url: str | None = Query(None, description="视频URL"),
|
||||
status: int | None = Query(None, description="状态:0禁用 1启用"),
|
||||
created_id: int | None = Query(None, description="创建人ID"),
|
||||
updated_id: int | None = Query(None, description="更新人ID"),
|
||||
created_time: list[DateTimeStr] | None = Query(None, description="创建时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]),
|
||||
updated_time: list[DateTimeStr] | None = Query(None, description="更新时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]),
|
||||
|
||||
) -> None:
|
||||
|
||||
# 模糊查询字段
|
||||
self.title = ("like", title)
|
||||
# 模糊查询字段
|
||||
self.subtitle = ("like", subtitle)
|
||||
# 模糊查询字段
|
||||
self.cover_url = ("like", cover_url)
|
||||
# 模糊查询字段
|
||||
self.video_url = ("like", video_url)
|
||||
# 精确查询字段
|
||||
self.status = status
|
||||
# 精确查询字段
|
||||
self.created_id = created_id
|
||||
# 精确查询字段
|
||||
self.updated_id = updated_id
|
||||
# 时间范围查询
|
||||
if created_time and len(created_time) == 2:
|
||||
self.created_time = ("between", (created_time[0], created_time[1]))
|
||||
if updated_time and len(updated_time) == 2:
|
||||
self.updated_time = ("between", (updated_time[0], updated_time[1]))
|
||||
@@ -0,0 +1,211 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import io
|
||||
from fastapi import UploadFile
|
||||
import pandas as pd
|
||||
|
||||
from app.core.base_schema import BatchSetAvailable
|
||||
from app.core.exceptions import CustomException
|
||||
from app.utils.excel_util import ExcelUtil
|
||||
from app.core.logger import log
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from .schema import YifanAboutVideoCreateSchema, YifanAboutVideoUpdateSchema, YifanAboutVideoOutSchema, YifanAboutVideoQueryParam
|
||||
from .crud import YifanAboutVideoCRUD
|
||||
|
||||
|
||||
class YifanAboutVideoService:
|
||||
"""
|
||||
视频内容服务层
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
async def detail_yifan_about_video_service(cls, auth: AuthSchema, id: int) -> dict:
|
||||
"""详情"""
|
||||
obj = await YifanAboutVideoCRUD(auth).get_by_id_yifan_about_video_crud(id=id)
|
||||
if not obj:
|
||||
raise CustomException(msg="该数据不存在")
|
||||
return YifanAboutVideoOutSchema.model_validate(obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def get_miniapp_about_video_service(cls, auth: AuthSchema) -> dict | None:
|
||||
"""小程序获取视频内容(获取第一条启用的数据)"""
|
||||
obj = await YifanAboutVideoCRUD(auth).get_first_available_yifan_about_video_crud()
|
||||
if not obj:
|
||||
return None
|
||||
return YifanAboutVideoOutSchema.model_validate(obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def list_yifan_about_video_service(cls, auth: AuthSchema, search: YifanAboutVideoQueryParam | None = None, order_by: list[dict] | None = None) -> list[dict]:
|
||||
"""列表查询"""
|
||||
search_dict = search.__dict__ if search else None
|
||||
obj_list = await YifanAboutVideoCRUD(auth).list_yifan_about_video_crud(search=search_dict, order_by=order_by)
|
||||
return [YifanAboutVideoOutSchema.model_validate(obj).model_dump() for obj in obj_list]
|
||||
|
||||
@classmethod
|
||||
async def page_yifan_about_video_service(cls, auth: AuthSchema, page_no: int, page_size: int, search: YifanAboutVideoQueryParam | None = None, order_by: list[dict] | None = None) -> dict:
|
||||
"""分页查询(数据库分页)"""
|
||||
search_dict = search.__dict__ if search else {}
|
||||
order_by_list = order_by or [{'id': 'asc'}]
|
||||
offset = (page_no - 1) * page_size
|
||||
result = await YifanAboutVideoCRUD(auth).page_yifan_about_video_crud(
|
||||
offset=offset,
|
||||
limit=page_size,
|
||||
order_by=order_by_list,
|
||||
search=search_dict
|
||||
)
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
async def create_yifan_about_video_service(cls, auth: AuthSchema, data: YifanAboutVideoCreateSchema) -> dict:
|
||||
"""创建"""
|
||||
# 检查唯一性约束
|
||||
obj = await YifanAboutVideoCRUD(auth).create_yifan_about_video_crud(data=data)
|
||||
return YifanAboutVideoOutSchema.model_validate(obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def update_yifan_about_video_service(cls, auth: AuthSchema, id: int, data: YifanAboutVideoUpdateSchema) -> dict:
|
||||
"""更新"""
|
||||
# 检查数据是否存在
|
||||
obj = await YifanAboutVideoCRUD(auth).get_by_id_yifan_about_video_crud(id=id)
|
||||
if not obj:
|
||||
raise CustomException(msg='更新失败,该数据不存在')
|
||||
|
||||
# 检查唯一性约束
|
||||
|
||||
obj = await YifanAboutVideoCRUD(auth).update_yifan_about_video_crud(id=id, data=data)
|
||||
return YifanAboutVideoOutSchema.model_validate(obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def delete_yifan_about_video_service(cls, auth: AuthSchema, ids: list[int]) -> None:
|
||||
"""删除"""
|
||||
if len(ids) < 1:
|
||||
raise CustomException(msg='删除失败,删除对象不能为空')
|
||||
for id in ids:
|
||||
obj = await YifanAboutVideoCRUD(auth).get_by_id_yifan_about_video_crud(id=id)
|
||||
if not obj:
|
||||
raise CustomException(msg=f'删除失败,ID为{id}的数据不存在')
|
||||
await YifanAboutVideoCRUD(auth).delete_yifan_about_video_crud(ids=ids)
|
||||
|
||||
@classmethod
|
||||
async def set_available_yifan_about_video_service(cls, auth: AuthSchema, data: BatchSetAvailable) -> None:
|
||||
"""批量设置状态"""
|
||||
await YifanAboutVideoCRUD(auth).set_available_yifan_about_video_crud(ids=data.ids, status=data.status)
|
||||
|
||||
@classmethod
|
||||
async def batch_export_yifan_about_video_service(cls, obj_list: list[dict]) -> bytes:
|
||||
"""批量导出"""
|
||||
mapping_dict = {
|
||||
'id': '主键ID',
|
||||
'title': '视频标题',
|
||||
'subtitle': '副标题/期数',
|
||||
'cover_url': '封面图URL',
|
||||
'video_url': '视频URL',
|
||||
'status': '状态:0禁用 1启用',
|
||||
'created_id': '创建人ID',
|
||||
'updated_id': '更新人ID',
|
||||
}
|
||||
|
||||
data = obj_list.copy()
|
||||
for item in data:
|
||||
# 状态转换
|
||||
if 'status' in item:
|
||||
item['status'] = '启用' if item.get('status') == '0' else '停用'
|
||||
# 创建者转换
|
||||
creator_info = item.get('creator')
|
||||
if isinstance(creator_info, dict):
|
||||
item['creator'] = creator_info.get('name', '未知')
|
||||
elif creator_info is None:
|
||||
item['creator'] = '未知'
|
||||
|
||||
return ExcelUtil.export_list2excel(list_data=data, mapping_dict=mapping_dict)
|
||||
|
||||
@classmethod
|
||||
async def batch_import_yifan_about_video_service(cls, auth: AuthSchema, file: UploadFile, update_support: bool = False) -> str:
|
||||
"""批量导入"""
|
||||
header_dict = {
|
||||
'主键ID': 'id',
|
||||
'视频标题': 'title',
|
||||
'副标题/期数': 'subtitle',
|
||||
'封面图URL': 'cover_url',
|
||||
'视频URL': 'video_url',
|
||||
'状态:0禁用 1启用': 'status',
|
||||
'创建人ID': 'created_id',
|
||||
'更新人ID': 'updated_id',
|
||||
}
|
||||
|
||||
try:
|
||||
contents = await file.read()
|
||||
df = pd.read_excel(io.BytesIO(contents))
|
||||
await file.close()
|
||||
|
||||
if df.empty:
|
||||
raise CustomException(msg="导入文件为空")
|
||||
|
||||
missing_headers = [header for header in header_dict.keys() if header not in df.columns]
|
||||
if missing_headers:
|
||||
raise CustomException(msg=f"导入文件缺少必要的列: {', '.join(missing_headers)}")
|
||||
|
||||
df.rename(columns=header_dict, inplace=True)
|
||||
|
||||
# 验证必填字段
|
||||
|
||||
error_msgs = []
|
||||
success_count = 0
|
||||
count = 0
|
||||
|
||||
for index, row in df.iterrows():
|
||||
count += 1
|
||||
try:
|
||||
data = {
|
||||
"id": row['id'],
|
||||
"title": row['title'],
|
||||
"subtitle": row['subtitle'],
|
||||
"cover_url": row['cover_url'],
|
||||
"video_url": row['video_url'],
|
||||
"status": row['status'],
|
||||
"created_id": row['created_id'],
|
||||
"updated_id": row['updated_id'],
|
||||
}
|
||||
# 使用CreateSchema做校验后入库
|
||||
create_schema = YifanAboutVideoCreateSchema.model_validate(data)
|
||||
|
||||
# 检查唯一性约束
|
||||
|
||||
await YifanAboutVideoCRUD(auth).create_yifan_about_video_crud(data=create_schema)
|
||||
success_count += 1
|
||||
except Exception as e:
|
||||
error_msgs.append(f"第{count}行: {str(e)}")
|
||||
continue
|
||||
|
||||
result = f"成功导入 {success_count} 条数据"
|
||||
if error_msgs:
|
||||
result += "\n错误信息:\n" + "\n".join(error_msgs)
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
log.error(f"批量导入失败: {str(e)}")
|
||||
raise CustomException(msg=f"导入失败: {str(e)}")
|
||||
|
||||
@classmethod
|
||||
async def import_template_download_yifan_about_video_service(cls) -> bytes:
|
||||
"""下载导入模板"""
|
||||
header_list = [
|
||||
'主键ID',
|
||||
'视频标题',
|
||||
'副标题/期数',
|
||||
'封面图URL',
|
||||
'视频URL',
|
||||
'状态:0禁用 1启用',
|
||||
'创建人ID',
|
||||
'更新人ID',
|
||||
]
|
||||
selector_header_list = []
|
||||
option_list = []
|
||||
|
||||
# 添加下拉选项
|
||||
|
||||
return ExcelUtil.get_excel_template(
|
||||
header_list=header_list,
|
||||
selector_header_list=selector_header_list,
|
||||
option_list=option_list
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
@@ -0,0 +1,102 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from app.core.dependencies import AuthPermission
|
||||
from app.common.response import SuccessResponse
|
||||
from app.core.base_params import PaginationQueryParam
|
||||
from app.core.logger import log
|
||||
from app.api.v1.module_yifan.yifan_affinity.service import YifanAffinityService
|
||||
from app.api.v1.module_yifan.yifan_affinity.schema import (
|
||||
YifanAffinityCreateSchema, YifanAffinityUpdateSchema, YifanAffinityQueryParam,
|
||||
AffinityCalculateRequestSchema
|
||||
)
|
||||
|
||||
YifanAffinityRouter = APIRouter(prefix="/yifan_affinity", tags=["缘分合盘"])
|
||||
|
||||
|
||||
@YifanAffinityRouter.post("/calculate", summary="缘分合盘测算", description="缘分合盘测算,创建任务并异步生成分析结果")
|
||||
async def calculate_affinity_controller(
|
||||
data: AffinityCalculateRequestSchema,
|
||||
auth: AuthSchema = Depends(AuthPermission([]))
|
||||
) -> JSONResponse:
|
||||
"""缘分合盘测算接口 - 创建任务,后台异步生成分析结果"""
|
||||
result_dict = await YifanAffinityService.calculate_affinity_service(auth=auth, data=data)
|
||||
log.info(f"缘分合盘任务已创建: {data.person1.name} & {data.person2.name} - {data.relationship}")
|
||||
return SuccessResponse(data=result_dict, msg="缘分合盘任务已创建,正在测算中")
|
||||
|
||||
|
||||
@YifanAffinityRouter.get("/result/{affinity_id}", summary="获取缘分合盘结果", description="根据ID获取缘分合盘测算结果")
|
||||
async def get_affinity_result_controller(
|
||||
affinity_id: int,
|
||||
auth: AuthSchema = Depends(AuthPermission([]))
|
||||
) -> JSONResponse:
|
||||
"""获取缘分合盘结果接口"""
|
||||
result_dict = await YifanAffinityService.get_affinity_result_service(auth=auth, affinity_id=affinity_id)
|
||||
log.info(f"获取缘分合盘结果成功: ID={affinity_id}")
|
||||
return SuccessResponse(data=result_dict.model_dump(), msg="获取缘分合盘结果成功")
|
||||
|
||||
|
||||
@YifanAffinityRouter.get("/list", summary="查询缘分合盘列表", description="查询缘分合盘列表")
|
||||
async def get_yifan_affinity_list_controller(
|
||||
page: PaginationQueryParam = Depends(),
|
||||
search: YifanAffinityQueryParam = Depends(),
|
||||
auth: AuthSchema = Depends(AuthPermission([]))
|
||||
) -> JSONResponse:
|
||||
"""查询缘分合盘列表接口(数据库分页)"""
|
||||
result_dict = await YifanAffinityService.page_yifan_affinity_service(
|
||||
auth=auth,
|
||||
page_no=page.page_no if page.page_no is not None else 1,
|
||||
page_size=page.page_size if page.page_size is not None else 10,
|
||||
search=search,
|
||||
order_by=page.order_by
|
||||
)
|
||||
log.info("查询缘分合盘列表成功")
|
||||
return SuccessResponse(data=result_dict, msg="查询缘分合盘列表成功")
|
||||
|
||||
|
||||
@YifanAffinityRouter.get("/{affinity_id}", summary="获取缘分合盘详情", description="根据ID获取缘分合盘详情")
|
||||
async def get_yifan_affinity_controller(
|
||||
affinity_id: int,
|
||||
auth: AuthSchema = Depends(AuthPermission([]))
|
||||
) -> JSONResponse:
|
||||
"""获取缘分合盘详情接口"""
|
||||
result_dict = await YifanAffinityService.get_yifan_affinity_service(auth=auth, affinity_id=affinity_id)
|
||||
log.info("获取缘分合盘详情成功")
|
||||
return SuccessResponse(data=result_dict, msg="获取缘分合盘详情成功")
|
||||
|
||||
|
||||
@YifanAffinityRouter.post("/create", summary="创建缘分合盘", description="创建缘分合盘")
|
||||
async def create_yifan_affinity_controller(
|
||||
data: YifanAffinityCreateSchema,
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_affinity:create"]))
|
||||
) -> JSONResponse:
|
||||
"""创建缘分合盘接口"""
|
||||
result_dict = await YifanAffinityService.create_yifan_affinity_service(auth=auth, data=data)
|
||||
log.info("创建缘分合盘成功")
|
||||
return SuccessResponse(data=result_dict, msg="创建缘分合盘成功")
|
||||
|
||||
|
||||
@YifanAffinityRouter.put("/{affinity_id}", summary="更新缘分合盘", description="根据ID更新缘分合盘")
|
||||
async def update_yifan_affinity_controller(
|
||||
affinity_id: int,
|
||||
data: YifanAffinityUpdateSchema,
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_affinity:update"]))
|
||||
) -> JSONResponse:
|
||||
"""更新缘分合盘接口"""
|
||||
result_dict = await YifanAffinityService.update_yifan_affinity_service(auth=auth, affinity_id=affinity_id, data=data)
|
||||
log.info("更新缘分合盘成功")
|
||||
return SuccessResponse(data=result_dict, msg="更新缘分合盘成功")
|
||||
|
||||
|
||||
@YifanAffinityRouter.delete("/{affinity_id}", summary="删除缘分合盘", description="根据ID删除缘分合盘")
|
||||
async def delete_yifan_affinity_controller(
|
||||
affinity_id: int,
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_affinity:delete"]))
|
||||
) -> JSONResponse:
|
||||
"""删除缘分合盘接口"""
|
||||
result = await YifanAffinityService.delete_yifan_affinity_service(auth=auth, affinity_id=affinity_id)
|
||||
log.info("删除缘分合盘成功")
|
||||
return SuccessResponse(data=result, msg="删除缘分合盘成功")
|
||||
@@ -0,0 +1,112 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from typing import Sequence
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.base_crud import CRUDBase
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from app.api.v1.module_yifan.yifan_affinity.model import YifanAffinityModel
|
||||
from app.api.v1.module_yifan.yifan_affinity.schema import YifanAffinityCreateSchema, YifanAffinityUpdateSchema
|
||||
|
||||
|
||||
class YifanAffinityCRUD(CRUDBase[YifanAffinityModel, YifanAffinityCreateSchema, YifanAffinityUpdateSchema]):
|
||||
"""缘分合盘CRUD操作"""
|
||||
|
||||
def __init__(self, auth: AuthSchema):
|
||||
super().__init__(model=YifanAffinityModel, auth=auth)
|
||||
|
||||
async def create_yifan_affinity_crud(self, data: YifanAffinityCreateSchema) -> YifanAffinityModel:
|
||||
"""
|
||||
创建缘分合盘记录
|
||||
|
||||
参数:
|
||||
- data (YifanAffinityCreateSchema): 创建数据
|
||||
|
||||
返回:
|
||||
- YifanAffinityModel: 模型实例
|
||||
"""
|
||||
return await self.create(data=data)
|
||||
|
||||
async def get_yifan_affinity_crud(self, affinity_id: int, preload: list | None = None) -> YifanAffinityModel | None:
|
||||
"""
|
||||
获取单个缘分合盘记录
|
||||
|
||||
参数:
|
||||
- affinity_id (int): 记录ID
|
||||
- preload (list | None): 预加载关系,未提供时使用模型默认项
|
||||
|
||||
返回:
|
||||
- YifanAffinityModel | None: 模型实例或None
|
||||
"""
|
||||
return await self.get(id=affinity_id, preload=preload)
|
||||
|
||||
async def update_yifan_affinity_crud(self, affinity_id: int, data: YifanAffinityUpdateSchema) -> YifanAffinityModel:
|
||||
"""
|
||||
更新缘分合盘记录
|
||||
|
||||
参数:
|
||||
- affinity_id (int): 记录ID
|
||||
- data (YifanAffinityUpdateSchema): 更新数据
|
||||
|
||||
返回:
|
||||
- YifanAffinityModel: 更新后的模型实例
|
||||
"""
|
||||
return await self.update(id=affinity_id, data=data)
|
||||
|
||||
async def delete_yifan_affinity_crud(self, affinity_id: int) -> bool:
|
||||
"""
|
||||
删除缘分合盘记录
|
||||
|
||||
参数:
|
||||
- affinity_id (int): 记录ID
|
||||
|
||||
返回:
|
||||
- bool: 删除是否成功
|
||||
"""
|
||||
return await self.delete(id=affinity_id)
|
||||
|
||||
async def list_yifan_affinity_crud(self, search: dict | None = None, order_by: list[dict] | None = None, preload: list | None = None) -> Sequence[YifanAffinityModel]:
|
||||
"""
|
||||
列表查询
|
||||
|
||||
参数:
|
||||
- search (dict | None): 查询参数
|
||||
- order_by (list[dict] | None): 排序参数,未提供时使用模型默认项
|
||||
- preload (list | None): 预加载关系,未提供时使用模型默认项
|
||||
|
||||
返回:
|
||||
- Sequence[YifanAffinityModel]: 模型实例序列
|
||||
"""
|
||||
return await self.list(search=search, order_by=order_by, preload=preload)
|
||||
|
||||
async def page_yifan_affinity_crud(self, page_no: int, page_size: int, search: dict | None = None, order_by: list[dict] | None = None, preload: list | None = None) -> dict:
|
||||
"""
|
||||
分页查询
|
||||
|
||||
参数:
|
||||
- page_no (int): 页码
|
||||
- page_size (int): 每页数量
|
||||
- search (dict | None): 查询参数
|
||||
- order_by (list[dict] | None): 排序参数,未提供时使用模型默认项
|
||||
- preload (list | None): 预加载关系,未提供时使用模型默认项
|
||||
|
||||
返回:
|
||||
- dict: 分页结果字典
|
||||
"""
|
||||
from app.api.v1.module_yifan.yifan_affinity.schema import YifanAffinityOutSchema
|
||||
|
||||
# 将页码转换为偏移量
|
||||
offset = (page_no - 1) * page_size
|
||||
|
||||
# 设置默认排序
|
||||
if order_by is None:
|
||||
order_by = [{"field": "created_time", "order": "desc"}]
|
||||
|
||||
return await self.page(
|
||||
offset=offset,
|
||||
limit=page_size,
|
||||
search=search or {},
|
||||
order_by=order_by,
|
||||
out_schema=YifanAffinityOutSchema,
|
||||
preload=preload
|
||||
)
|
||||
@@ -0,0 +1,59 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import datetime
|
||||
from sqlalchemy import Integer, DateTime, SmallInteger, Text, String, JSON, Numeric
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.base_model import ModelMixin, UserMixin
|
||||
|
||||
|
||||
class YifanAffinityModel(ModelMixin, UserMixin):
|
||||
"""
|
||||
缘分合盘表
|
||||
"""
|
||||
__tablename__: str = 'yifan_affinity'
|
||||
__table_args__: dict[str, str] = {'comment': '缘分合盘'}
|
||||
__loader_options__: list[str] = ["created_by", "updated_by"]
|
||||
|
||||
is_deleted: Mapped[int | None] = mapped_column(SmallInteger, nullable=True, comment='是否删除(0否 1是)')
|
||||
task_status: Mapped[int | None] = mapped_column(SmallInteger, nullable=True, comment='任务状态(1已创建 2测算中 5测算成功 3测算超时 0任务失败)')
|
||||
user_id: Mapped[int | None] = mapped_column(Integer, nullable=True, comment='用户ID')
|
||||
|
||||
# 缘分类型
|
||||
relationship: Mapped[str | None] = mapped_column(String(32), nullable=True, comment='缘分类型(couple情侣 married夫妻 crush暗恋 partner合伙 friend知己 family亲缘)')
|
||||
|
||||
# 甲方信息
|
||||
person1_name: Mapped[str | None] = mapped_column(String(100), nullable=True, comment='甲方姓名')
|
||||
person1_gender: Mapped[str | None] = mapped_column(String(10), nullable=True, comment='甲方性别(male男 female女)')
|
||||
person1_birth_date: Mapped[str | None] = mapped_column(String(100), nullable=True, comment='甲方生辰显示格式')
|
||||
person1_birth_date_api: Mapped[datetime.datetime | None] = mapped_column(DateTime, nullable=True, comment='甲方生辰API格式')
|
||||
|
||||
# 乙方信息
|
||||
person2_name: Mapped[str | None] = mapped_column(String(100), nullable=True, comment='乙方姓名')
|
||||
person2_gender: Mapped[str | None] = mapped_column(String(10), nullable=True, comment='乙方性别(male男 female女)')
|
||||
person2_birth_date: Mapped[str | None] = mapped_column(String(100), nullable=True, comment='乙方生辰显示格式')
|
||||
person2_birth_date_api: Mapped[datetime.datetime | None] = mapped_column(DateTime, nullable=True, comment='乙方生辰API格式')
|
||||
|
||||
# 测算结果
|
||||
score: Mapped[int | None] = mapped_column(Integer, nullable=True, comment='默契指数总分(0-100)')
|
||||
score_badge: Mapped[str | None] = mapped_column(String(32), nullable=True, comment='婚配等级(上上婚 上等婚 中等婚 下等婚)')
|
||||
|
||||
# 六维契合度评分(JSON格式存储)
|
||||
six_dimension: Mapped[str | None] = mapped_column(JSON, nullable=True, comment='六维契合度评分JSON')
|
||||
radar_desc: Mapped[str | None] = mapped_column(Text, nullable=True, comment='六维雷达图描述')
|
||||
|
||||
# 分析卡片(JSON格式存储)
|
||||
analysis_cards: Mapped[str | None] = mapped_column(JSON, nullable=True, comment='分析卡片列表JSON')
|
||||
|
||||
# 解锁状态
|
||||
is_unlocked: Mapped[int | None] = mapped_column(SmallInteger, nullable=True, comment='是否已解锁深度报告(0否 1是)')
|
||||
unlock_price: Mapped[str | None] = mapped_column(Numeric(10, 2), nullable=True, comment='解锁价格')
|
||||
|
||||
# 解锁后的深度内容(JSON格式存储)
|
||||
unlocked_content: Mapped[str | None] = mapped_column(JSON, nullable=True, comment='解锁后的深度内容JSON')
|
||||
|
||||
# AI原始数据
|
||||
ai_source_data: Mapped[str | None] = mapped_column(Text, nullable=True, comment='AI原始数据')
|
||||
error_message: Mapped[str | None] = mapped_column(Text, nullable=True, comment='错误信息')
|
||||
|
||||
remark: Mapped[str | None] = mapped_column(String(255), nullable=True, comment='备注')
|
||||
@@ -0,0 +1,244 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import datetime
|
||||
from typing import List, Dict, Any, Optional
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from fastapi import Query
|
||||
|
||||
from app.core.validator import DateTimeStr
|
||||
from app.core.base_schema import BaseSchema, UserBySchema
|
||||
|
||||
|
||||
class PersonSchema(BaseModel):
|
||||
"""人员信息模型"""
|
||||
name: str = Field(..., description='姓名', example='张三')
|
||||
gender: str = Field(..., description='性别', example='male')
|
||||
birth_date: str = Field(..., description='生辰八字(显示格式)', example='1990年1月1日 子时')
|
||||
birth_date_api: datetime.datetime = Field(..., description='生辰八字(API格式)', example='1990-01-01 00:30:00')
|
||||
|
||||
|
||||
class AffinityCalculateRequestSchema(BaseModel):
|
||||
"""缘分合盘测算请求模型"""
|
||||
relationship: str = Field(..., description='缘分类型', example='couple')
|
||||
person1: PersonSchema = Field(..., description='甲方信息(君)')
|
||||
person2: PersonSchema = Field(..., description='乙方信息(卿)')
|
||||
|
||||
|
||||
class SixDimensionSchema(BaseModel):
|
||||
"""六维契合度评分模型"""
|
||||
emotional: int = Field(..., description='情感共鸣', ge=0, le=100, example=92)
|
||||
communication: int = Field(..., description='沟通模式', ge=0, le=100, example=78)
|
||||
trust: int = Field(..., description='信任安全', ge=0, le=100, example=95)
|
||||
values: int = Field(..., description='价值观', ge=0, le=100, example=85)
|
||||
compatibility: int = Field(..., description='互补性', ge=0, le=100, example=88)
|
||||
luck: int = Field(..., description='互旺运势', ge=0, le=100, example=88)
|
||||
|
||||
|
||||
class AnalysisCardSchema(BaseModel):
|
||||
"""分析卡片模型"""
|
||||
id: str = Field(..., description='卡片ID', example='emotional')
|
||||
icon: str = Field(..., description='图标emoji', example='[表情]')
|
||||
icon_bg: str = Field(..., description='图标背景色', example='rgba(236, 72, 153, 0.2)')
|
||||
title: str = Field(..., description='标题', example='情感共鸣')
|
||||
score: str = Field(..., description='评分', example='92')
|
||||
summary: str = Field(..., description='摘要(折叠时显示)')
|
||||
content: str = Field(..., description='详细内容(展开时显示)')
|
||||
|
||||
|
||||
class PastLifeSchema(BaseModel):
|
||||
"""前世羁绊模型"""
|
||||
description: str = Field(..., description='前世关系描述')
|
||||
depth: str = Field(..., description='缘分深浅', example='三生石上旧精魂')
|
||||
relationship: str = Field(..., description='还债关系', example='互不相欠 · 共同成长')
|
||||
|
||||
|
||||
class MatchItemSchema(BaseModel):
|
||||
"""正缘特征验证模型"""
|
||||
label: str = Field(..., description='验证项', example='外貌特征')
|
||||
match: int = Field(..., description='契合度百分比', ge=0, le=100, example=90)
|
||||
desc: str = Field(..., description='描述')
|
||||
|
||||
|
||||
class GuideSchema(BaseModel):
|
||||
"""潜意识与相处指南模型"""
|
||||
real_needs: str = Field(..., description='TA的真实需求')
|
||||
red_zone: str = Field(..., description='绝对雷区')
|
||||
tips: List[str] = Field(..., description='如何拿捏TA')
|
||||
|
||||
|
||||
class MonthlyFortuneSchema(BaseModel):
|
||||
"""未来12个月感情运势模型"""
|
||||
heats: List[int] = Field(..., description='12个月的热度值(0-3)')
|
||||
heat_labels: List[str] = Field(..., description='热度标签')
|
||||
highlights: str = Field(..., description='高光时刻提示')
|
||||
warnings: str = Field(..., description='预警时刻提示')
|
||||
|
||||
|
||||
class TimelineItemSchema(BaseModel):
|
||||
"""未来十年关键节点项模型"""
|
||||
year: str = Field(..., description='年份', example='2025')
|
||||
title: str = Field(..., description='阶段标题', example='升温期')
|
||||
desc: str = Field(..., description='描述')
|
||||
highlight: bool = Field(..., description='是否高亮显示')
|
||||
|
||||
|
||||
class MasterAdviceSchema(BaseModel):
|
||||
"""大师寄语与化解之道模型"""
|
||||
message: str = Field(..., description='寄语')
|
||||
tips: List[str] = Field(..., description='开运建议')
|
||||
|
||||
|
||||
class UnlockedContentSchema(BaseModel):
|
||||
"""解锁后的深度内容模型"""
|
||||
past_life: PastLifeSchema = Field(..., description='前世羁绊')
|
||||
match_items: List[MatchItemSchema] = Field(..., description='正缘特征验证')
|
||||
guide: GuideSchema = Field(..., description='潜意识与相处指南')
|
||||
monthly_fortune: MonthlyFortuneSchema = Field(..., description='未来12个月感情运势')
|
||||
timeline: List[TimelineItemSchema] = Field(..., description='未来十年关键节点')
|
||||
master_advice: MasterAdviceSchema = Field(..., description='大师寄语与化解之道')
|
||||
|
||||
|
||||
class UnlockStatsSchema(BaseModel):
|
||||
"""解锁统计信息模型"""
|
||||
unlock_count: int = Field(..., description='已解锁人数', example=12392)
|
||||
accuracy: str = Field(..., description='准确率', example='98%')
|
||||
|
||||
|
||||
class PersonResponseSchema(BaseModel):
|
||||
"""人员响应信息模型"""
|
||||
name: str = Field(..., description='姓名')
|
||||
gender: str = Field(..., description='性别')
|
||||
birth_date: str = Field(..., description='生辰八字')
|
||||
|
||||
|
||||
class AffinityCalculateResponseSchema(BaseModel):
|
||||
"""缘分合盘测算响应模型"""
|
||||
relationship: str = Field(..., description='缘分类型')
|
||||
relationship_label: str = Field(..., description='缘分类型标签')
|
||||
person1: PersonResponseSchema = Field(..., description='甲方信息')
|
||||
person2: PersonResponseSchema = Field(..., description='乙方信息')
|
||||
score: int = Field(..., description='默契指数总分', ge=0, le=100)
|
||||
score_badge: str = Field(..., description='婚配等级')
|
||||
six_dimension: SixDimensionSchema = Field(..., description='六维契合度评分')
|
||||
radar_desc: str = Field(..., description='六维雷达图描述')
|
||||
analysis_cards: List[AnalysisCardSchema] = Field(..., description='分析卡片列表')
|
||||
unlocked: Optional[UnlockedContentSchema] = Field(None, description='解锁后的深度内容')
|
||||
is_unlocked: bool = Field(..., description='是否已解锁深度报告')
|
||||
unlock_price: float = Field(..., description='解锁价格(元)')
|
||||
unlock_stats: UnlockStatsSchema = Field(..., description='解锁统计信息')
|
||||
|
||||
|
||||
class YifanAffinityCreateSchema(BaseModel):
|
||||
"""
|
||||
缘分合盘新增模型
|
||||
"""
|
||||
is_deleted: int = Field(default=0, description='是否删除(0否 1是)')
|
||||
task_status: int = Field(default=1, description='任务状态(1已创建 2测算中 5测算成功 3测算超时 0任务失败)')
|
||||
user_id: int | None = Field(default=None, description='用户ID')
|
||||
relationship: str = Field(default=..., description='缘分类型')
|
||||
person1_name: str = Field(default=..., description='甲方姓名')
|
||||
person1_gender: str = Field(default=..., description='甲方性别')
|
||||
person1_birth_date: str = Field(default=..., description='甲方生辰显示格式')
|
||||
person1_birth_date_api: datetime.datetime = Field(default=..., description='甲方生辰API格式')
|
||||
person2_name: str = Field(default=..., description='乙方姓名')
|
||||
person2_gender: str = Field(default=..., description='乙方性别')
|
||||
person2_birth_date: str = Field(default=..., description='乙方生辰显示格式')
|
||||
person2_birth_date_api: datetime.datetime = Field(default=..., description='乙方生辰API格式')
|
||||
score: int | None = Field(default=None, description='默契指数总分')
|
||||
score_badge: str | None = Field(default=None, description='婚配等级')
|
||||
six_dimension: str | None = Field(default=None, description='六维契合度评分JSON')
|
||||
radar_desc: str | None = Field(default=None, description='六维雷达图描述')
|
||||
analysis_cards: str | None = Field(default=None, description='分析卡片列表JSON')
|
||||
is_unlocked: int = Field(default=0, description='是否已解锁深度报告')
|
||||
unlock_price: float | None = Field(default=9.9, description='解锁价格')
|
||||
unlocked_content: str | None = Field(default=None, description='解锁后的深度内容JSON')
|
||||
ai_source_data: str | None = Field(default=None, description='AI原始数据')
|
||||
error_message: str | None = Field(default=None, description='错误信息')
|
||||
remark: str | None = Field(default=None, description='备注')
|
||||
|
||||
|
||||
class YifanAffinityUpdateSchema(BaseModel):
|
||||
"""
|
||||
缘分合盘更新模型
|
||||
"""
|
||||
is_deleted: int | None = Field(default=None, description='是否删除(0否 1是)')
|
||||
task_status: int | None = Field(default=None, description='任务状态(1已创建 2测算中 5测算成功 3测算超时 0任务失败)')
|
||||
user_id: int | None = Field(default=None, description='用户ID')
|
||||
relationship: str | None = Field(default=None, description='缘分类型')
|
||||
person1_name: str | None = Field(default=None, description='甲方姓名')
|
||||
person1_gender: str | None = Field(default=None, description='甲方性别')
|
||||
person1_birth_date: str | None = Field(default=None, description='甲方生辰显示格式')
|
||||
person1_birth_date_api: datetime.datetime | None = Field(default=None, description='甲方生辰API格式')
|
||||
person2_name: str | None = Field(default=None, description='乙方姓名')
|
||||
person2_gender: str | None = Field(default=None, description='乙方性别')
|
||||
person2_birth_date: str | None = Field(default=None, description='乙方生辰显示格式')
|
||||
person2_birth_date_api: datetime.datetime | None = Field(default=None, description='乙方生辰API格式')
|
||||
score: int | None = Field(default=None, description='默契指数总分(0-100)')
|
||||
score_badge: str | None = Field(default=None, description='婚配等级')
|
||||
six_dimension: str | None = Field(default=None, description='六维契合度评分JSON')
|
||||
radar_desc: str | None = Field(default=None, description='六维雷达图描述')
|
||||
analysis_cards: str | None = Field(default=None, description='分析卡片列表JSON')
|
||||
is_unlocked: int | None = Field(default=None, description='是否已解锁深度报告')
|
||||
unlock_price: float | None = Field(default=None, description='解锁价格')
|
||||
unlocked_content: str | None = Field(default=None, description='解锁后的深度内容JSON')
|
||||
ai_source_data: str | None = Field(default=None, description='AI原始数据')
|
||||
error_message: str | None = Field(default=None, description='错误信息')
|
||||
remark: str | None = Field(default=None, description='备注')
|
||||
|
||||
|
||||
class YifanAffinityOutSchema(YifanAffinityCreateSchema, BaseSchema, UserBySchema):
|
||||
"""
|
||||
缘分合盘响应模型
|
||||
"""
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class YifanAffinityQueryParam:
|
||||
"""缘分合盘查询参数"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
relationship: str | None = Query(None, description="缘分类型"),
|
||||
person1_name: str | None = Query(None, description="甲方姓名"),
|
||||
person2_name: str | None = Query(None, description="乙方姓名"),
|
||||
score_min: int | None = Query(None, description="最低分数"),
|
||||
score_max: int | None = Query(None, description="最高分数"),
|
||||
score_badge: str | None = Query(None, description="婚配等级"),
|
||||
is_unlocked: int | None = Query(None, description="是否已解锁深度报告"),
|
||||
created_id: int | None = Query(None, description="创建人ID"),
|
||||
updated_id: int | None = Query(None, description="更新人ID"),
|
||||
is_deleted: int | None = Query(None, description="是否删除(0否 1是)"),
|
||||
task_status: int | None = Query(None, description="任务状态"),
|
||||
user_id: int | None = Query(None, description="用户ID"),
|
||||
created_time: list[DateTimeStr] | None = Query(None, description="创建时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]),
|
||||
updated_time: list[DateTimeStr] | None = Query(None, description="更新时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]),
|
||||
|
||||
) -> None:
|
||||
|
||||
# 精确查询字段
|
||||
self.created_id = created_id
|
||||
self.updated_id = updated_id
|
||||
self.is_deleted = is_deleted
|
||||
self.task_status = task_status
|
||||
self.user_id = user_id
|
||||
self.is_unlocked = is_unlocked
|
||||
|
||||
# 模糊查询字段
|
||||
self.relationship = ("like", relationship)
|
||||
self.person1_name = ("like", person1_name)
|
||||
self.person2_name = ("like", person2_name)
|
||||
self.score_badge = ("like", score_badge)
|
||||
|
||||
# 范围查询字段
|
||||
if score_min is not None and score_max is not None:
|
||||
self.score = ("between", (score_min, score_max))
|
||||
elif score_min is not None:
|
||||
self.score = (">=", score_min)
|
||||
elif score_max is not None:
|
||||
self.score = ("<=", score_max)
|
||||
|
||||
# 时间范围查询
|
||||
if created_time and len(created_time) == 2:
|
||||
self.created_time = ("between", (created_time[0], created_time[1]))
|
||||
if updated_time and len(updated_time) == 2:
|
||||
self.updated_time = ("between", (updated_time[0], updated_time[1]))
|
||||
@@ -0,0 +1,377 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import json
|
||||
import asyncio
|
||||
from typing import Dict, Any
|
||||
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from app.core.exceptions import CustomException
|
||||
from app.core.logger import log
|
||||
from app.api.v1.module_yifan.yifan_affinity.crud import YifanAffinityCRUD
|
||||
from app.api.v1.module_yifan.yifan_affinity.schema import (
|
||||
YifanAffinityCreateSchema, YifanAffinityUpdateSchema, YifanAffinityOutSchema, YifanAffinityQueryParam,
|
||||
AffinityCalculateRequestSchema, AffinityCalculateResponseSchema
|
||||
)
|
||||
|
||||
|
||||
class YifanAffinityService:
|
||||
"""缘分合盘服务"""
|
||||
|
||||
@classmethod
|
||||
async def calculate_affinity_service(cls, auth: AuthSchema, data: AffinityCalculateRequestSchema) -> Dict[str, Any]:
|
||||
"""
|
||||
缘分合盘测算服务(异步模式)
|
||||
|
||||
1. 校验数据,创建报告记录(status=1 已创建)
|
||||
2. 启动后台任务执行AI测算
|
||||
3. 立即返回 report_id + status
|
||||
"""
|
||||
user_id = auth.user.id if auth.user else 0
|
||||
|
||||
# 创建缘分合盘记录
|
||||
create_data = YifanAffinityCreateSchema(
|
||||
user_id=user_id,
|
||||
relationship=data.relationship,
|
||||
person1_name=data.person1.name,
|
||||
person1_gender=data.person1.gender,
|
||||
person1_birth_date=data.person1.birth_date,
|
||||
person1_birth_date_api=data.person1.birth_date_api,
|
||||
person2_name=data.person2.name,
|
||||
person2_gender=data.person2.gender,
|
||||
person2_birth_date=data.person2.birth_date,
|
||||
person2_birth_date_api=data.person2.birth_date_api,
|
||||
task_status=1,
|
||||
is_deleted=0
|
||||
)
|
||||
|
||||
try:
|
||||
affinity_obj = await YifanAffinityCRUD(auth).create_yifan_affinity_crud(data=create_data)
|
||||
affinity_id = affinity_obj.id
|
||||
log.info(f"[缘分合盘] 报告创建成功,ID: {affinity_id}")
|
||||
except Exception as e:
|
||||
log.error(f"[缘分合盘] 报告创建失败: {str(e)}")
|
||||
raise CustomException(msg=f"保存缘分合盘报告失败: {str(e)}")
|
||||
|
||||
# 启动后台任务执行AI测算
|
||||
asyncio.create_task(
|
||||
cls._process_affinity_calculation(
|
||||
affinity_id=affinity_id,
|
||||
data=data,
|
||||
user_id=user_id,
|
||||
)
|
||||
)
|
||||
|
||||
return {"affinity_id": affinity_id, "status": 1}
|
||||
|
||||
@classmethod
|
||||
async def _process_affinity_calculation(
|
||||
cls,
|
||||
affinity_id: int,
|
||||
data: AffinityCalculateRequestSchema,
|
||||
user_id: int,
|
||||
) -> None:
|
||||
"""后台任务:执行缘分合盘AI测算"""
|
||||
log_prefix = f"[缘分合盘-{affinity_id}]"
|
||||
|
||||
try:
|
||||
# 更新状态为测算中
|
||||
await cls._update_affinity_status(affinity_id, task_status=2)
|
||||
log.info(f"{log_prefix} 开始AI测算")
|
||||
|
||||
# 构建AI输入文本
|
||||
relationship_labels = {
|
||||
"couple": "情侣",
|
||||
"married": "夫妻",
|
||||
"crush": "暗恋",
|
||||
"partner": "合伙",
|
||||
"friend": "知己",
|
||||
"family": "亲缘"
|
||||
}
|
||||
|
||||
relationship_label = relationship_labels.get(data.relationship, data.relationship)
|
||||
|
||||
ai_input_text = f"""缘分合盘测算请求:
|
||||
缘分类型:{relationship_label}
|
||||
甲方信息:
|
||||
- 姓名:{data.person1.name}
|
||||
- 性别:{'男' if data.person1.gender == 'male' else '女'}
|
||||
- 生辰:{data.person1.birth_date}
|
||||
乙方信息:
|
||||
- 姓名:{data.person2.name}
|
||||
- 性别:{'男' if data.person2.gender == 'male' else '女'}
|
||||
- 生辰:{data.person2.birth_date}
|
||||
|
||||
请根据以上信息进行缘分合盘测算,返回详细的分析结果。"""
|
||||
|
||||
# 调用AI服务进行测算
|
||||
ai_response = await cls._call_ai_service(ai_input_text)
|
||||
log.info(f"ai_response is {ai_response}")
|
||||
if not ai_response:
|
||||
raise Exception("AI服务返回空结果")
|
||||
|
||||
# 解析AI响应并构建结果数据
|
||||
result_data = cls._parse_ai_response(ai_response, data, relationship_label)
|
||||
log.info(f"result_data is {result_data}")
|
||||
|
||||
# 更新数据库记录
|
||||
update_data = YifanAffinityUpdateSchema(
|
||||
score=result_data.get('score'),
|
||||
score_badge=result_data.get('score_badge'),
|
||||
six_dimension=json.dumps(result_data.get('six_dimension'), ensure_ascii=False),
|
||||
radar_desc=result_data.get('radar_desc'),
|
||||
analysis_cards=json.dumps(result_data.get('analysis_cards'), ensure_ascii=False),
|
||||
unlocked_content=json.dumps(result_data.get('unlocked'), ensure_ascii=False),
|
||||
ai_source_data=ai_response,
|
||||
task_status=5 # 测算成功
|
||||
)
|
||||
|
||||
await cls._update_affinity_record(affinity_id, update_data)
|
||||
log.info(f"{log_prefix} AI测算完成并保存成功")
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
log.error(f"{log_prefix} AI测算超时")
|
||||
await cls._update_affinity_status(affinity_id, task_status=3, error_message="AI测算超时")
|
||||
except Exception as e:
|
||||
log.error(f"{log_prefix} AI测算失败: {str(e)}")
|
||||
await cls._update_affinity_status(affinity_id, task_status=0, error_message=str(e)[:500])
|
||||
|
||||
@classmethod
|
||||
async def _call_ai_service(cls, text: str) -> str:
|
||||
"""
|
||||
调用AI服务进行缘分合盘测算
|
||||
|
||||
Args:
|
||||
text: 输入文本
|
||||
|
||||
Returns:
|
||||
AI响应结果
|
||||
"""
|
||||
from app.api.v1.module_application.ai.service import AIModelTestService
|
||||
|
||||
log.info(f"[缘分合盘] 调用AI服务,输入长度: {len(text)}")
|
||||
|
||||
try:
|
||||
# 调用实际的AI服务,使用personal_naming模型类型
|
||||
ai_response = await AIModelTestService.test_naming(
|
||||
model_type="yuanfen_hepan",
|
||||
text=text
|
||||
)
|
||||
|
||||
log.info(f"[缘分合盘] AI服务响应成功,响应长度: {len(ai_response)}")
|
||||
return ai_response
|
||||
|
||||
except Exception as e:
|
||||
log.error(f"[缘分合盘] AI服务调用失败: {str(e)}")
|
||||
# 如果AI服务调用失败,返回模拟数据作为降级方案
|
||||
log.warning("[缘分合盘] 使用模拟数据作为降级方案")
|
||||
|
||||
mock_response = {
|
||||
"score": 85,
|
||||
"score_badge": "上等婚",
|
||||
"six_dimension": {
|
||||
"personality": 88,
|
||||
"career": 82,
|
||||
"wealth": 90,
|
||||
"health": 85,
|
||||
"emotion": 87,
|
||||
"family": 83
|
||||
},
|
||||
"radar_desc": "你们在财运和性格方面非常契合,情感交流也很顺畅。",
|
||||
"analysis_cards": [
|
||||
{
|
||||
"title": "性格契合度",
|
||||
"content": "你们的性格互补,能够相互理解和支持。",
|
||||
"score": 88
|
||||
},
|
||||
{
|
||||
"title": "事业发展",
|
||||
"content": "在事业上你们有共同目标,能够相互促进。",
|
||||
"score": 82
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
return json.dumps(mock_response, ensure_ascii=False)
|
||||
|
||||
@classmethod
|
||||
def _parse_ai_response(cls, ai_response: str, request_data: AffinityCalculateRequestSchema, relationship_label: str) -> Dict[str, Any]:
|
||||
"""解析AI响应数据"""
|
||||
try:
|
||||
ai_data = json.loads(ai_response)
|
||||
|
||||
# 构建完整的响应数据
|
||||
result_data = {
|
||||
"relationship": request_data.relationship,
|
||||
"relationship_label": relationship_label,
|
||||
"person1": {
|
||||
"name": request_data.person1.name,
|
||||
"gender": request_data.person1.gender,
|
||||
"birth_date": request_data.person1.birth_date
|
||||
},
|
||||
"person2": {
|
||||
"name": request_data.person2.name,
|
||||
"gender": request_data.person2.gender,
|
||||
"birth_date": request_data.person2.birth_date
|
||||
},
|
||||
"score": ai_data.get("score", 0),
|
||||
"score_badge": ai_data.get("score_badge", "中等婚"),
|
||||
"six_dimension": ai_data.get("six_dimension", {}),
|
||||
"radar_desc": ai_data.get("radar_desc", ""),
|
||||
"analysis_cards": ai_data.get("analysis_cards", []),
|
||||
"unlocked": ai_data.get("unlocked", {}),
|
||||
"is_unlocked": False,
|
||||
"unlock_price": 9.9,
|
||||
"unlock_stats": {
|
||||
"unlock_count": 12392,
|
||||
"accuracy": "98%"
|
||||
}
|
||||
}
|
||||
|
||||
return result_data
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
log.error(f"AI响应JSON解析失败: {str(e)}")
|
||||
raise Exception(f"AI响应格式错误: {str(e)}")
|
||||
|
||||
@classmethod
|
||||
async def _update_affinity_status(cls, affinity_id: int, task_status: int, error_message: str = None):
|
||||
"""更新缘分合盘状态"""
|
||||
from app.core.database import async_db_session
|
||||
|
||||
async with async_db_session() as session:
|
||||
try:
|
||||
# 创建一个临时的auth对象用于更新操作
|
||||
temp_auth = AuthSchema(user=None, db=session)
|
||||
crud = YifanAffinityCRUD(temp_auth)
|
||||
crud.session = session
|
||||
|
||||
update_data = YifanAffinityUpdateSchema(
|
||||
task_status=task_status,
|
||||
error_message=error_message
|
||||
)
|
||||
|
||||
await crud.update_yifan_affinity_crud(affinity_id, update_data)
|
||||
await session.commit() # 提交事务
|
||||
log.info(f"[缘分合盘] 状态更新成功,ID: {affinity_id}, 状态: {task_status}")
|
||||
except Exception as e:
|
||||
await session.rollback() # 回滚事务
|
||||
log.error(f"[缘分合盘] 状态更新失败,ID: {affinity_id}, 错误: {str(e)}")
|
||||
raise
|
||||
|
||||
@classmethod
|
||||
async def _update_affinity_record(cls, affinity_id: int, update_data: YifanAffinityUpdateSchema):
|
||||
"""更新缘分合盘记录"""
|
||||
from app.core.database import async_db_session
|
||||
|
||||
async with async_db_session() as session:
|
||||
try:
|
||||
# 创建一个临时的auth对象用于更新操作
|
||||
temp_auth = AuthSchema(user=None, db=session)
|
||||
crud = YifanAffinityCRUD(temp_auth)
|
||||
crud.session = session
|
||||
|
||||
await crud.update_yifan_affinity_crud(affinity_id, update_data)
|
||||
await session.commit() # 提交事务
|
||||
log.info(f"[缘分合盘] 记录更新成功,ID: {affinity_id}")
|
||||
except Exception as e:
|
||||
await session.rollback() # 回滚事务
|
||||
log.error(f"[缘分合盘] 记录更新失败,ID: {affinity_id}, 错误: {str(e)}")
|
||||
raise
|
||||
|
||||
@classmethod
|
||||
async def get_affinity_result_service(cls, auth: AuthSchema, affinity_id: int) -> AffinityCalculateResponseSchema:
|
||||
"""获取缘分合盘结果"""
|
||||
affinity_obj = await YifanAffinityCRUD(auth).get_yifan_affinity_crud(affinity_id)
|
||||
|
||||
if not affinity_obj:
|
||||
raise CustomException(msg="缘分合盘记录不存在")
|
||||
|
||||
if affinity_obj.task_status != 5:
|
||||
raise CustomException(msg="缘分合盘测算尚未完成")
|
||||
|
||||
# 构建响应数据
|
||||
response_data = {
|
||||
"relationship": affinity_obj.relationship,
|
||||
"relationship_label": cls._get_relationship_label(affinity_obj.relationship),
|
||||
"person1": {
|
||||
"name": affinity_obj.person1_name,
|
||||
"gender": affinity_obj.person1_gender,
|
||||
"birth_date": affinity_obj.person1_birth_date
|
||||
},
|
||||
"person2": {
|
||||
"name": affinity_obj.person2_name,
|
||||
"gender": affinity_obj.person2_gender,
|
||||
"birth_date": affinity_obj.person2_birth_date
|
||||
},
|
||||
"score": affinity_obj.score or 0,
|
||||
"score_badge": affinity_obj.score_badge or "中等婚",
|
||||
"six_dimension": json.loads(affinity_obj.six_dimension) if affinity_obj.six_dimension else {},
|
||||
"radar_desc": affinity_obj.radar_desc or "",
|
||||
"analysis_cards": json.loads(affinity_obj.analysis_cards) if affinity_obj.analysis_cards else [],
|
||||
"unlocked": json.loads(affinity_obj.unlocked_content) if affinity_obj.unlocked_content and affinity_obj.is_unlocked else None,
|
||||
"is_unlocked": bool(affinity_obj.is_unlocked),
|
||||
"unlock_price": float(affinity_obj.unlock_price or 9.9),
|
||||
"unlock_stats": {
|
||||
"unlock_count": 12392,
|
||||
"accuracy": "98%"
|
||||
}
|
||||
}
|
||||
|
||||
return AffinityCalculateResponseSchema(**response_data)
|
||||
|
||||
@classmethod
|
||||
def _get_relationship_label(cls, relationship: str) -> str:
|
||||
"""获取缘分类型标签"""
|
||||
labels = {
|
||||
"couple": "情侣",
|
||||
"married": "夫妻",
|
||||
"crush": "暗恋",
|
||||
"partner": "合伙",
|
||||
"friend": "知己",
|
||||
"family": "亲缘"
|
||||
}
|
||||
return labels.get(relationship, relationship)
|
||||
|
||||
@classmethod
|
||||
async def create_yifan_affinity_service(cls, auth: AuthSchema, data: YifanAffinityCreateSchema) -> dict:
|
||||
"""创建缘分合盘记录"""
|
||||
result_dict = await YifanAffinityCRUD(auth).create_yifan_affinity_crud(data=data)
|
||||
return YifanAffinityOutSchema.model_validate(result_dict).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def get_yifan_affinity_service(cls, auth: AuthSchema, affinity_id: int) -> dict:
|
||||
"""获取单个缘分合盘记录"""
|
||||
result_dict = await YifanAffinityCRUD(auth).get_yifan_affinity_crud(affinity_id=affinity_id)
|
||||
if not result_dict:
|
||||
raise CustomException(msg="缘分合盘记录不存在")
|
||||
return YifanAffinityOutSchema.model_validate(result_dict).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def update_yifan_affinity_service(cls, auth: AuthSchema, affinity_id: int, data: YifanAffinityUpdateSchema) -> dict:
|
||||
"""更新缘分合盘记录"""
|
||||
result_dict = await YifanAffinityCRUD(auth).update_yifan_affinity_crud(affinity_id=affinity_id, data=data)
|
||||
return YifanAffinityOutSchema.model_validate(result_dict).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def delete_yifan_affinity_service(cls, auth: AuthSchema, affinity_id: int) -> bool:
|
||||
"""删除缘分合盘记录"""
|
||||
return await YifanAffinityCRUD(auth).delete_yifan_affinity_crud(affinity_id=affinity_id)
|
||||
|
||||
@classmethod
|
||||
async def list_yifan_affinity_service(cls, auth: AuthSchema, search: YifanAffinityQueryParam | None = None, order_by: list[dict] | None = None) -> list[dict]:
|
||||
"""列表查询"""
|
||||
search_dict = search.__dict__ if search else None
|
||||
obj_list = await YifanAffinityCRUD(auth).list_yifan_affinity_crud(search=search_dict, order_by=order_by)
|
||||
return [YifanAffinityOutSchema.model_validate(obj).model_dump() for obj in obj_list]
|
||||
|
||||
@classmethod
|
||||
async def page_yifan_affinity_service(cls, auth: AuthSchema, page_no: int, page_size: int, search: YifanAffinityQueryParam | None = None, order_by: list[dict] | None = None) -> dict:
|
||||
"""分页查询"""
|
||||
search_dict = search.__dict__ if search else None
|
||||
result_dict = await YifanAffinityCRUD(auth).page_yifan_affinity_crud(page_no=page_no, page_size=page_size, search=search_dict, order_by=order_by)
|
||||
|
||||
# 基础CRUD返回的是items键,我们需要转换为data键以保持API一致性
|
||||
if "items" in result_dict:
|
||||
result_dict["data"] = result_dict.pop("items")
|
||||
|
||||
return result_dict
|
||||
@@ -0,0 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from .controller import YifanBaziZejiRouter
|
||||
|
||||
__all__ = ["YifanBaziZejiRouter"]
|
||||
@@ -0,0 +1,102 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from app.core.dependencies import AuthPermission
|
||||
from app.common.response import SuccessResponse
|
||||
from app.core.base_params import PaginationQueryParam
|
||||
from app.core.logger import log
|
||||
from app.api.v1.module_yifan.yifan_bazi_zeji.service import YifanBaziZejiService
|
||||
from app.api.v1.module_yifan.yifan_bazi_zeji.schema import (
|
||||
YifanBaziZejiCreateSchema, YifanBaziZejiUpdateSchema, YifanBaziZejiQueryParam,
|
||||
BaziZejiCalculateRequestSchema
|
||||
)
|
||||
|
||||
YifanBaziZejiRouter = APIRouter(prefix="/yifan_bazi_zeji", tags=["八字择吉测算"])
|
||||
|
||||
|
||||
@YifanBaziZejiRouter.post("/calculate", summary="八字择吉测算", description="八字择吉测算,创建任务并异步生成分析结果")
|
||||
async def calculate_bazi_zeji_controller(
|
||||
data: BaziZejiCalculateRequestSchema,
|
||||
auth: AuthSchema = Depends(AuthPermission([]))
|
||||
) -> JSONResponse:
|
||||
"""八字择吉测算接口 - 创建任务,后台异步生成分析结果"""
|
||||
result_dict = await YifanBaziZejiService.calculate_bazi_zeji_service(auth=auth, data=data)
|
||||
log.info(f"八字择吉任务已创建: {data.name} - {data.zeji_type}")
|
||||
return SuccessResponse(data=result_dict, msg="八字择吉任务已创建,正在测算中")
|
||||
|
||||
|
||||
@YifanBaziZejiRouter.get("/result/{zeji_id}", summary="获取八字择吉结果", description="根据ID获取八字择吉测算结果")
|
||||
async def get_bazi_zeji_result_controller(
|
||||
zeji_id: int,
|
||||
auth: AuthSchema = Depends(AuthPermission([]))
|
||||
) -> JSONResponse:
|
||||
"""获取八字择吉结果接口"""
|
||||
result_dict = await YifanBaziZejiService.get_bazi_zeji_result_service(auth=auth, zeji_id=zeji_id)
|
||||
log.info(f"获取八字择吉结果成功: ID={zeji_id}")
|
||||
return SuccessResponse(data=result_dict.model_dump(), msg="获取八字择吉结果成功")
|
||||
|
||||
|
||||
@YifanBaziZejiRouter.get("/list", summary="查询八字择吉列表", description="查询八字择吉测算列表")
|
||||
async def get_yifan_bazi_zeji_list_controller(
|
||||
page: PaginationQueryParam = Depends(),
|
||||
search: YifanBaziZejiQueryParam = Depends(),
|
||||
auth: AuthSchema = Depends(AuthPermission([]))
|
||||
) -> JSONResponse:
|
||||
"""查询八字择吉列表接口(数据库分页)"""
|
||||
result_dict = await YifanBaziZejiService.page_yifan_bazi_zeji_service(
|
||||
auth=auth,
|
||||
page_no=page.page_no if page.page_no is not None else 1,
|
||||
page_size=page.page_size if page.page_size is not None else 10,
|
||||
search=search,
|
||||
order_by=page.order_by
|
||||
)
|
||||
log.info("查询八字择吉列表成功")
|
||||
return SuccessResponse(data=result_dict, msg="查询八字择吉列表成功")
|
||||
|
||||
|
||||
@YifanBaziZejiRouter.get("/{zeji_id}", summary="获取八字择吉详情", description="根据ID获取八字择吉详情")
|
||||
async def get_yifan_bazi_zeji_controller(
|
||||
zeji_id: int,
|
||||
auth: AuthSchema = Depends(AuthPermission([]))
|
||||
) -> JSONResponse:
|
||||
"""获取八字择吉详情接口"""
|
||||
result_dict = await YifanBaziZejiService.get_yifan_bazi_zeji_service(auth=auth, zeji_id=zeji_id)
|
||||
log.info("获取八字择吉详情成功")
|
||||
return SuccessResponse(data=result_dict, msg="获取八字择吉详情成功")
|
||||
|
||||
|
||||
@YifanBaziZejiRouter.post("/create", summary="创建八字择吉", description="创建八字择吉测算记录")
|
||||
async def create_yifan_bazi_zeji_controller(
|
||||
data: YifanBaziZejiCreateSchema,
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_bazi_zeji:create"]))
|
||||
) -> JSONResponse:
|
||||
"""创建八字择吉接口"""
|
||||
result_dict = await YifanBaziZejiService.create_yifan_bazi_zeji_service(auth=auth, data=data)
|
||||
log.info("创建八字择吉成功")
|
||||
return SuccessResponse(data=result_dict, msg="创建八字择吉成功")
|
||||
|
||||
|
||||
@YifanBaziZejiRouter.put("/{zeji_id}", summary="更新八字择吉", description="根据ID更新八字择吉测算记录")
|
||||
async def update_yifan_bazi_zeji_controller(
|
||||
zeji_id: int,
|
||||
data: YifanBaziZejiUpdateSchema,
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_bazi_zeji:update"]))
|
||||
) -> JSONResponse:
|
||||
"""更新八字择吉接口"""
|
||||
result_dict = await YifanBaziZejiService.update_yifan_bazi_zeji_service(auth=auth, zeji_id=zeji_id, data=data)
|
||||
log.info("更新八字择吉成功")
|
||||
return SuccessResponse(data=result_dict, msg="更新八字择吉成功")
|
||||
|
||||
|
||||
@YifanBaziZejiRouter.delete("/{zeji_id}", summary="删除八字择吉", description="根据ID删除八字择吉测算记录")
|
||||
async def delete_yifan_bazi_zeji_controller(
|
||||
zeji_id: int,
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_bazi_zeji:delete"]))
|
||||
) -> JSONResponse:
|
||||
"""删除八字择吉接口"""
|
||||
result = await YifanBaziZejiService.delete_yifan_bazi_zeji_service(auth=auth, zeji_id=zeji_id)
|
||||
log.info("删除八字择吉成功")
|
||||
return SuccessResponse(data=result, msg="删除八字择吉成功")
|
||||
@@ -0,0 +1,112 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from typing import Sequence
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.base_crud import CRUDBase
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from app.api.v1.module_yifan.yifan_bazi_zeji.model import YifanBaziZejiModel
|
||||
from app.api.v1.module_yifan.yifan_bazi_zeji.schema import YifanBaziZejiCreateSchema, YifanBaziZejiUpdateSchema
|
||||
|
||||
|
||||
class YifanBaziZejiCRUD(CRUDBase[YifanBaziZejiModel, YifanBaziZejiCreateSchema, YifanBaziZejiUpdateSchema]):
|
||||
"""八字择吉测算CRUD操作"""
|
||||
|
||||
def __init__(self, auth: AuthSchema):
|
||||
super().__init__(model=YifanBaziZejiModel, auth=auth)
|
||||
|
||||
async def create_yifan_bazi_zeji_crud(self, data: YifanBaziZejiCreateSchema) -> YifanBaziZejiModel:
|
||||
"""
|
||||
创建八字择吉测算记录
|
||||
|
||||
参数:
|
||||
- data (YifanBaziZejiCreateSchema): 创建数据
|
||||
|
||||
返回:
|
||||
- YifanBaziZejiModel: 模型实例
|
||||
"""
|
||||
return await self.create(data=data)
|
||||
|
||||
async def get_yifan_bazi_zeji_crud(self, zeji_id: int, preload: list | None = None) -> YifanBaziZejiModel | None:
|
||||
"""
|
||||
获取单个八字择吉测算记录
|
||||
|
||||
参数:
|
||||
- zeji_id (int): 记录ID
|
||||
- preload (list | None): 预加载关系,未提供时使用模型默认项
|
||||
|
||||
返回:
|
||||
- YifanBaziZejiModel | None: 模型实例或None
|
||||
"""
|
||||
return await self.get(id=zeji_id, preload=preload)
|
||||
|
||||
async def update_yifan_bazi_zeji_crud(self, zeji_id: int, data: YifanBaziZejiUpdateSchema) -> YifanBaziZejiModel:
|
||||
"""
|
||||
更新八字择吉测算记录
|
||||
|
||||
参数:
|
||||
- zeji_id (int): 记录ID
|
||||
- data (YifanBaziZejiUpdateSchema): 更新数据
|
||||
|
||||
返回:
|
||||
- YifanBaziZejiModel: 更新后的模型实例
|
||||
"""
|
||||
return await self.update(id=zeji_id, data=data)
|
||||
|
||||
async def delete_yifan_bazi_zeji_crud(self, zeji_id: int) -> bool:
|
||||
"""
|
||||
删除八字择吉测算记录
|
||||
|
||||
参数:
|
||||
- zeji_id (int): 记录ID
|
||||
|
||||
返回:
|
||||
- bool: 删除是否成功
|
||||
"""
|
||||
return await self.delete(id=zeji_id)
|
||||
|
||||
async def list_yifan_bazi_zeji_crud(self, search: dict | None = None, order_by: list[dict] | None = None, preload: list | None = None) -> Sequence[YifanBaziZejiModel]:
|
||||
"""
|
||||
列表查询
|
||||
|
||||
参数:
|
||||
- search (dict | None): 查询参数
|
||||
- order_by (list[dict] | None): 排序参数,未提供时使用模型默认项
|
||||
- preload (list | None): 预加载关系,未提供时使用模型默认项
|
||||
|
||||
返回:
|
||||
- Sequence[YifanBaziZejiModel]: 模型实例序列
|
||||
"""
|
||||
return await self.list(search=search, order_by=order_by, preload=preload)
|
||||
|
||||
async def page_yifan_bazi_zeji_crud(self, page_no: int, page_size: int, search: dict | None = None, order_by: list[dict] | None = None, preload: list | None = None) -> dict:
|
||||
"""
|
||||
分页查询
|
||||
|
||||
参数:
|
||||
- page_no (int): 页码
|
||||
- page_size (int): 每页数量
|
||||
- search (dict | None): 查询参数
|
||||
- order_by (list[dict] | None): 排序参数,未提供时使用模型默认项
|
||||
- preload (list | None): 预加载关系,未提供时使用模型默认项
|
||||
|
||||
返回:
|
||||
- dict: 分页结果字典
|
||||
"""
|
||||
from app.api.v1.module_yifan.yifan_bazi_zeji.schema import YifanBaziZejiOutSchema
|
||||
|
||||
# 将页码转换为偏移量
|
||||
offset = (page_no - 1) * page_size
|
||||
|
||||
# 设置默认排序
|
||||
if order_by is None:
|
||||
order_by = [{"field": "created_time", "order": "desc"}]
|
||||
|
||||
return await self.page(
|
||||
offset=offset,
|
||||
limit=page_size,
|
||||
search=search or {},
|
||||
order_by=order_by,
|
||||
out_schema=YifanBaziZejiOutSchema,
|
||||
preload=preload
|
||||
)
|
||||
@@ -0,0 +1,71 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import datetime
|
||||
from sqlalchemy import Integer, DateTime, SmallInteger, Text, String, JSON, Numeric, Date
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.base_model import ModelMixin, UserMixin
|
||||
|
||||
|
||||
class YifanBaziZejiModel(ModelMixin, UserMixin):
|
||||
"""
|
||||
八字择吉测算表
|
||||
"""
|
||||
__tablename__: str = 'yifan_bazi_zeji'
|
||||
__table_args__: dict[str, str] = {'comment': '八字择吉测算'}
|
||||
__loader_options__: list[str] = ["created_by", "updated_by"]
|
||||
|
||||
is_deleted: Mapped[int | None] = mapped_column(SmallInteger, nullable=True, comment='是否删除(0否 1是)')
|
||||
task_status: Mapped[int | None] = mapped_column(SmallInteger, nullable=True, comment='任务状态(1已创建 2测算中 5测算成功 3测算超时 0任务失败)')
|
||||
user_id: Mapped[int | None] = mapped_column(Integer, nullable=True, comment='用户ID')
|
||||
|
||||
# 基本信息
|
||||
name: Mapped[str | None] = mapped_column(String(100), nullable=True, comment='姓名')
|
||||
gender: Mapped[str | None] = mapped_column(String(10), nullable=True, comment='性别(male男 female女)')
|
||||
birth_date: Mapped[str | None] = mapped_column(String(100), nullable=True, comment='生辰显示格式')
|
||||
birth_date_api: Mapped[datetime.datetime | None] = mapped_column(DateTime, nullable=True, comment='生辰API格式')
|
||||
birth_place: Mapped[str | None] = mapped_column(String(255), nullable=True, comment='出生地')
|
||||
|
||||
# 择吉类型和事项
|
||||
zeji_type: Mapped[str | None] = mapped_column(String(50), nullable=True, comment='择吉类型(wedding婚嫁 business开业 move搬家 travel出行 investment投资 surgery手术 contract签约 other其他)')
|
||||
zeji_purpose: Mapped[str | None] = mapped_column(Text, nullable=True, comment='择吉目的描述')
|
||||
date_range_start: Mapped[datetime.date | None] = mapped_column(Date, nullable=True, comment='期望日期范围开始')
|
||||
date_range_end: Mapped[datetime.date | None] = mapped_column(Date, nullable=True, comment='期望日期范围结束')
|
||||
|
||||
# 测算结果
|
||||
lucky_score: Mapped[int | None] = mapped_column(Integer, nullable=True, comment='总体吉凶评分(0-100)')
|
||||
lucky_level: Mapped[str | None] = mapped_column(String(32), nullable=True, comment='吉凶等级(大吉 中吉 小吉 平 小凶 中凶 大凶)')
|
||||
|
||||
# 推荐日期(JSON格式存储)
|
||||
recommended_dates: Mapped[str | None] = mapped_column(JSON, nullable=True, comment='推荐吉日列表JSON')
|
||||
|
||||
# 五行分析(JSON格式存储)
|
||||
wuxing_analysis: Mapped[str | None] = mapped_column(JSON, nullable=True, comment='五行分析JSON')
|
||||
|
||||
# 八字分析
|
||||
bazi_analysis: Mapped[str | None] = mapped_column(Text, nullable=True, comment='八字详细分析')
|
||||
tiangang_analysis: Mapped[str | None] = mapped_column(Text, nullable=True, comment='天干地支分析')
|
||||
|
||||
# 择吉建议
|
||||
zeji_suggestions: Mapped[str | None] = mapped_column(Text, nullable=True, comment='择吉建议')
|
||||
avoid_suggestions: Mapped[str | None] = mapped_column(Text, nullable=True, comment='忌避建议')
|
||||
best_time_periods: Mapped[str | None] = mapped_column(JSON, nullable=True, comment='最佳时辰JSON')
|
||||
|
||||
# 风水建议
|
||||
fengshui_tips: Mapped[str | None] = mapped_column(Text, nullable=True, comment='风水建议')
|
||||
lucky_directions: Mapped[str | None] = mapped_column(JSON, nullable=True, comment='吉利方位JSON')
|
||||
lucky_colors: Mapped[str | None] = mapped_column(JSON, nullable=True, comment='幸运颜色JSON')
|
||||
lucky_numbers: Mapped[str | None] = mapped_column(JSON, nullable=True, comment='幸运数字JSON')
|
||||
|
||||
# 解锁状态
|
||||
is_unlocked: Mapped[int | None] = mapped_column(SmallInteger, nullable=True, comment='是否已解锁深度报告(0否 1是)')
|
||||
unlock_price: Mapped[str | None] = mapped_column(Numeric(10, 2), nullable=True, comment='解锁价格')
|
||||
|
||||
# 解锁后的深度内容(JSON格式存储)
|
||||
unlocked_content: Mapped[str | None] = mapped_column(JSON, nullable=True, comment='解锁后的深度内容JSON')
|
||||
|
||||
# AI原始数据
|
||||
ai_source_data: Mapped[str | None] = mapped_column(Text, nullable=True, comment='AI原始数据')
|
||||
error_message: Mapped[str | None] = mapped_column(Text, nullable=True, comment='错误信息')
|
||||
|
||||
remark: Mapped[str | None] = mapped_column(String(255), nullable=True, comment='备注')
|
||||
@@ -0,0 +1,226 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import datetime
|
||||
from typing import List, Dict, Any, Optional
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from fastapi import Query
|
||||
|
||||
from app.core.validator import DateTimeStr
|
||||
from app.core.base_schema import BaseSchema, UserBySchema
|
||||
|
||||
|
||||
class RecommendedDateSchema(BaseModel):
|
||||
"""推荐日期模型"""
|
||||
date: str = Field(..., description='日期', example='2024-03-15')
|
||||
lunar_date: str = Field(..., description='农历日期', example='二月初六')
|
||||
weekday: str = Field(..., description='星期', example='星期五')
|
||||
score: int = Field(..., description='评分', ge=0, le=100, example=92)
|
||||
level: str = Field(..., description='吉凶等级', example='大吉')
|
||||
best_times: List[str] = Field(..., description='最佳时辰', example=['09:00-11:00', '13:00-15:00'])
|
||||
reason: str = Field(..., description='推荐理由')
|
||||
yi: List[str] = Field(..., description='宜做事项', example=['开市', '签约', '祈福'])
|
||||
ji: List[str] = Field(..., description='忌做事项', example=['动土', '安葬'])
|
||||
|
||||
|
||||
class WuxingAnalysisSchema(BaseModel):
|
||||
"""五行分析模型"""
|
||||
personal_wuxing: str = Field(..., description='个人五行特点', example='金旺木弱')
|
||||
suitable_elements: List[str] = Field(..., description='适宜五行', example=['水', '木'])
|
||||
avoid_elements: List[str] = Field(..., description='忌避五行', example=['火', '土'])
|
||||
balance_suggestion: str = Field(..., description='平衡建议')
|
||||
|
||||
|
||||
class TimePeriodSchema(BaseModel):
|
||||
"""时辰模型"""
|
||||
time: str = Field(..., description='时间段', example='09:00-11:00')
|
||||
name: str = Field(..., description='时辰名', example='巳时')
|
||||
description: str = Field(..., description='时辰描述')
|
||||
|
||||
|
||||
class UnlockedContentSchema(BaseModel):
|
||||
"""解锁内容模型"""
|
||||
detailed_bazi: str = Field(..., description='详细八字解析')
|
||||
yearly_fortune: str = Field(..., description='全年运势分析')
|
||||
monthly_guide: str = Field(..., description='逐月择吉指南')
|
||||
master_advice: str = Field(..., description='大师特别建议')
|
||||
|
||||
|
||||
class BaziZejiCalculateRequestSchema(BaseModel):
|
||||
"""八字择吉测算请求模型"""
|
||||
name: str = Field(..., description='姓名', example='张三')
|
||||
gender: str = Field(..., description='性别(male/female)', example='male')
|
||||
birth_date: str = Field(..., description='生辰八字(显示格式)', example='1990年1月1日 子时')
|
||||
birth_date_api: datetime.datetime = Field(..., description='生辰八字(API格式)', example='1990-01-01 00:30:00')
|
||||
birth_place: str = Field(..., description='出生地', example='北京市')
|
||||
zeji_type: str = Field(..., description='择吉类型', example='wedding')
|
||||
zeji_purpose: str = Field(..., description='择吉目的描述', example='选择结婚吉日')
|
||||
date_range_start: datetime.date = Field(..., description='期望日期范围开始', example='2024-03-01')
|
||||
date_range_end: datetime.date = Field(..., description='期望日期范围结束', example='2024-06-30')
|
||||
|
||||
|
||||
class BaziZejiCalculateResponseSchema(BaseModel):
|
||||
"""八字择吉测算响应模型"""
|
||||
zeji_id: int = Field(..., description='择吉记录ID')
|
||||
name: str = Field(..., description='姓名')
|
||||
zeji_type: str = Field(..., description='择吉类型')
|
||||
zeji_type_label: str = Field(..., description='择吉类型标签')
|
||||
lucky_score: int = Field(..., description='总体吉凶评分', ge=0, le=100)
|
||||
lucky_level: str = Field(..., description='吉凶等级')
|
||||
|
||||
# 推荐日期
|
||||
recommended_dates: List[RecommendedDateSchema] = Field(..., description='推荐吉日列表')
|
||||
|
||||
# 五行分析
|
||||
wuxing_analysis: WuxingAnalysisSchema = Field(..., description='五行分析')
|
||||
|
||||
# 八字分析
|
||||
bazi_analysis: str = Field(..., description='八字详细分析')
|
||||
tiangang_analysis: str = Field(..., description='天干地支分析')
|
||||
|
||||
# 择吉建议
|
||||
zeji_suggestions: str = Field(..., description='择吉建议')
|
||||
avoid_suggestions: str = Field(..., description='忌避建议')
|
||||
best_time_periods: List[TimePeriodSchema] = Field(..., description='最佳时辰')
|
||||
|
||||
# 风水建议
|
||||
fengshui_tips: str = Field(..., description='风水建议')
|
||||
lucky_directions: List[str] = Field(..., description='吉利方位')
|
||||
lucky_colors: List[str] = Field(..., description='幸运颜色')
|
||||
lucky_numbers: List[int] = Field(..., description='幸运数字')
|
||||
|
||||
# 解锁状态
|
||||
unlocked: Optional[UnlockedContentSchema] = Field(None, description='解锁后的深度内容')
|
||||
is_unlocked: bool = Field(..., description='是否已解锁深度报告')
|
||||
unlock_price: float = Field(..., description='解锁价格(元)')
|
||||
|
||||
|
||||
class YifanBaziZejiCreateSchema(BaseModel):
|
||||
"""
|
||||
八字择吉测算新增模型
|
||||
"""
|
||||
is_deleted: int = Field(default=0, description='是否删除(0否 1是)')
|
||||
task_status: int = Field(default=1, description='任务状态(1已创建 2测算中 5测算成功 3测算超时 0任务失败)')
|
||||
user_id: int | None = Field(default=None, description='用户ID')
|
||||
name: str = Field(default=..., description='姓名')
|
||||
gender: str = Field(default=..., description='性别(male男 female女)')
|
||||
birth_date: str = Field(default=..., description='生辰显示格式')
|
||||
birth_date_api: datetime.datetime = Field(default=..., description='生辰API格式')
|
||||
birth_place: str = Field(default=..., description='出生地')
|
||||
zeji_type: str = Field(default=..., description='择吉类型')
|
||||
zeji_purpose: str = Field(default=..., description='择吉目的描述')
|
||||
date_range_start: datetime.date = Field(default=..., description='期望日期范围开始')
|
||||
date_range_end: datetime.date = Field(default=..., description='期望日期范围结束')
|
||||
lucky_score: int | None = Field(default=None, description='总体吉凶评分(0-100)')
|
||||
lucky_level: str | None = Field(default=None, description='吉凶等级')
|
||||
recommended_dates: str | None = Field(default=None, description='推荐吉日列表JSON')
|
||||
wuxing_analysis: str | None = Field(default=None, description='五行分析JSON')
|
||||
bazi_analysis: str | None = Field(default=None, description='八字详细分析')
|
||||
tiangang_analysis: str | None = Field(default=None, description='天干地支分析')
|
||||
zeji_suggestions: str | None = Field(default=None, description='择吉建议')
|
||||
avoid_suggestions: str | None = Field(default=None, description='忌避建议')
|
||||
best_time_periods: str | None = Field(default=None, description='最佳时辰JSON')
|
||||
fengshui_tips: str | None = Field(default=None, description='风水建议')
|
||||
lucky_directions: str | None = Field(default=None, description='吉利方位JSON')
|
||||
lucky_colors: str | None = Field(default=None, description='幸运颜色JSON')
|
||||
lucky_numbers: str | None = Field(default=None, description='幸运数字JSON')
|
||||
is_unlocked: int = Field(default=0, description='是否已解锁深度报告')
|
||||
unlock_price: float | None = Field(default=19.90, description='解锁价格')
|
||||
unlocked_content: str | None = Field(default=None, description='解锁后的深度内容JSON')
|
||||
ai_source_data: str | None = Field(default=None, description='AI原始数据')
|
||||
error_message: str | None = Field(default=None, description='错误信息')
|
||||
remark: str | None = Field(default=None, description='备注')
|
||||
|
||||
|
||||
class YifanBaziZejiUpdateSchema(BaseModel):
|
||||
"""
|
||||
八字择吉测算更新模型
|
||||
"""
|
||||
is_deleted: int | None = Field(default=None, description='是否删除(0否 1是)')
|
||||
task_status: int | None = Field(default=None, description='任务状态(1已创建 2测算中 5测算成功 3测算超时 0任务失败)')
|
||||
user_id: int | None = Field(default=None, description='用户ID')
|
||||
name: str | None = Field(default=None, description='姓名')
|
||||
gender: str | None = Field(default=None, description='性别(male男 female女)')
|
||||
birth_date: str | None = Field(default=None, description='生辰显示格式')
|
||||
birth_date_api: datetime.datetime | None = Field(default=None, description='生辰API格式')
|
||||
birth_place: str | None = Field(default=None, description='出生地')
|
||||
zeji_type: str | None = Field(default=None, description='择吉类型')
|
||||
zeji_purpose: str | None = Field(default=None, description='择吉目的描述')
|
||||
date_range_start: datetime.date | None = Field(default=None, description='期望日期范围开始')
|
||||
date_range_end: datetime.date | None = Field(default=None, description='期望日期范围结束')
|
||||
lucky_score: int | None = Field(default=None, description='总体吉凶评分(0-100)')
|
||||
lucky_level: str | None = Field(default=None, description='吉凶等级')
|
||||
recommended_dates: str | None = Field(default=None, description='推荐吉日列表JSON')
|
||||
wuxing_analysis: str | None = Field(default=None, description='五行分析JSON')
|
||||
bazi_analysis: str | None = Field(default=None, description='八字详细分析')
|
||||
tiangang_analysis: str | None = Field(default=None, description='天干地支分析')
|
||||
zeji_suggestions: str | None = Field(default=None, description='择吉建议')
|
||||
avoid_suggestions: str | None = Field(default=None, description='忌避建议')
|
||||
best_time_periods: str | None = Field(default=None, description='最佳时辰JSON')
|
||||
fengshui_tips: str | None = Field(default=None, description='风水建议')
|
||||
lucky_directions: str | None = Field(default=None, description='吉利方位JSON')
|
||||
lucky_colors: str | None = Field(default=None, description='幸运颜色JSON')
|
||||
lucky_numbers: str | None = Field(default=None, description='幸运数字JSON')
|
||||
is_unlocked: int | None = Field(default=None, description='是否已解锁深度报告')
|
||||
unlock_price: float | None = Field(default=None, description='解锁价格')
|
||||
unlocked_content: str | None = Field(default=None, description='解锁后的深度内容JSON')
|
||||
ai_source_data: str | None = Field(default=None, description='AI原始数据')
|
||||
error_message: str | None = Field(default=None, description='错误信息')
|
||||
remark: str | None = Field(default=None, description='备注')
|
||||
|
||||
|
||||
class YifanBaziZejiOutSchema(YifanBaziZejiCreateSchema, BaseSchema, UserBySchema):
|
||||
"""
|
||||
八字择吉测算响应模型
|
||||
"""
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class YifanBaziZejiQueryParam:
|
||||
"""八字择吉测算查询参数"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str | None = Query(None, description="姓名"),
|
||||
gender: str | None = Query(None, description="性别(male男 female女)"),
|
||||
zeji_type: str | None = Query(None, description="择吉类型"),
|
||||
lucky_level: str | None = Query(None, description="吉凶等级"),
|
||||
score_min: int | None = Query(None, description="最低分数"),
|
||||
score_max: int | None = Query(None, description="最高分数"),
|
||||
is_unlocked: int | None = Query(None, description="是否已解锁深度报告"),
|
||||
created_id: int | None = Query(None, description="创建人ID"),
|
||||
updated_id: int | None = Query(None, description="更新人ID"),
|
||||
is_deleted: int | None = Query(None, description="是否删除(0否 1是)"),
|
||||
task_status: int | None = Query(None, description="任务状态"),
|
||||
user_id: int | None = Query(None, description="用户ID"),
|
||||
created_time: list[DateTimeStr] | None = Query(None, description="创建时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]),
|
||||
updated_time: list[DateTimeStr] | None = Query(None, description="更新时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]),
|
||||
|
||||
) -> None:
|
||||
|
||||
# 精确查询字段
|
||||
self.created_id = created_id
|
||||
self.updated_id = updated_id
|
||||
self.is_deleted = is_deleted
|
||||
self.task_status = task_status
|
||||
self.user_id = user_id
|
||||
self.is_unlocked = is_unlocked
|
||||
|
||||
# 模糊查询字段
|
||||
self.name = ("like", name)
|
||||
self.gender = ("like", gender)
|
||||
self.zeji_type = ("like", zeji_type)
|
||||
self.lucky_level = ("like", lucky_level)
|
||||
|
||||
# 范围查询字段
|
||||
if score_min is not None and score_max is not None:
|
||||
self.lucky_score = ("between", (score_min, score_max))
|
||||
elif score_min is not None:
|
||||
self.lucky_score = (">=", score_min)
|
||||
elif score_max is not None:
|
||||
self.lucky_score = ("<=", score_max)
|
||||
|
||||
# 时间范围查询
|
||||
if created_time and len(created_time) == 2:
|
||||
self.created_time = ("between", (created_time[0], created_time[1]))
|
||||
if updated_time and len(updated_time) == 2:
|
||||
self.updated_time = ("between", (updated_time[0], updated_time[1]))
|
||||
@@ -0,0 +1,420 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import json
|
||||
import asyncio
|
||||
from typing import Dict, Any
|
||||
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from app.core.exceptions import CustomException
|
||||
from app.core.logger import log
|
||||
from app.api.v1.module_yifan.yifan_bazi_zeji.crud import YifanBaziZejiCRUD
|
||||
from app.api.v1.module_yifan.yifan_bazi_zeji.schema import (
|
||||
YifanBaziZejiCreateSchema, YifanBaziZejiUpdateSchema, YifanBaziZejiOutSchema, YifanBaziZejiQueryParam,
|
||||
BaziZejiCalculateRequestSchema, BaziZejiCalculateResponseSchema
|
||||
)
|
||||
|
||||
|
||||
class YifanBaziZejiService:
|
||||
"""八字择吉测算服务"""
|
||||
|
||||
@classmethod
|
||||
async def calculate_bazi_zeji_service(cls, auth: AuthSchema, data: BaziZejiCalculateRequestSchema) -> Dict[str, Any]:
|
||||
"""
|
||||
八字择吉测算服务(异步模式)
|
||||
|
||||
1. 校验数据,创建报告记录(status=1 已创建)
|
||||
2. 启动后台任务执行AI测算
|
||||
3. 立即返回 zeji_id + status
|
||||
"""
|
||||
user_id = auth.user.id if auth.user else 0
|
||||
|
||||
# 创建八字择吉测算记录
|
||||
create_data = YifanBaziZejiCreateSchema(
|
||||
user_id=user_id,
|
||||
name=data.name,
|
||||
gender=data.gender,
|
||||
birth_date=data.birth_date,
|
||||
birth_date_api=data.birth_date_api,
|
||||
birth_place=data.birth_place,
|
||||
zeji_type=data.zeji_type,
|
||||
zeji_purpose=data.zeji_purpose,
|
||||
date_range_start=data.date_range_start,
|
||||
date_range_end=data.date_range_end,
|
||||
task_status=1,
|
||||
is_deleted=0
|
||||
)
|
||||
|
||||
try:
|
||||
zeji_obj = await YifanBaziZejiCRUD(auth).create_yifan_bazi_zeji_crud(data=create_data)
|
||||
zeji_id = zeji_obj.id
|
||||
log.info(f"[八字择吉] 报告创建成功,ID: {zeji_id}")
|
||||
except Exception as e:
|
||||
log.error(f"[八字择吉] 报告创建失败: {str(e)}")
|
||||
raise CustomException(msg=f"保存八字择吉报告失败: {str(e)}")
|
||||
|
||||
# 启动后台任务执行AI测算
|
||||
asyncio.create_task(
|
||||
cls._process_bazi_zeji_calculation(
|
||||
zeji_id=zeji_id,
|
||||
data=data,
|
||||
user_id=user_id,
|
||||
)
|
||||
)
|
||||
|
||||
return {"zeji_id": zeji_id, "status": 1}
|
||||
|
||||
@classmethod
|
||||
async def _process_bazi_zeji_calculation(
|
||||
cls,
|
||||
zeji_id: int,
|
||||
data: BaziZejiCalculateRequestSchema,
|
||||
user_id: int,
|
||||
) -> None:
|
||||
"""后台任务:执行八字择吉AI测算"""
|
||||
log_prefix = f"[八字择吉-{zeji_id}]"
|
||||
|
||||
try:
|
||||
# 更新状态为测算中
|
||||
await cls._update_zeji_status(zeji_id, task_status=2)
|
||||
log.info(f"{log_prefix} 开始AI测算")
|
||||
|
||||
# 构建AI输入文本
|
||||
zeji_type_labels = {
|
||||
"wedding": "婚嫁择吉",
|
||||
"business": "开业择吉",
|
||||
"move": "搬家择吉",
|
||||
"travel": "出行择吉",
|
||||
"investment": "投资择吉",
|
||||
"surgery": "手术择吉",
|
||||
"contract": "签约择吉",
|
||||
"other": "其他择吉"
|
||||
}
|
||||
|
||||
zeji_type_label = zeji_type_labels.get(data.zeji_type, data.zeji_type)
|
||||
|
||||
ai_input_text = f"""八字择吉测算请求:
|
||||
姓名:{data.name}
|
||||
性别:{'男' if data.gender == 'male' else '女'}
|
||||
生辰:{data.birth_date}
|
||||
出生地:{data.birth_place}
|
||||
择吉类型:{zeji_type_label}
|
||||
择吉目的:{data.zeji_purpose}
|
||||
期望日期范围:{data.date_range_start} 至 {data.date_range_end}
|
||||
|
||||
请根据以上信息进行八字择吉测算,分析个人八字特点,推荐最适宜的吉日良辰,并提供详细的择吉建议。"""
|
||||
|
||||
# 调用AI服务进行测算
|
||||
ai_response = await cls._call_ai_service(ai_input_text)
|
||||
log.info(f"ai_response is {ai_response}")
|
||||
log.info(f"[DEBUG] 调用前 ai_response type: {type(ai_response)}, length: {len(ai_response) if ai_response else 'None'}")
|
||||
|
||||
if not ai_response:
|
||||
raise Exception("AI服务返回空结果")
|
||||
|
||||
# 解析AI响应并构建结果数据
|
||||
result_data = cls._parse_ai_response(ai_response, data, zeji_type_label)
|
||||
log.info(f"result_data is {result_data}")
|
||||
|
||||
# 更新数据库记录
|
||||
update_data = YifanBaziZejiUpdateSchema(
|
||||
lucky_score=result_data.get('lucky_score'),
|
||||
lucky_level=result_data.get('lucky_level'),
|
||||
recommended_dates=json.dumps(result_data.get('recommended_dates'), ensure_ascii=False),
|
||||
wuxing_analysis=json.dumps(result_data.get('wuxing_analysis'), ensure_ascii=False),
|
||||
bazi_analysis=result_data.get('bazi_analysis'),
|
||||
tiangang_analysis=result_data.get('tiangang_analysis'),
|
||||
zeji_suggestions=result_data.get('zeji_suggestions'),
|
||||
avoid_suggestions=result_data.get('avoid_suggestions'),
|
||||
best_time_periods=json.dumps(result_data.get('best_time_periods'), ensure_ascii=False),
|
||||
fengshui_tips=result_data.get('fengshui_tips'),
|
||||
lucky_directions=json.dumps(result_data.get('lucky_directions'), ensure_ascii=False),
|
||||
lucky_colors=json.dumps(result_data.get('lucky_colors'), ensure_ascii=False),
|
||||
lucky_numbers=json.dumps(result_data.get('lucky_numbers'), ensure_ascii=False),
|
||||
unlocked_content=json.dumps(result_data.get('unlocked_content'), ensure_ascii=False),
|
||||
ai_source_data=ai_response,
|
||||
task_status=5 # 测算成功
|
||||
)
|
||||
|
||||
await cls._update_zeji_record(zeji_id, update_data)
|
||||
log.info(f"{log_prefix} AI测算完成并保存成功")
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
log.error(f"{log_prefix} AI测算超时")
|
||||
await cls._update_zeji_status(zeji_id, task_status=3, error_message="AI测算超时")
|
||||
except Exception as e:
|
||||
log.error(f"{log_prefix} AI测算失败: {str(e)}")
|
||||
await cls._update_zeji_status(zeji_id, task_status=0, error_message=str(e)[:500])
|
||||
|
||||
@classmethod
|
||||
async def _call_ai_service(cls, text: str) -> str:
|
||||
"""
|
||||
调用AI服务进行八字择吉测算
|
||||
|
||||
Args:
|
||||
text: 输入文本
|
||||
|
||||
Returns:
|
||||
AI响应结果
|
||||
"""
|
||||
from app.api.v1.module_application.ai.service import AIModelTestService
|
||||
|
||||
log.info(f"[八字择吉] 调用AI服务,输入长度: {len(text)}")
|
||||
|
||||
try:
|
||||
# 调用实际的AI服务,使用bazi_zeji模型类型
|
||||
ai_response = await AIModelTestService.test_naming(
|
||||
model_type="bazi_zeji",
|
||||
text=text
|
||||
)
|
||||
|
||||
log.info(f"[八字择吉] AI服务响应成功,响应长度: {len(ai_response)}")
|
||||
return ai_response
|
||||
|
||||
except Exception as e:
|
||||
log.error(f"[八字择吉] AI服务调用失败: {str(e)}")
|
||||
# 如果AI服务调用失败,返回模拟数据作为降级方案
|
||||
log.warning("[八字择吉] 使用模拟数据作为降级方案")
|
||||
|
||||
mock_response = {
|
||||
"lucky_score": 88,
|
||||
"lucky_level": "中吉",
|
||||
"recommended_dates": [
|
||||
{
|
||||
"date": "2024-03-15",
|
||||
"lunar_date": "二月初六",
|
||||
"weekday": "星期五",
|
||||
"score": 92,
|
||||
"level": "大吉",
|
||||
"best_times": ["09:00-11:00", "13:00-15:00"],
|
||||
"reason": "此日天干地支与您八字相合,五行流通顺畅,最宜开业庆典",
|
||||
"yi": ["开市", "签约", "祈福"],
|
||||
"ji": ["动土", "安葬"]
|
||||
}
|
||||
],
|
||||
"wuxing_analysis": {
|
||||
"personal_wuxing": "金旺木弱",
|
||||
"suitable_elements": ["水", "木"],
|
||||
"avoid_elements": ["火", "土"],
|
||||
"balance_suggestion": "宜多接触水木属性事物,避免火土过旺"
|
||||
},
|
||||
"bazi_analysis": "您的八字显示金旺木弱,需要水来调和...",
|
||||
"tiangang_analysis": "天干地支分析显示您适合在春季进行重要决策...",
|
||||
"zeji_suggestions": "根据您的八字特点,建议选择水旺木盛的日子...",
|
||||
"avoid_suggestions": "需要避开火土过旺的时间...",
|
||||
"best_time_periods": [
|
||||
{
|
||||
"time": "09:00-11:00",
|
||||
"name": "巳时",
|
||||
"description": "此时辰阳气上升,最利开创事业"
|
||||
}
|
||||
],
|
||||
"fengshui_tips": "建议在东南方位进行重要活动...",
|
||||
"lucky_directions": ["东南", "正东"],
|
||||
"lucky_colors": ["绿色", "蓝色", "黑色"],
|
||||
"lucky_numbers": [3, 8, 13, 18],
|
||||
"unlocked_content": {
|
||||
"detailed_bazi": "详细八字解析...",
|
||||
"yearly_fortune": "全年运势分析...",
|
||||
"monthly_guide": "逐月择吉指南...",
|
||||
"master_advice": "大师特别建议..."
|
||||
}
|
||||
}
|
||||
|
||||
return json.dumps(mock_response, ensure_ascii=False)
|
||||
|
||||
@classmethod
|
||||
def _parse_ai_response(cls, ai_response: str, request_data: BaziZejiCalculateRequestSchema, zeji_type_label: str) -> Dict[str, Any]:
|
||||
"""解析AI响应数据"""
|
||||
try:
|
||||
# 添加详细的调试信息
|
||||
log.info(f"[DEBUG] ai_response type: {type(ai_response)}")
|
||||
log.info(f"[DEBUG] ai_response length: {len(ai_response) if ai_response else 'None'}")
|
||||
log.info(f"[DEBUG] ai_response repr: {repr(ai_response[:200] if ai_response else 'None')}")
|
||||
|
||||
if not ai_response:
|
||||
raise ValueError("AI响应为空")
|
||||
|
||||
if not isinstance(ai_response, str):
|
||||
log.warning(f"AI响应不是字符串类型,尝试转换: {type(ai_response)}")
|
||||
ai_response = str(ai_response)
|
||||
|
||||
# 去除可能的前后空白字符和特殊字符
|
||||
ai_response = ai_response.strip()
|
||||
|
||||
# 尝试找到JSON开始和结束位置
|
||||
start_idx = ai_response.find('{')
|
||||
end_idx = ai_response.rfind('}')
|
||||
|
||||
if start_idx != -1 and end_idx != -1 and end_idx > start_idx:
|
||||
json_str = ai_response[start_idx:end_idx+1]
|
||||
log.info(f"[DEBUG] 提取的JSON字符串: {json_str[:200]}...")
|
||||
ai_data = json.loads(json_str)
|
||||
else:
|
||||
log.error(f"[DEBUG] 无法找到有效的JSON结构")
|
||||
raise ValueError("AI响应中没有找到有效的JSON结构")
|
||||
log.info(f"解析AI响应数据 ai_data is {ai_data}")
|
||||
|
||||
# 构建完整的响应数据
|
||||
result_data = {
|
||||
"lucky_score": ai_data.get("lucky_score", 0),
|
||||
"lucky_level": ai_data.get("lucky_level", "平"),
|
||||
"recommended_dates": ai_data.get("recommended_dates", []),
|
||||
"wuxing_analysis": ai_data.get("wuxing_analysis", {}),
|
||||
"bazi_analysis": ai_data.get("bazi_analysis", ""),
|
||||
"tiangang_analysis": ai_data.get("tiangang_analysis", ""),
|
||||
"zeji_suggestions": ai_data.get("zeji_suggestions", ""),
|
||||
"avoid_suggestions": ai_data.get("avoid_suggestions", ""),
|
||||
"best_time_periods": ai_data.get("best_time_periods", []),
|
||||
"fengshui_tips": ai_data.get("fengshui_tips", ""),
|
||||
"lucky_directions": ai_data.get("lucky_directions", []),
|
||||
"lucky_colors": ai_data.get("lucky_colors", []),
|
||||
"lucky_numbers": ai_data.get("lucky_numbers", []),
|
||||
"unlocked_content": ai_data.get("unlocked_content", {})
|
||||
}
|
||||
|
||||
return result_data
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
log.error(f"AI响应JSON解析失败: {str(e)}")
|
||||
raise Exception(f"AI响应格式错误: {str(e)}")
|
||||
|
||||
@classmethod
|
||||
async def _update_zeji_status(cls, zeji_id: int, task_status: int, error_message: str = None):
|
||||
"""更新八字择吉测算状态"""
|
||||
from app.core.database import async_db_session
|
||||
|
||||
async with async_db_session() as session:
|
||||
try:
|
||||
# 创建一个临时的auth对象用于更新操作
|
||||
temp_auth = AuthSchema(user=None, db=session)
|
||||
crud = YifanBaziZejiCRUD(temp_auth)
|
||||
crud.session = session
|
||||
|
||||
update_data = YifanBaziZejiUpdateSchema(
|
||||
task_status=task_status,
|
||||
error_message=error_message
|
||||
)
|
||||
|
||||
await crud.update_yifan_bazi_zeji_crud(zeji_id, update_data)
|
||||
await session.commit() # 提交事务
|
||||
log.info(f"[八字择吉] 状态更新成功,ID: {zeji_id}, 状态: {task_status}")
|
||||
except Exception as e:
|
||||
await session.rollback() # 回滚事务
|
||||
log.error(f"[八字择吉] 状态更新失败,ID: {zeji_id}, 错误: {str(e)}")
|
||||
raise
|
||||
|
||||
@classmethod
|
||||
async def _update_zeji_record(cls, zeji_id: int, update_data: YifanBaziZejiUpdateSchema):
|
||||
"""更新八字择吉测算记录"""
|
||||
from app.core.database import async_db_session
|
||||
|
||||
async with async_db_session() as session:
|
||||
try:
|
||||
# 创建一个临时的auth对象用于更新操作
|
||||
temp_auth = AuthSchema(user=None, db=session)
|
||||
crud = YifanBaziZejiCRUD(temp_auth)
|
||||
crud.session = session
|
||||
|
||||
await crud.update_yifan_bazi_zeji_crud(zeji_id, update_data)
|
||||
await session.commit() # 提交事务
|
||||
log.info(f"[八字择吉] 记录更新成功,ID: {zeji_id}")
|
||||
except Exception as e:
|
||||
await session.rollback() # 回滚事务
|
||||
log.error(f"[八字择吉] 记录更新失败,ID: {zeji_id}, 错误: {str(e)}")
|
||||
raise
|
||||
|
||||
@classmethod
|
||||
async def get_bazi_zeji_result_service(cls, auth: AuthSchema, zeji_id: int) -> BaziZejiCalculateResponseSchema:
|
||||
"""获取八字择吉测算结果"""
|
||||
zeji_obj = await YifanBaziZejiCRUD(auth).get_yifan_bazi_zeji_crud(zeji_id)
|
||||
|
||||
if not zeji_obj:
|
||||
raise CustomException(msg="八字择吉测算记录不存在")
|
||||
|
||||
if zeji_obj.task_status != 5:
|
||||
raise CustomException(msg="八字择吉测算尚未完成")
|
||||
|
||||
# 构建响应数据
|
||||
response_data = {
|
||||
"zeji_id": zeji_obj.id,
|
||||
"name": zeji_obj.name,
|
||||
"zeji_type": zeji_obj.zeji_type,
|
||||
"zeji_type_label": cls._get_zeji_type_label(zeji_obj.zeji_type),
|
||||
"lucky_score": zeji_obj.lucky_score or 0,
|
||||
"lucky_level": zeji_obj.lucky_level or "平",
|
||||
"recommended_dates": json.loads(zeji_obj.recommended_dates) if zeji_obj.recommended_dates else [],
|
||||
"wuxing_analysis": json.loads(zeji_obj.wuxing_analysis) if zeji_obj.wuxing_analysis else {},
|
||||
"bazi_analysis": zeji_obj.bazi_analysis or "",
|
||||
"tiangang_analysis": zeji_obj.tiangang_analysis or "",
|
||||
"zeji_suggestions": zeji_obj.zeji_suggestions or "",
|
||||
"avoid_suggestions": zeji_obj.avoid_suggestions or "",
|
||||
"best_time_periods": json.loads(zeji_obj.best_time_periods) if zeji_obj.best_time_periods else [],
|
||||
"fengshui_tips": zeji_obj.fengshui_tips or "",
|
||||
"lucky_directions": json.loads(zeji_obj.lucky_directions) if zeji_obj.lucky_directions else [],
|
||||
"lucky_colors": json.loads(zeji_obj.lucky_colors) if zeji_obj.lucky_colors else [],
|
||||
"lucky_numbers": json.loads(zeji_obj.lucky_numbers) if zeji_obj.lucky_numbers else [],
|
||||
"unlocked": json.loads(zeji_obj.unlocked_content) if zeji_obj.unlocked_content and zeji_obj.is_unlocked else None,
|
||||
"is_unlocked": bool(zeji_obj.is_unlocked),
|
||||
"unlock_price": float(zeji_obj.unlock_price or 19.90)
|
||||
}
|
||||
|
||||
return BaziZejiCalculateResponseSchema(**response_data)
|
||||
|
||||
@classmethod
|
||||
def _get_zeji_type_label(cls, zeji_type: str) -> str:
|
||||
"""获取择吉类型标签"""
|
||||
labels = {
|
||||
"wedding": "婚嫁择吉",
|
||||
"business": "开业择吉",
|
||||
"move": "搬家择吉",
|
||||
"travel": "出行择吉",
|
||||
"investment": "投资择吉",
|
||||
"surgery": "手术择吉",
|
||||
"contract": "签约择吉",
|
||||
"other": "其他择吉"
|
||||
}
|
||||
return labels.get(zeji_type, zeji_type)
|
||||
|
||||
@classmethod
|
||||
async def create_yifan_bazi_zeji_service(cls, auth: AuthSchema, data: YifanBaziZejiCreateSchema) -> dict:
|
||||
"""创建八字择吉测算记录"""
|
||||
result_dict = await YifanBaziZejiCRUD(auth).create_yifan_bazi_zeji_crud(data=data)
|
||||
return YifanBaziZejiOutSchema.model_validate(result_dict).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def get_yifan_bazi_zeji_service(cls, auth: AuthSchema, zeji_id: int) -> dict:
|
||||
"""获取单个八字择吉测算记录"""
|
||||
result_dict = await YifanBaziZejiCRUD(auth).get_yifan_bazi_zeji_crud(zeji_id=zeji_id)
|
||||
if not result_dict:
|
||||
raise CustomException(msg="八字择吉测算记录不存在")
|
||||
return YifanBaziZejiOutSchema.model_validate(result_dict).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def update_yifan_bazi_zeji_service(cls, auth: AuthSchema, zeji_id: int, data: YifanBaziZejiUpdateSchema) -> dict:
|
||||
"""更新八字择吉测算记录"""
|
||||
result_dict = await YifanBaziZejiCRUD(auth).update_yifan_bazi_zeji_crud(zeji_id=zeji_id, data=data)
|
||||
return YifanBaziZejiOutSchema.model_validate(result_dict).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def delete_yifan_bazi_zeji_service(cls, auth: AuthSchema, zeji_id: int) -> bool:
|
||||
"""删除八字择吉测算记录"""
|
||||
return await YifanBaziZejiCRUD(auth).delete_yifan_bazi_zeji_crud(zeji_id=zeji_id)
|
||||
|
||||
@classmethod
|
||||
async def list_yifan_bazi_zeji_service(cls, auth: AuthSchema, search: YifanBaziZejiQueryParam | None = None, order_by: list[dict] | None = None) -> list[dict]:
|
||||
"""列表查询"""
|
||||
search_dict = search.__dict__ if search else None
|
||||
obj_list = await YifanBaziZejiCRUD(auth).list_yifan_bazi_zeji_crud(search=search_dict, order_by=order_by)
|
||||
return [YifanBaziZejiOutSchema.model_validate(obj).model_dump() for obj in obj_list]
|
||||
|
||||
@classmethod
|
||||
async def page_yifan_bazi_zeji_service(cls, auth: AuthSchema, page_no: int, page_size: int, search: YifanBaziZejiQueryParam | None = None, order_by: list[dict] | None = None) -> dict:
|
||||
"""分页查询"""
|
||||
search_dict = search.__dict__ if search else None
|
||||
result_dict = await YifanBaziZejiCRUD(auth).page_yifan_bazi_zeji_crud(page_no=page_no, page_size=page_size, search=search_dict, order_by=order_by)
|
||||
|
||||
# 基础CRUD返回的是items键,我们需要转换为data键以保持API一致性
|
||||
if "items" in result_dict:
|
||||
result_dict["data"] = result_dict.pop("items")
|
||||
|
||||
return result_dict
|
||||
@@ -0,0 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from .controller import YifanCaiyunJiexiRouter
|
||||
|
||||
__all__ = ["YifanCaiyunJiexiRouter"]
|
||||
@@ -0,0 +1,135 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from app.core.dependencies import AuthPermission
|
||||
from app.common.response import SuccessResponse
|
||||
from app.core.base_params import PaginationQueryParam
|
||||
from app.core.logger import log
|
||||
from app.api.v1.module_yifan.yifan_caiyun_jiexi.service import YifanCaiyunJiexiService
|
||||
from app.api.v1.module_yifan.yifan_caiyun_jiexi.schema import (
|
||||
YifanCaiyunJiexiCreateSchema, YifanCaiyunJiexiUpdateSchema, YifanCaiyunJiexiQueryParam,
|
||||
CaiyunJiexiAnalyzeRequestSchema
|
||||
)
|
||||
|
||||
YifanCaiyunJiexiRouter = APIRouter(prefix="/yifan_caiyun_jiexi", tags=["财运解析"])
|
||||
|
||||
|
||||
@YifanCaiyunJiexiRouter.post("/analyze", summary="财运解析", description="根据方案ID生成财运解析,创建任务并异步生成分析结果")
|
||||
async def analyze_caiyun_controller(
|
||||
data: CaiyunJiexiAnalyzeRequestSchema,
|
||||
auth: AuthSchema = Depends(AuthPermission([]))
|
||||
) -> JSONResponse:
|
||||
"""财运解析接口 - 创建任务,后台异步生成分析结果"""
|
||||
result_dict = await YifanCaiyunJiexiService.analyze_caiyun_service(auth=auth, data=data)
|
||||
log.info(f"财运解析任务已创建: 方案ID={data.report_id}")
|
||||
return SuccessResponse(data=result_dict, msg="财运解析任务已创建,正在分析中")
|
||||
|
||||
|
||||
@YifanCaiyunJiexiRouter.get("/result/{jiexi_id}", summary="获取财运解析结果", description="根据ID获取财运解析结果")
|
||||
async def get_caiyun_jiexi_result_controller(
|
||||
jiexi_id: int,
|
||||
auth: AuthSchema = Depends(AuthPermission([]))
|
||||
) -> JSONResponse:
|
||||
"""获取财运解析结果接口"""
|
||||
result_dict = await YifanCaiyunJiexiService.get_caiyun_jiexi_result_service(auth=auth, jiexi_id=jiexi_id)
|
||||
log.info(f"获取财运解析结果成功: ID={jiexi_id}")
|
||||
return SuccessResponse(data=result_dict.model_dump(), msg="获取财运解析结果成功")
|
||||
|
||||
|
||||
@YifanCaiyunJiexiRouter.get("/result/by-report/{report_id}", summary="根据报告ID获取财运解析结果", description="根据报告ID获取财运解析结果")
|
||||
async def get_caiyun_jiexi_result_by_report_controller(
|
||||
report_id: int,
|
||||
auth: AuthSchema = Depends(AuthPermission([]))
|
||||
) -> JSONResponse:
|
||||
"""根据报告ID获取财运解析结果接口"""
|
||||
result_dict = await YifanCaiyunJiexiService.get_caiyun_jiexi_result_by_report_service(auth=auth, report_id=report_id)
|
||||
log.info(f"根据报告ID获取财运解析结果成功: 报告ID={report_id}")
|
||||
return SuccessResponse(data=result_dict.model_dump(), msg="获取财运解析结果成功")
|
||||
|
||||
|
||||
@YifanCaiyunJiexiRouter.get("/status/{jiexi_id}", summary="获取财运解析状态", description="根据ID获取财运解析任务状态")
|
||||
async def get_caiyun_jiexi_status_controller(
|
||||
jiexi_id: int,
|
||||
auth: AuthSchema = Depends(AuthPermission([]))
|
||||
) -> JSONResponse:
|
||||
"""获取财运解析状态接口"""
|
||||
result_dict = await YifanCaiyunJiexiService.get_caiyun_jiexi_status_service(auth=auth, jiexi_id=jiexi_id)
|
||||
log.info(f"获取财运解析状态成功: ID={jiexi_id}")
|
||||
return SuccessResponse(data=result_dict.model_dump(), msg="获取财运解析状态成功")
|
||||
|
||||
|
||||
@YifanCaiyunJiexiRouter.get("/status/by-report/{report_id}", summary="根据报告ID获取财运解析状态", description="根据报告ID获取财运解析任务状态")
|
||||
async def get_caiyun_jiexi_status_by_report_controller(
|
||||
report_id: int,
|
||||
auth: AuthSchema = Depends(AuthPermission([]))
|
||||
) -> JSONResponse:
|
||||
"""根据报告ID获取财运解析状态接口"""
|
||||
result_dict = await YifanCaiyunJiexiService.get_caiyun_jiexi_status_by_report_service(auth=auth, report_id=report_id)
|
||||
log.info(f"根据报告ID获取财运解析状态成功: 报告ID={report_id}")
|
||||
return SuccessResponse(data=result_dict.model_dump(), msg="获取财运解析状态成功")
|
||||
|
||||
|
||||
@YifanCaiyunJiexiRouter.get("/list", summary="查询财运解析列表", description="查询财运解析列表")
|
||||
async def get_yifan_caiyun_jiexi_list_controller(
|
||||
page: PaginationQueryParam = Depends(),
|
||||
search: YifanCaiyunJiexiQueryParam = Depends(),
|
||||
auth: AuthSchema = Depends(AuthPermission([]))
|
||||
) -> JSONResponse:
|
||||
"""查询财运解析列表接口(数据库分页)"""
|
||||
result_dict = await YifanCaiyunJiexiService.page_yifan_caiyun_jiexi_service(
|
||||
auth=auth,
|
||||
page_no=page.page_no if page.page_no is not None else 1,
|
||||
page_size=page.page_size if page.page_size is not None else 10,
|
||||
search=search,
|
||||
order_by=page.order_by
|
||||
)
|
||||
log.info("查询财运解析列表成功")
|
||||
return SuccessResponse(data=result_dict, msg="查询财运解析列表成功")
|
||||
|
||||
|
||||
@YifanCaiyunJiexiRouter.get("/{jiexi_id}", summary="获取财运解析详情", description="根据ID获取财运解析详情")
|
||||
async def get_yifan_caiyun_jiexi_controller(
|
||||
jiexi_id: int,
|
||||
auth: AuthSchema = Depends(AuthPermission([]))
|
||||
) -> JSONResponse:
|
||||
"""获取财运解析详情接口"""
|
||||
result_dict = await YifanCaiyunJiexiService.get_yifan_caiyun_jiexi_service(auth=auth, jiexi_id=jiexi_id)
|
||||
log.info("获取财运解析详情成功")
|
||||
return SuccessResponse(data=result_dict, msg="获取财运解析详情成功")
|
||||
|
||||
|
||||
@YifanCaiyunJiexiRouter.post("/create", summary="创建财运解析", description="创建财运解析记录")
|
||||
async def create_yifan_caiyun_jiexi_controller(
|
||||
data: YifanCaiyunJiexiCreateSchema,
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_caiyun_jiexi:create"]))
|
||||
) -> JSONResponse:
|
||||
"""创建财运解析接口"""
|
||||
result_dict = await YifanCaiyunJiexiService.create_yifan_caiyun_jiexi_service(auth=auth, data=data)
|
||||
log.info("创建财运解析成功")
|
||||
return SuccessResponse(data=result_dict, msg="创建财运解析成功")
|
||||
|
||||
|
||||
@YifanCaiyunJiexiRouter.put("/{jiexi_id}", summary="更新财运解析", description="根据ID更新财运解析记录")
|
||||
async def update_yifan_caiyun_jiexi_controller(
|
||||
jiexi_id: int,
|
||||
data: YifanCaiyunJiexiUpdateSchema,
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_caiyun_jiexi:update"]))
|
||||
) -> JSONResponse:
|
||||
"""更新财运解析接口"""
|
||||
result_dict = await YifanCaiyunJiexiService.update_yifan_caiyun_jiexi_service(auth=auth, jiexi_id=jiexi_id, data=data)
|
||||
log.info("更新财运解析成功")
|
||||
return SuccessResponse(data=result_dict, msg="更新财运解析成功")
|
||||
|
||||
|
||||
@YifanCaiyunJiexiRouter.delete("/{jiexi_id}", summary="删除财运解析", description="根据ID删除财运解析记录")
|
||||
async def delete_yifan_caiyun_jiexi_controller(
|
||||
jiexi_id: int,
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_caiyun_jiexi:delete"]))
|
||||
) -> JSONResponse:
|
||||
"""删除财运解析接口"""
|
||||
result = await YifanCaiyunJiexiService.delete_yifan_caiyun_jiexi_service(auth=auth, jiexi_id=jiexi_id)
|
||||
log.info("删除财运解析成功")
|
||||
return SuccessResponse(data=result, msg="删除财运解析成功")
|
||||
@@ -0,0 +1,161 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from typing import List, Dict, Any, Optional
|
||||
from sqlalchemy import select, and_, or_, desc, asc
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from app.core.base_crud import CRUDBase
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from app.api.v1.module_yifan.yifan_caiyun_jiexi.model import YifanCaiyunJiexiModel
|
||||
from app.api.v1.module_yifan.yifan_caiyun_jiexi.schema import (
|
||||
YifanCaiyunJiexiCreateSchema, YifanCaiyunJiexiUpdateSchema, YifanCaiyunJiexiQueryParam
|
||||
)
|
||||
|
||||
|
||||
class YifanCaiyunJiexiCRUD(CRUDBase[YifanCaiyunJiexiModel, YifanCaiyunJiexiCreateSchema, YifanCaiyunJiexiUpdateSchema]):
|
||||
"""财运解析CRUD操作"""
|
||||
|
||||
def __init__(self, auth: AuthSchema):
|
||||
super().__init__(model=YifanCaiyunJiexiModel, auth=auth)
|
||||
|
||||
async def create_yifan_caiyun_jiexi_crud(self, data: YifanCaiyunJiexiCreateSchema) -> YifanCaiyunJiexiModel:
|
||||
"""
|
||||
创建财运解析记录
|
||||
|
||||
参数:
|
||||
- data (YifanCaiyunJiexiCreateSchema): 创建数据
|
||||
|
||||
返回:
|
||||
- YifanCaiyunJiexiModel: 模型实例
|
||||
"""
|
||||
return await self.create(data=data)
|
||||
|
||||
async def get_yifan_caiyun_jiexi_crud(self, jiexi_id: int) -> YifanCaiyunJiexiModel:
|
||||
"""
|
||||
获取财运解析记录
|
||||
|
||||
参数:
|
||||
- jiexi_id (int): 财运解析ID
|
||||
|
||||
返回:
|
||||
- YifanCaiyunJiexiModel: 模型实例
|
||||
"""
|
||||
return await self.get(id=jiexi_id)
|
||||
|
||||
async def get_by_report_id_crud(self, report_id: int) -> Optional[YifanCaiyunJiexiModel]:
|
||||
"""
|
||||
根据方案ID获取财运解析记录
|
||||
|
||||
参数:
|
||||
- report_id (int): 方案ID
|
||||
|
||||
返回:
|
||||
- YifanCaiyunJiexiModel: 模型实例或None
|
||||
"""
|
||||
stmt = select(self.model).where(
|
||||
and_(
|
||||
self.model.report_id == report_id,
|
||||
self.model.is_deleted == 0
|
||||
)
|
||||
)
|
||||
result = await self.auth.db.execute(stmt)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
async def update_yifan_caiyun_jiexi_crud(self, jiexi_id: int, data: YifanCaiyunJiexiUpdateSchema) -> YifanCaiyunJiexiModel:
|
||||
"""
|
||||
更新财运解析记录
|
||||
|
||||
参数:
|
||||
- jiexi_id (int): 财运解析ID
|
||||
- data (YifanCaiyunJiexiUpdateSchema): 更新数据
|
||||
|
||||
返回:
|
||||
- YifanCaiyunJiexiModel: 更新后的模型实例
|
||||
"""
|
||||
return await self.update(id=jiexi_id, data=data)
|
||||
|
||||
async def delete_yifan_caiyun_jiexi_crud(self, jiexi_id: int) -> bool:
|
||||
"""
|
||||
删除财运解析记录(软删除)
|
||||
|
||||
参数:
|
||||
- jiexi_id (int): 财运解析ID
|
||||
|
||||
返回:
|
||||
- bool: 删除是否成功
|
||||
"""
|
||||
await self.delete(ids=[jiexi_id])
|
||||
return True
|
||||
|
||||
async def list_yifan_caiyun_jiexi_crud(self, search: YifanCaiyunJiexiQueryParam) -> List[YifanCaiyunJiexiModel]:
|
||||
"""
|
||||
查询财运解析列表
|
||||
|
||||
参数:
|
||||
- search (YifanCaiyunJiexiQueryParam): 查询参数
|
||||
|
||||
返回:
|
||||
- List[YifanCaiyunJiexiModel]: 模型实例列表
|
||||
"""
|
||||
return await self.list(search=search)
|
||||
|
||||
async def page_yifan_caiyun_jiexi_crud(
|
||||
self,
|
||||
page_no: int,
|
||||
page_size: int,
|
||||
search: YifanCaiyunJiexiQueryParam,
|
||||
order_by: str = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
分页查询财运解析列表
|
||||
|
||||
参数:
|
||||
- page_no (int): 页码
|
||||
- page_size (int): 每页数量
|
||||
- search (YifanCaiyunJiexiQueryParam): 查询参数
|
||||
- order_by (str): 排序字段
|
||||
|
||||
返回:
|
||||
- Dict[str, Any]: 分页结果
|
||||
"""
|
||||
return await self.page(
|
||||
page_no=page_no,
|
||||
page_size=page_size,
|
||||
search=search,
|
||||
order_by=order_by
|
||||
)
|
||||
|
||||
def _build_search_conditions(self, search: YifanCaiyunJiexiQueryParam) -> List:
|
||||
"""
|
||||
构建搜索条件
|
||||
|
||||
参数:
|
||||
- search (YifanCaiyunJiexiQueryParam): 查询参数
|
||||
|
||||
返回:
|
||||
- List: 查询条件列表
|
||||
"""
|
||||
conditions = [self.model.is_deleted == 0]
|
||||
|
||||
if search.user_id is not None:
|
||||
conditions.append(self.model.user_id == search.user_id)
|
||||
|
||||
if search.report_id is not None:
|
||||
conditions.append(self.model.report_id == search.report_id)
|
||||
|
||||
if search.report_type:
|
||||
conditions.append(self.model.report_type == search.report_type)
|
||||
|
||||
if search.task_status is not None:
|
||||
conditions.append(self.model.task_status == search.task_status)
|
||||
|
||||
if search.wealth_level:
|
||||
conditions.append(self.model.wealth_level == search.wealth_level)
|
||||
|
||||
if search.is_unlocked is not None:
|
||||
conditions.append(self.model.is_unlocked == search.is_unlocked)
|
||||
|
||||
if search.name:
|
||||
conditions.append(self.model.name.like(f'%{search.name}%'))
|
||||
|
||||
return conditions
|
||||
@@ -0,0 +1,63 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import datetime
|
||||
from sqlalchemy import String, Integer, SmallInteger, DateTime, Text, JSON, Numeric
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.base_model import ModelMixin, UserMixin
|
||||
|
||||
|
||||
class YifanCaiyunJiexiModel(ModelMixin, UserMixin):
|
||||
"""
|
||||
财运解析表
|
||||
"""
|
||||
__tablename__: str = 'yifan_caiyun_jiexi'
|
||||
__table_args__: dict[str, str] = {'comment': '财运解析'}
|
||||
__loader_options__: list[str] = ["created_by", "updated_by"]
|
||||
|
||||
is_deleted: Mapped[int | None] = mapped_column(SmallInteger, nullable=True, comment='是否删除(0否 1是)')
|
||||
user_id: Mapped[int | None] = mapped_column(Integer, nullable=True, comment='用户ID')
|
||||
report_id: Mapped[int] = mapped_column(Integer, nullable=False, comment='关联的方案ID(yifan_naming_reports表ID)')
|
||||
report_type: Mapped[str | None] = mapped_column(String(50), nullable=True, comment='方案类型(personal:个人起名 company:公司起名等)')
|
||||
|
||||
# 基础信息(从方案中获取)
|
||||
name: Mapped[str | None] = mapped_column(String(100), nullable=True, comment='姓名/公司名')
|
||||
gender: Mapped[str | None] = mapped_column(String(10), nullable=True, comment='性别(male男 female女,公司为空)')
|
||||
birth_date: Mapped[str | None] = mapped_column(String(100), nullable=True, comment='生辰八字显示格式')
|
||||
birth_date_api: Mapped[datetime.datetime | None] = mapped_column(DateTime, nullable=True, comment='生辰八字API格式')
|
||||
birth_place: Mapped[str | None] = mapped_column(String(100), nullable=True, comment='出生地')
|
||||
|
||||
# 财运解析结果
|
||||
wealth_score: Mapped[int | None] = mapped_column(Integer, nullable=True, comment='财运总分(0-100)')
|
||||
wealth_level: Mapped[str | None] = mapped_column(String(32), nullable=True, comment='财运等级(大富 中富 小富 平财 欠财)')
|
||||
wealth_trend: Mapped[str | None] = mapped_column(String(32), nullable=True, comment='财运趋势(上升 稳定 下降 波动)')
|
||||
|
||||
# 命盘精批(JSON格式存储)
|
||||
mingpan_jingpi: Mapped[str | None] = mapped_column(JSON, nullable=True, comment='命盘精批详细内容JSON')
|
||||
|
||||
# 流年总运(JSON格式存储)
|
||||
liunian_zongyun: Mapped[str | None] = mapped_column(JSON, nullable=True, comment='流年总运分析JSON')
|
||||
|
||||
# 风水锦囊(JSON格式存储)
|
||||
fengshui_jinnang: Mapped[str | None] = mapped_column(JSON, nullable=True, comment='风水锦囊指导JSON')
|
||||
|
||||
# 月度祥批(JSON格式存储)
|
||||
yuedo_xiangpi: Mapped[str | None] = mapped_column(JSON, nullable=True, comment='月度祥批JSON(包含全年月份、干支、核心运势判词、吉凶信息和重点月令深度解析)')
|
||||
|
||||
# 每日运程(JSON格式存储)
|
||||
meiri_yuncheng: Mapped[str | None] = mapped_column(JSON, nullable=True, comment='每日运程JSON(包含今日运势、财神方位、喜神方位、时辰吉凶信息)')
|
||||
|
||||
# 解锁相关
|
||||
is_unlocked: Mapped[int | None] = mapped_column(SmallInteger, nullable=True, comment='是否已解锁深度解析(0否 1是)')
|
||||
unlock_price: Mapped[str | None] = mapped_column(Numeric(10, 2), nullable=True, comment='解锁价格')
|
||||
unlocked_content: Mapped[str | None] = mapped_column(JSON, nullable=True, comment='解锁后的深度内容JSON')
|
||||
|
||||
# 任务状态
|
||||
task_status: Mapped[int | None] = mapped_column(SmallInteger, nullable=True, comment='任务状态(1已创建 2解析中 5解析成功 3解析超时 0任务失败)')
|
||||
error_message: Mapped[str | None] = mapped_column(Text, nullable=True, comment='错误信息')
|
||||
|
||||
# AI相关
|
||||
ai_source_data: Mapped[str | None] = mapped_column(Text, nullable=True, comment='AI原始响应数据')
|
||||
ai_model_version: Mapped[str | None] = mapped_column(String(50), nullable=True, comment='AI模型版本')
|
||||
|
||||
remark: Mapped[str | None] = mapped_column(String(255), nullable=True, comment='备注')
|
||||
@@ -0,0 +1,147 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Optional, List, Dict, Any
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from fastapi import Query
|
||||
|
||||
|
||||
# ==================== 嵌套模型 ====================
|
||||
# 注意:原有的复杂嵌套Schema已简化为字符串类型,以避免Pydantic验证错误
|
||||
# 如需要复杂结构,可在前端解析JSON字符串
|
||||
|
||||
|
||||
# ==================== 请求模型 ====================
|
||||
|
||||
class CaiyunJiexiAnalyzeRequestSchema(BaseModel):
|
||||
"""财运解析分析请求模型"""
|
||||
report_id: int = Field(..., description='方案ID', example=12345)
|
||||
|
||||
|
||||
# ==================== 响应模型 ====================
|
||||
|
||||
class CaiyunJiexiAnalyzeResponseSchema(BaseModel):
|
||||
"""财运解析分析响应模型"""
|
||||
jiexi_id: int = Field(..., description='财运解析ID')
|
||||
report_id: int = Field(..., description='关联方案ID')
|
||||
name: str = Field(..., description='姓名/公司名')
|
||||
wealth_score: int = Field(..., description='财运总分', ge=0, le=100)
|
||||
wealth_level: str = Field(..., description='财运等级')
|
||||
wealth_trend: str = Field(..., description='财运趋势')
|
||||
|
||||
mingpan_jingpi: str = Field(..., description='命盘精批JSON字符串')
|
||||
liunian_zongyun: str = Field(..., description='流年总运JSON字符串')
|
||||
fengshui_jinnang: str = Field(..., description='风水锦囊JSON字符串')
|
||||
yuedo_xiangpi: str = Field(..., description='月度祥批JSON字符串(包含全年月份、干支、核心运势判词、吉凶信息和重点月令深度解析)')
|
||||
meiri_yuncheng: str = Field(..., description='每日运程JSON字符串(包含今日运势、财神方位、喜神方位、时辰吉凶信息)')
|
||||
|
||||
unlocked: Optional[str] = Field(None, description='解锁后的深度内容JSON字符串')
|
||||
is_unlocked: bool = Field(..., description='是否已解锁深度解析')
|
||||
unlock_price: float = Field(..., description='解锁价格(元)')
|
||||
|
||||
|
||||
# ==================== CRUD模型 ====================
|
||||
|
||||
class YifanCaiyunJiexiCreateSchema(BaseModel):
|
||||
"""财运解析创建模型"""
|
||||
user_id: Optional[int] = Field(None, description='用户ID')
|
||||
report_id: int = Field(..., description='关联的方案ID')
|
||||
report_type: Optional[str] = Field(None, description='方案类型')
|
||||
|
||||
# 基础信息
|
||||
name: Optional[str] = Field(None, description='姓名/公司名')
|
||||
gender: Optional[str] = Field(None, description='性别')
|
||||
birth_date: Optional[str] = Field(None, description='生辰八字显示格式')
|
||||
birth_date_api: Optional[datetime] = Field(None, description='生辰八字API格式')
|
||||
birth_place: Optional[str] = Field(None, description='出生地')
|
||||
|
||||
# 财运解析结果
|
||||
wealth_score: Optional[int] = Field(None, description='财运总分')
|
||||
wealth_level: Optional[str] = Field(None, description='财运等级')
|
||||
wealth_trend: Optional[str] = Field(None, description='财运趋势')
|
||||
|
||||
# 五大模块JSON字段
|
||||
mingpan_jingpi: Optional[str] = Field(None, description='命盘精批JSON')
|
||||
liunian_zongyun: Optional[str] = Field(None, description='流年总运JSON')
|
||||
fengshui_jinnang: Optional[str] = Field(None, description='风水锦囊JSON')
|
||||
yuedo_xiangpi: Optional[str] = Field(None, description='月度祥批JSON')
|
||||
meiri_yuncheng: Optional[str] = Field(None, description='每日运程JSON')
|
||||
|
||||
# 解锁相关
|
||||
is_unlocked: Optional[int] = Field(0, description='是否已解锁深度解析')
|
||||
unlock_price: Optional[float] = Field(19.90, description='解锁价格')
|
||||
unlocked_content: Optional[str] = Field(None, description='解锁后的深度内容JSON')
|
||||
|
||||
# 任务状态
|
||||
task_status: Optional[int] = Field(1, description='任务状态')
|
||||
error_message: Optional[str] = Field(None, description='错误信息')
|
||||
|
||||
# AI相关
|
||||
ai_source_data: Optional[str] = Field(None, description='AI原始响应数据')
|
||||
ai_model_version: Optional[str] = Field(None, description='AI模型版本')
|
||||
|
||||
is_deleted: Optional[int] = Field(0, description='是否删除')
|
||||
remark: Optional[str] = Field(None, description='备注')
|
||||
|
||||
|
||||
class YifanCaiyunJiexiUpdateSchema(BaseModel):
|
||||
"""财运解析更新模型"""
|
||||
# 财运解析结果
|
||||
wealth_score: Optional[int] = Field(None, description='财运总分')
|
||||
wealth_level: Optional[str] = Field(None, description='财运等级')
|
||||
wealth_trend: Optional[str] = Field(None, description='财运趋势')
|
||||
|
||||
# 五大模块JSON字段
|
||||
mingpan_jingpi: Optional[str] = Field(None, description='命盘精批JSON')
|
||||
liunian_zongyun: Optional[str] = Field(None, description='流年总运JSON')
|
||||
fengshui_jinnang: Optional[str] = Field(None, description='风水锦囊JSON')
|
||||
yuedo_xiangpi: Optional[str] = Field(None, description='月度祥批JSON')
|
||||
meiri_yuncheng: Optional[str] = Field(None, description='每日运程JSON')
|
||||
|
||||
# 解锁相关
|
||||
is_unlocked: Optional[int] = Field(None, description='是否已解锁深度解析')
|
||||
unlock_price: Optional[float] = Field(None, description='解锁价格')
|
||||
unlocked_content: Optional[str] = Field(None, description='解锁后的深度内容JSON')
|
||||
|
||||
# 任务状态
|
||||
task_status: Optional[int] = Field(None, description='任务状态')
|
||||
error_message: Optional[str] = Field(None, description='错误信息')
|
||||
|
||||
# AI相关
|
||||
ai_source_data: Optional[str] = Field(None, description='AI原始响应数据')
|
||||
ai_model_version: Optional[str] = Field(None, description='AI模型版本')
|
||||
|
||||
remark: Optional[str] = Field(None, description='备注')
|
||||
|
||||
|
||||
class YifanCaiyunJiexiQueryParam:
|
||||
"""财运解析查询参数"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
user_id: Optional[int] = Query(None, description='用户ID'),
|
||||
report_id: Optional[int] = Query(None, description='方案ID'),
|
||||
report_type: Optional[str] = Query(None, description='方案类型'),
|
||||
task_status: Optional[int] = Query(None, description='任务状态'),
|
||||
wealth_level: Optional[str] = Query(None, description='财运等级'),
|
||||
is_unlocked: Optional[int] = Query(None, description='是否已解锁'),
|
||||
name: Optional[str] = Query(None, description='姓名/公司名(模糊查询)')
|
||||
) -> None:
|
||||
self.user_id = user_id
|
||||
self.report_id = report_id
|
||||
self.report_type = report_type
|
||||
self.task_status = task_status
|
||||
self.wealth_level = wealth_level
|
||||
self.is_unlocked = is_unlocked
|
||||
self.name = name
|
||||
|
||||
|
||||
# ==================== 任务状态响应 ====================
|
||||
|
||||
class CaiyunJiexiTaskStatusSchema(BaseModel):
|
||||
"""财运解析任务状态响应"""
|
||||
jiexi_id: int = Field(..., description='财运解析ID')
|
||||
status: int = Field(..., description='任务状态(1已创建 2解析中 5解析成功 3解析超时 0任务失败)')
|
||||
message: str = Field(..., description='状态描述')
|
||||
progress: Optional[int] = Field(None, description='进度百分比', ge=0, le=100)
|
||||
@@ -0,0 +1,769 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import json
|
||||
import asyncio
|
||||
import time
|
||||
from typing import Dict, Any
|
||||
|
||||
from app.core.logger import log
|
||||
from app.core.exceptions import CustomException
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from app.api.v1.module_yifan.yifan_caiyun_jiexi.crud import YifanCaiyunJiexiCRUD
|
||||
from app.api.v1.module_yifan.yifan_caiyun_jiexi.schema import (
|
||||
YifanCaiyunJiexiCreateSchema, YifanCaiyunJiexiUpdateSchema, YifanCaiyunJiexiQueryParam,
|
||||
CaiyunJiexiAnalyzeRequestSchema, CaiyunJiexiAnalyzeResponseSchema, CaiyunJiexiTaskStatusSchema
|
||||
)
|
||||
from app.api.v1.module_yifan.yifan_naming_reports.crud import YifanNamingReportsCRUD
|
||||
|
||||
|
||||
class YifanCaiyunJiexiService:
|
||||
"""财运解析业务服务"""
|
||||
|
||||
@classmethod
|
||||
async def analyze_caiyun_service(cls, auth: AuthSchema, data: CaiyunJiexiAnalyzeRequestSchema) -> Dict[str, Any]:
|
||||
"""财运解析服务(异步模式)"""
|
||||
log.info(f"[财运解析] 服务开始,用户: {auth.user.id if auth.user else None}, 方案ID: {data.report_id}")
|
||||
user_id = auth.user.id if auth.user else 0
|
||||
|
||||
# 获取方案详情
|
||||
report_crud = YifanNamingReportsCRUD(auth)
|
||||
report_obj = await report_crud.get_by_id_yifan_naming_reports_crud(data.report_id)
|
||||
if not report_obj:
|
||||
raise CustomException(msg="方案记录不存在")
|
||||
|
||||
# 检查是否已经生成过财运解析
|
||||
jiexi_crud = YifanCaiyunJiexiCRUD(auth)
|
||||
existing_jiexi = await jiexi_crud.get_by_report_id_crud(data.report_id)
|
||||
|
||||
if existing_jiexi:
|
||||
if existing_jiexi.task_status == 5: # 已完成
|
||||
return {"jiexi_id": existing_jiexi.id, "status": 5, "message": "财运解析已完成"}
|
||||
elif existing_jiexi.task_status in [1, 2]: # 进行中
|
||||
return {"jiexi_id": existing_jiexi.id, "status": existing_jiexi.task_status, "message": "财运解析进行中"}
|
||||
else: # 失败,重新生成
|
||||
await jiexi_crud.delete_yifan_caiyun_jiexi_crud(existing_jiexi.id)
|
||||
|
||||
# 构建完整姓名
|
||||
full_name = ""
|
||||
if report_obj.surname and report_obj.given_name:
|
||||
full_name = f"{report_obj.surname}{report_obj.given_name}"
|
||||
elif report_obj.surname:
|
||||
full_name = report_obj.surname
|
||||
elif report_obj.given_name:
|
||||
full_name = report_obj.given_name
|
||||
|
||||
# 处理生日字段类型转换
|
||||
birthday = getattr(report_obj, 'birthday', None)
|
||||
birth_date_str = None
|
||||
if birthday:
|
||||
if hasattr(birthday, 'strftime'):
|
||||
birth_date_str = birthday.strftime('%Y年%m月%d日')
|
||||
else:
|
||||
birth_date_str = str(birthday)
|
||||
|
||||
# 创建财运解析记录
|
||||
create_data = YifanCaiyunJiexiCreateSchema(
|
||||
user_id=user_id,
|
||||
report_id=data.report_id,
|
||||
report_type=report_obj.service_type,
|
||||
name=full_name,
|
||||
gender=getattr(report_obj, 'gender', None),
|
||||
birth_date=birth_date_str,
|
||||
birth_date_api=birthday,
|
||||
birth_place=getattr(report_obj, 'address', None),
|
||||
task_status=1,
|
||||
is_deleted=0
|
||||
)
|
||||
|
||||
log.info(f"[DEBUG] 准备创建财运解析记录,数据: {create_data}")
|
||||
try:
|
||||
jiexi_obj = await jiexi_crud.create_yifan_caiyun_jiexi_crud(data=create_data)
|
||||
jiexi_id = jiexi_obj.id
|
||||
|
||||
# 确保事务提交,让后台任务能够找到刚创建的记录
|
||||
if hasattr(auth.db, 'commit'):
|
||||
await auth.db.commit()
|
||||
|
||||
log.info(f"[财运解析] 记录创建成功,ID: {jiexi_id}")
|
||||
except Exception as e:
|
||||
log.error(f"[财运解析] 记录创建失败: {str(e)}")
|
||||
log.error(f"[DEBUG] 异常详情: {type(e).__name__}: {str(e)}")
|
||||
import traceback
|
||||
log.error(f"[DEBUG] 异常堆栈: {traceback.format_exc()}")
|
||||
raise CustomException(msg=f"保存财运解析记录失败: {str(e)}")
|
||||
|
||||
# 启动后台任务执行AI解析
|
||||
asyncio.create_task(
|
||||
cls._process_caiyun_analysis(
|
||||
jiexi_id=jiexi_id,
|
||||
report_obj=report_obj,
|
||||
user_id=user_id,
|
||||
)
|
||||
)
|
||||
|
||||
return {"jiexi_id": jiexi_id, "status": 1, "message": "财运解析任务已创建"}
|
||||
|
||||
@classmethod
|
||||
async def _process_caiyun_analysis(
|
||||
cls,
|
||||
jiexi_id: int,
|
||||
report_obj: Any,
|
||||
user_id: int,
|
||||
) -> None:
|
||||
"""后台任务:执行财运解析AI分析"""
|
||||
import asyncio
|
||||
|
||||
# 等待一小段时间,确保主事务已经提交
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
log_prefix = f"[财运解析-{jiexi_id}]"
|
||||
try:
|
||||
# 更新状态为解析中
|
||||
await cls._update_jiexi_status(jiexi_id, task_status=2)
|
||||
log.info(f"{log_prefix} 开始AI解析")
|
||||
|
||||
# 根据category判断是个人还是公司
|
||||
is_company = report_obj.category in ['company', 'Company']
|
||||
|
||||
if is_company:
|
||||
# 公司财运解析
|
||||
company_name = getattr(report_obj, 'given_name', '未提供')
|
||||
industry = getattr(report_obj, 'industry', '未提供')
|
||||
founder_name = getattr(report_obj, 'father_name', '未提供')
|
||||
founder_birthday = getattr(report_obj, 'father_birth_date', None)
|
||||
|
||||
founder_birth_display = '未提供'
|
||||
if founder_birthday:
|
||||
if hasattr(founder_birthday, 'strftime'):
|
||||
founder_birth_display = founder_birthday.strftime('%Y年%m月%d日')
|
||||
else:
|
||||
founder_birth_display = str(founder_birthday)
|
||||
|
||||
# 构建公司财运解析AI输入文本
|
||||
ai_input_text = f"""企业财运解析请求:
|
||||
公司名称:{company_name}
|
||||
所属行业:{industry}
|
||||
创始人姓名:{founder_name}
|
||||
创始人生辰:{founder_birth_display}
|
||||
公司地址:{getattr(report_obj, 'address', '未提供')}
|
||||
方案类型:{report_obj.service_type}
|
||||
|
||||
请根据以上信息进行全面的企业财运解析,包括企业命盘精批、流年总运和风水锦囊等,返回详细的分析结果。"""
|
||||
else:
|
||||
# 个人财运解析
|
||||
full_name = ""
|
||||
if report_obj.surname and report_obj.given_name:
|
||||
full_name = f"{report_obj.surname}{report_obj.given_name}"
|
||||
elif report_obj.surname:
|
||||
full_name = report_obj.surname
|
||||
elif report_obj.given_name:
|
||||
full_name = report_obj.given_name
|
||||
|
||||
# 处理生日显示格式
|
||||
birthday = getattr(report_obj, 'birthday', None)
|
||||
birth_display = '未提供'
|
||||
if birthday:
|
||||
if hasattr(birthday, 'strftime'):
|
||||
birth_display = birthday.strftime('%Y年%m月%d日')
|
||||
else:
|
||||
birth_display = str(birthday)
|
||||
|
||||
# 构建个人财运解析AI输入文本
|
||||
ai_input_text = f"""个人财运解析请求:
|
||||
姓名:{full_name}
|
||||
性别:{getattr(report_obj, 'gender', '未知')}
|
||||
生辰:{birth_display}
|
||||
出生地:{getattr(report_obj, 'address', '未提供')}
|
||||
方案类型:{report_obj.service_type}
|
||||
|
||||
请根据以上信息进行全面的个人财运解析,包括财运评分、财运等级、投资建议、风水指导等,返回详细的分析结果。"""
|
||||
|
||||
# 调用AI服务进行解析
|
||||
ai_response = await cls._call_ai_service(ai_input_text, is_company=is_company)
|
||||
|
||||
if not ai_response:
|
||||
raise Exception("AI服务返回空结果")
|
||||
|
||||
# 解析AI响应并构建结果数据
|
||||
result_data = cls._parse_ai_response(ai_response, report_obj)
|
||||
|
||||
# 更新数据库记录
|
||||
update_data = YifanCaiyunJiexiUpdateSchema(
|
||||
wealth_score=result_data.get('wealth_score'),
|
||||
wealth_level=result_data.get('wealth_level'),
|
||||
wealth_trend=result_data.get('wealth_trend'),
|
||||
mingpan_jingpi=json.dumps(result_data.get('mingpan_jingpi'), ensure_ascii=False),
|
||||
liunian_zongyun=json.dumps(result_data.get('liunian_zongyun'), ensure_ascii=False),
|
||||
fengshui_jinnang=json.dumps(result_data.get('fengshui_jinnang'), ensure_ascii=False),
|
||||
yuedo_xiangpi=json.dumps(result_data.get('yuedo_xiangpi'), ensure_ascii=False),
|
||||
meiri_yuncheng=json.dumps(result_data.get('meiri_yuncheng'), ensure_ascii=False),
|
||||
unlocked_content=json.dumps(result_data.get('unlocked'), ensure_ascii=False),
|
||||
ai_source_data=ai_response,
|
||||
task_status=5 # 解析成功
|
||||
)
|
||||
|
||||
await cls._update_jiexi_record(jiexi_id, update_data)
|
||||
log.info(f"{log_prefix} AI解析完成并保存成功")
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
log.error(f"{log_prefix} AI解析超时")
|
||||
await cls._update_jiexi_status(jiexi_id, task_status=3, error_message="AI解析超时")
|
||||
except Exception as e:
|
||||
log.error(f"{log_prefix} AI解析失败: {str(e)}")
|
||||
await cls._update_jiexi_status(jiexi_id, task_status=0, error_message=str(e)[:500])
|
||||
|
||||
@classmethod
|
||||
async def _call_ai_service(cls, text: str, is_company: bool = False, max_retries: int = 3) -> str:
|
||||
"""
|
||||
调用AI服务进行财运解析(带重试机制)
|
||||
|
||||
Args:
|
||||
text: 输入文本
|
||||
is_company: 是否为公司财运解析
|
||||
max_retries: 最大重试次数,默认3次
|
||||
|
||||
Returns:
|
||||
AI响应结果
|
||||
"""
|
||||
from app.api.v1.module_application.ai.service import AIModelTestService
|
||||
|
||||
log.info(f"[财运解析] 调用AI服务,类型: {'企业' if is_company else '个人'},输入长度: {len(text)}")
|
||||
|
||||
# 根据类型选择不同的模型类型
|
||||
model_type = "caiyun_jiexi_qiye" if is_company else "caiyun_jiexi"
|
||||
|
||||
last_exception = None
|
||||
|
||||
for attempt in range(max_retries + 1): # +1 因为包含第一次尝试
|
||||
try:
|
||||
if attempt > 0:
|
||||
# 重试前等待,使用指数退避策略
|
||||
wait_time = min(2 ** attempt, 10) # 最大等待10秒
|
||||
log.info(f"[财运解析] 第{attempt + 1}次尝试,等待{wait_time}秒后重试")
|
||||
await asyncio.sleep(wait_time)
|
||||
|
||||
# 调用实际的AI服务,根据类型使用不同的模型
|
||||
ai_response = await AIModelTestService.test_naming(
|
||||
model_type=model_type,
|
||||
text=text
|
||||
)
|
||||
|
||||
log.info(f"[财运解析] AI服务响应成功,响应长度: {len(ai_response)}")
|
||||
return ai_response
|
||||
|
||||
except Exception as e:
|
||||
last_exception = e
|
||||
error_msg = str(e).lower()
|
||||
|
||||
# 判断是否为可重试的错误
|
||||
retryable_errors = [
|
||||
'invalid_grant',
|
||||
'bad_response_status_code',
|
||||
'timeout',
|
||||
'服务暂时不可用',
|
||||
'connection',
|
||||
'network',
|
||||
'500',
|
||||
'502',
|
||||
'503',
|
||||
'504'
|
||||
]
|
||||
|
||||
is_retryable = any(err in error_msg for err in retryable_errors)
|
||||
|
||||
if attempt < max_retries and is_retryable:
|
||||
log.warning(f"[财运解析] AI服务调用失败(第{attempt + 1}次尝试): {str(e)},将进行重试")
|
||||
continue
|
||||
else:
|
||||
if attempt >= max_retries:
|
||||
log.error(f"[财运解析] AI服务调用失败,已达到最大重试次数({max_retries}),最后错误: {str(e)}")
|
||||
else:
|
||||
log.error(f"[财运解析] AI服务调用失败,错误不可重试: {str(e)}")
|
||||
break
|
||||
|
||||
# 所有重试都失败了,使用降级方案
|
||||
log.warning("[财运解析] 使用模拟数据作为降级方案")
|
||||
|
||||
mock_response = {
|
||||
"wealth_score": 85,
|
||||
"wealth_level": "中富",
|
||||
"wealth_trend": "上升",
|
||||
"mingpan_jingpi": {
|
||||
"mingzao": "甲子年 丙寅月 戊午日 壬子时",
|
||||
"zhen_taiyang_shi": "1984年2月15日 23:45 (真太阳时)",
|
||||
"nongli_shengchen": "甲子年正月十四日子时",
|
||||
"qianshi_yinji": "前世为商贾之人,善于理财,今生承继财运基因",
|
||||
"jinsheng_keti": "此生课题:学会平衡物质与精神,通过正当途径积累财富",
|
||||
"bazi_paipan": {
|
||||
"nian": {"gan": "甲", "zhi": "子"},
|
||||
"yue": {"gan": "丙", "zhi": "寅"},
|
||||
"ri": {"gan": "戊", "zhi": "午"},
|
||||
"shi": {"gan": "壬", "zhi": "子"}
|
||||
},
|
||||
"mingge_cengci": "中等命格,财运中上",
|
||||
"guji_duanyu": "《滴天髓》云:戊土生于寅月,木旺土虚,得火生扶,财运可期",
|
||||
"qimen_paipan": "开门落宫,财星当令,主财运亨通",
|
||||
"qimen_geju": "天盘甲子戊加地盘甲戌己,为青龙返首,主财运回升",
|
||||
"wuxing_nengliang": {"mu": 30, "huo": 25, "tu": 20, "jin": 15, "shui": 10},
|
||||
"dashi_pizhu": "命主八字中财星透干,且得月令生扶,中年后财运大开",
|
||||
"wuxing_kaiyun": "宜补金水,可佩戴金饰或水晶,增强财运",
|
||||
"shishen_mangdian": "偏财心性,容易冲动投资,需控制投机心理",
|
||||
"shiye_caiyun_dingshu": "命中注定通过稳健经营获得财富,不宜投机取巧",
|
||||
"hunyin_qinggan": "财星为用神,配偶贤内助,有助财运提升",
|
||||
"wuxing_jiankang": "土弱易患脾胃疾病,金弱易有呼吸系统问题",
|
||||
"shensha_guiren": "命带天乙贵人,遇困有贵人相助;文昌入命,利文职财运"
|
||||
},
|
||||
"liunian_zongyun": {
|
||||
"dayun_zoushi": "30-40岁走丙寅大运,木火通明,事业财运俱佳",
|
||||
"taisui_jiangjun": "甲辰太岁,与命主年柱相合,主吉利亨通",
|
||||
"dangnian_liunian": "今年财运上升,投资理财有所收获,但需防小人",
|
||||
"liunian_shensha": "流年遇天德贵人,化险为夷;犯太阴煞,防女性小人",
|
||||
"fenqunti_zhuanyun": {
|
||||
"qingnian": "多学习理财知识,积累第一桶金",
|
||||
"zhongnian": "把握事业机遇,适度扩大投资",
|
||||
"laonian": "保守理财,注重资产保值"
|
||||
},
|
||||
"liunian_jixiong_fangwei": {
|
||||
"ji": ["东南", "正南", "西南"],
|
||||
"xiong": ["正北", "东北"]
|
||||
},
|
||||
"caifu_laiyuan": "主要来源于工作收入和稳健投资,偏财运一般",
|
||||
"touzi_lingyu_zhiyin": {
|
||||
"fangdichan": "吉利,可适度投资",
|
||||
"gupiao": "需谨慎,不宜重仓",
|
||||
"jijin": "适合定投,风险可控",
|
||||
"huangjin": "中性,可少量配置"
|
||||
},
|
||||
"dangnian_jiugong_feixing": "八白财星飞临东南方,利于财运提升",
|
||||
"liunian_huajie": "可在东南方摆放招财植物,佩戴黄水晶化解不利"
|
||||
},
|
||||
"fengshui_jinnang": {
|
||||
"guiren_huaxiang": "贵人多为中年男性,从事金融或地产行业,性格稳重",
|
||||
"waiju_shaji_huajie": {
|
||||
"lu_chong": "门前直路冲射,可摆放屏风化解",
|
||||
"jian_dao_sha": "两楼夹缝形成剪刀煞,宜挂八卦镜",
|
||||
"fan_gong_sha": "对面建筑反光,可用窗帘遮挡"
|
||||
},
|
||||
"jiaju_caiwei": {
|
||||
"ming_caiwei": "客厅进门对角线位置,宜摆放发财树",
|
||||
"an_caiwei": "根据飞星,今年在西南方,可放聚宝盆"
|
||||
},
|
||||
"zhichang_gaosheng": "办公桌左侧摆放文昌塔,背后靠实墙,面向开阔",
|
||||
"cuiwang_taohua": "卧室西南角摆放粉水晶,增强桃花运",
|
||||
"jiaju_zhiwu": [
|
||||
{"name": "发财树", "position": "客厅财位", "effect": "招财进宝"},
|
||||
{"name": "金钱树", "position": "办公室", "effect": "事业财运"},
|
||||
{"name": "富贵竹", "position": "书房", "effect": "文昌财运"}
|
||||
],
|
||||
"mengchong_fengshui": "养金鱼利财运,数量宜1、6、8条,忌养猫",
|
||||
"shuzi_nengliang": {
|
||||
"shouji_hao": "尾号带6、8、9的号码利财运",
|
||||
"che_pai": "选择数字组合168、888、666",
|
||||
"lou_ceng": "6、8、16、18层为吉"
|
||||
},
|
||||
"aiche_pingan": "车内可悬挂平安符,选择金色或红色装饰",
|
||||
"xijin_qianbao": {
|
||||
"yanse": "黄色、棕色、金色最佳",
|
||||
"zhidi": "真皮材质,避免帆布",
|
||||
"shiyong": "钱包内放红绳穿制的古钱币"
|
||||
},
|
||||
"xingyun_se_peidai": {
|
||||
"xingyun_se": ["金色", "黄色", "棕色"],
|
||||
"peidai_shipin": [
|
||||
{"name": "黄水晶手链", "effect": "增强正财运"},
|
||||
{"name": "貔貅吊坠", "effect": "招财辟邪"},
|
||||
{"name": "金戒指", "effect": "稳固财运"}
|
||||
]
|
||||
}
|
||||
},
|
||||
"yuedo_xiangpi": {
|
||||
"quannian_gaishu": {
|
||||
"zongyun_pinggu": "全年财运稳中有升,上半年平稳,下半年转旺",
|
||||
"caiyun_zhuti": "正财为主,偏财为辅,宜稳健经营",
|
||||
"guanjian_yueling": ["三月", "七月", "十月"],
|
||||
"zhuyao_jixiong": "整体吉利,需防五月小人,十二月破财"
|
||||
},
|
||||
"yueling_xiangxi": {
|
||||
"zhengYue": {
|
||||
"ganzhi": "甲寅",
|
||||
"hexin_yunshi": "新春开运,财源广进",
|
||||
"jixiong_dengji": "大吉",
|
||||
"caiyun_zhishu": 85,
|
||||
"shiye_zhishu": 78,
|
||||
"jiankang_zhishu": 82,
|
||||
"renmai_zhishu": 75,
|
||||
"zhongdian_shixiang": ["制定年度财务计划", "拓展人脉关系", "投资理财布局"],
|
||||
"jixiong_rizi": {
|
||||
"daji_rizi": ["初三", "初八", "十五"],
|
||||
"xiaoji_rizi": ["初五", "十二", "二十"],
|
||||
"jiji_rizi": ["初七", "十九", "二十五"]
|
||||
},
|
||||
"shendu_jiexi": "正月财星当令,利于开展新业务和投资项目,宜把握机遇积极行动"
|
||||
}
|
||||
},
|
||||
"kaiyun_zhidao": {
|
||||
"quannian_kaiyun": "佩戴黄水晶,办公室摆放招财树,增强财运磁场",
|
||||
"zhongdian_yueling_zhidao": {
|
||||
"最旺月份": "七月金水相生,财运最旺,可加大投资力度",
|
||||
"最弱月份": "五月木土相克,需谨慎理财,避免冲动消费"
|
||||
},
|
||||
"wuxing_tiaohe": "补金水,泄木火,平衡五行能量",
|
||||
"fengshui_buju": "财位摆放聚宝盆,门口悬挂五帝钱"
|
||||
}
|
||||
},
|
||||
"meiri_yuncheng": {
|
||||
"jinri_gaishu": {
|
||||
"riqi_ganzhi": "甲子日",
|
||||
"wuxing_zhuti": "木旺水相",
|
||||
"zongyun_pinggu": "今日运势中上,财运亨通,事业顺遂",
|
||||
"jixiong_dengji": "中吉",
|
||||
"zhuyao_yingxiang": "甲木得水生扶,生机勃勃,利于开拓进取",
|
||||
"kaiyun_yanse": ["青色", "绿色"],
|
||||
"jiji_yanse": ["红色", "紫色"]
|
||||
},
|
||||
"yunshi_xiangxi": {
|
||||
"caiyun_zhishu": 78,
|
||||
"caiyun_fenxi": "今日正财运佳,工作收入稳定,投资理财有小幅收益,适合签约合作",
|
||||
"shiye_zhishu": 82,
|
||||
"shiye_fenxi": "工作效率高,决策判断准确,与同事合作愉快,有升职加薪机会",
|
||||
"jiankang_zhishu": 75,
|
||||
"jiankang_fenxi": "精神状态良好,体力充沛,注意保护肝胆,少食辛辣",
|
||||
"qinggan_zhishu": 70,
|
||||
"qinggan_fenxi": "人际关系和谐,感情发展平稳,家庭氛围温馨"
|
||||
},
|
||||
"fangwei_zhidao": {
|
||||
"caishen_fangwei": {
|
||||
"fangxiang": "正东",
|
||||
"jiaodu": "75度",
|
||||
"shiyong_shijian": "上午9-11点最佳",
|
||||
"juti_zhidao": "面向正东方工作或谈判,有利于财运提升"
|
||||
},
|
||||
"xishen_fangwei": {
|
||||
"fangxiang": "东南",
|
||||
"jiaodu": "135度",
|
||||
"shiyong_shijian": "下午1-3点最佳",
|
||||
"juti_zhidao": "东南方摆放鲜花或绿植,增强喜庆气场"
|
||||
},
|
||||
"wenchang_fangwei": {
|
||||
"fangxiang": "正南",
|
||||
"jiaodu": "180度",
|
||||
"shiyong_shijian": "上午7-9点最佳",
|
||||
"juti_zhidao": "正南方学习工作,有利于思维敏捷,决策正确"
|
||||
},
|
||||
"jiji_fangwei": ["西北", "正北"],
|
||||
"jiji_zhidao": "避免在西北和正北方进行重要决策或签约"
|
||||
},
|
||||
"shichen_jixiong": {
|
||||
"chen_shi": {
|
||||
"shijian": "07:00-09:00",
|
||||
"ganzhi": "戊辰时",
|
||||
"jixiong": "大吉",
|
||||
"zhishu": 88,
|
||||
"shiyong_shixiang": ["工作开始", "学习进修", "商务会议"],
|
||||
"jiji_shixiang": ["懒散拖延", "负面思考"],
|
||||
"xiangxi_fenxi": "辰时龙抬头,精神饱满,工作效率最高,适合处理重要事务"
|
||||
}
|
||||
},
|
||||
"jinri_zhidao": {
|
||||
"zuijia_shijian": "辰时(07:00-09:00)和酉时(17:00-19:00)",
|
||||
"zhuyao_jiji": "未时(13:00-15:00)需特别谨慎",
|
||||
"kaiyun_jianyi": ["佩戴绿色饰品", "多接触大自然", "保持积极心态"],
|
||||
"zhuyi_shixiang": ["避免冲动决策", "注意肝胆保养", "防范小人是非"],
|
||||
"chuanyi_jianyi": {
|
||||
"yanse_xuanze": ["绿色", "青色"],
|
||||
"fengge_jianyi": "清新自然风格",
|
||||
"peishi_zhidao": "佩戴木质或绿色宝石饰品"
|
||||
},
|
||||
"yinshi_jianyi": {
|
||||
"shiwu_xuanze": ["绿叶蔬菜", "新鲜水果"],
|
||||
"jiji_shiwu": ["辛辣食物", "油腻食品"],
|
||||
"yinshi_shijian": "早餐7-9点,午餐11-13点最佳"
|
||||
}
|
||||
},
|
||||
"tebie_tixing": {
|
||||
"zhongda_shixiang": "今日适合签署重要合同,但需在上午完成",
|
||||
"touzi_licai": "可适度增加股票投资,但避免高风险产品",
|
||||
"renmai_guanxi": "主动与他人沟通,有利于建立长期合作关系",
|
||||
"jiankang_yangsheng": "多做户外运动,呼吸新鲜空气,有益身心健康"
|
||||
}
|
||||
},
|
||||
"unlocked": {
|
||||
"shendu_fenxi": "深度八字命盘解析,包含详细的大运流年分析...",
|
||||
"touzi_shiji": "最佳投资时机为每年3、6、9月,避开7、12月...",
|
||||
"caifu_mimi": "通过特定的风水布局和开运方法提升财运...",
|
||||
"weilai_yuce": "未来五年财运预测及重大投资建议..."
|
||||
}
|
||||
}
|
||||
|
||||
return json.dumps(mock_response, ensure_ascii=False)
|
||||
|
||||
@classmethod
|
||||
def _parse_ai_response(cls, ai_response: str, report_obj: Any) -> Dict[str, Any]:
|
||||
"""解析AI响应数据"""
|
||||
try:
|
||||
# 添加详细的调试信息
|
||||
log.info(f"[DEBUG] ai_response type: {type(ai_response)}")
|
||||
log.info(f"[DEBUG] ai_response length: {len(ai_response) if ai_response else 'None'}")
|
||||
log.info(f"[DEBUG] ai_response repr: {repr(ai_response[:200] if ai_response else 'None')}")
|
||||
|
||||
if not ai_response:
|
||||
raise ValueError("AI响应为空")
|
||||
|
||||
if not isinstance(ai_response, str):
|
||||
log.warning(f"AI响应不是字符串类型,尝试转换: {type(ai_response)}")
|
||||
ai_response = str(ai_response)
|
||||
|
||||
# 去除可能的前后空白字符和特殊字符
|
||||
ai_response = ai_response.strip()
|
||||
|
||||
# 尝试找到JSON开始和结束位置
|
||||
start_idx = ai_response.find('{')
|
||||
end_idx = ai_response.rfind('}')
|
||||
|
||||
if start_idx != -1 and end_idx != -1 and end_idx > start_idx:
|
||||
json_str = ai_response[start_idx:end_idx+1]
|
||||
log.info(f"[DEBUG] 提取的JSON字符串: {json_str[:200]}...")
|
||||
ai_data = json.loads(json_str)
|
||||
else:
|
||||
log.error(f"[DEBUG] 无法找到有效的JSON结构")
|
||||
raise ValueError("AI响应中没有找到有效的JSON结构")
|
||||
|
||||
# 构建完整的响应数据
|
||||
result_data = {
|
||||
"wealth_score": ai_data.get("wealth_score", 0),
|
||||
"wealth_level": ai_data.get("wealth_level", "平财"),
|
||||
"wealth_trend": ai_data.get("wealth_trend", "稳定"),
|
||||
"mingpan_jingpi": ai_data.get("mingpan_jingpi", {}),
|
||||
"liunian_zongyun": ai_data.get("liunian_zongyun", {}),
|
||||
"fengshui_jinnang": ai_data.get("fengshui_jinnang", {}),
|
||||
"yuedo_xiangpi": ai_data.get("yuedo_xiangpi", {}),
|
||||
"meiri_yuncheng": ai_data.get("meiri_yuncheng", {}),
|
||||
"unlocked": ai_data.get("unlocked", {}),
|
||||
"is_unlocked": False,
|
||||
"unlock_price": 19.90
|
||||
}
|
||||
|
||||
return result_data
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
log.error(f"AI响应JSON解析失败: {str(e)}")
|
||||
raise Exception(f"AI响应格式错误: {str(e)}")
|
||||
|
||||
@classmethod
|
||||
async def _update_jiexi_status(cls, jiexi_id: int, task_status: int, error_message: str = None):
|
||||
"""更新财运解析状态"""
|
||||
from app.core.database import async_db_session
|
||||
|
||||
async with async_db_session() as session:
|
||||
try:
|
||||
# 创建一个临时的auth对象用于更新操作
|
||||
temp_auth = AuthSchema(user=None, db=session)
|
||||
crud = YifanCaiyunJiexiCRUD(temp_auth)
|
||||
|
||||
# 先检查记录是否存在
|
||||
existing_record = await crud.get_yifan_caiyun_jiexi_crud(jiexi_id)
|
||||
if not existing_record:
|
||||
log.error(f"[财运解析] 记录不存在,ID: {jiexi_id}")
|
||||
raise Exception(f"财运解析记录不存在,ID: {jiexi_id}")
|
||||
|
||||
update_data = YifanCaiyunJiexiUpdateSchema(
|
||||
task_status=task_status,
|
||||
error_message=error_message
|
||||
)
|
||||
|
||||
await crud.update_yifan_caiyun_jiexi_crud(jiexi_id, update_data)
|
||||
await session.commit() # 提交事务
|
||||
log.info(f"[财运解析] 状态更新成功,ID: {jiexi_id}, 状态: {task_status}")
|
||||
except Exception as e:
|
||||
await session.rollback() # 回滚事务
|
||||
log.error(f"[财运解析] 状态更新失败,ID: {jiexi_id}, 错误: {str(e)}")
|
||||
raise
|
||||
|
||||
@classmethod
|
||||
async def _update_jiexi_record(cls, jiexi_id: int, update_data: YifanCaiyunJiexiUpdateSchema):
|
||||
"""更新财运解析记录"""
|
||||
from app.core.database import async_db_session
|
||||
|
||||
async with async_db_session() as session:
|
||||
try:
|
||||
# 创建一个临时的auth对象用于更新操作
|
||||
temp_auth = AuthSchema(user=None, db=session)
|
||||
crud = YifanCaiyunJiexiCRUD(temp_auth)
|
||||
|
||||
await crud.update_yifan_caiyun_jiexi_crud(jiexi_id, update_data)
|
||||
await session.commit() # 提交事务
|
||||
log.info(f"[财运解析] 记录更新成功,ID: {jiexi_id}")
|
||||
except Exception as e:
|
||||
await session.rollback() # 回滚事务
|
||||
log.error(f"[财运解析] 记录更新失败,ID: {jiexi_id}, 错误: {str(e)}")
|
||||
raise
|
||||
|
||||
@classmethod
|
||||
async def get_caiyun_jiexi_result_service(cls, auth: AuthSchema, jiexi_id: int) -> CaiyunJiexiAnalyzeResponseSchema:
|
||||
"""获取财运解析结果"""
|
||||
jiexi_obj = await YifanCaiyunJiexiCRUD(auth).get_yifan_caiyun_jiexi_crud(jiexi_id)
|
||||
|
||||
if not jiexi_obj:
|
||||
raise CustomException(msg="财运解析记录不存在")
|
||||
|
||||
if jiexi_obj.task_status != 5:
|
||||
raise CustomException(msg="财运解析尚未完成")
|
||||
|
||||
# 构建响应数据
|
||||
response_data = {
|
||||
"jiexi_id": jiexi_obj.id,
|
||||
"report_id": jiexi_obj.report_id,
|
||||
"name": jiexi_obj.name or "",
|
||||
"wealth_score": jiexi_obj.wealth_score or 0,
|
||||
"wealth_level": jiexi_obj.wealth_level or "平财",
|
||||
"wealth_trend": jiexi_obj.wealth_trend or "稳定",
|
||||
"mingpan_jingpi": jiexi_obj.mingpan_jingpi or "{}",
|
||||
"liunian_zongyun": jiexi_obj.liunian_zongyun or "{}",
|
||||
"fengshui_jinnang": jiexi_obj.fengshui_jinnang or "{}",
|
||||
"yuedo_xiangpi": jiexi_obj.yuedo_xiangpi or "{}",
|
||||
"meiri_yuncheng": jiexi_obj.meiri_yuncheng or "{}",
|
||||
"unlocked": jiexi_obj.unlocked_content if jiexi_obj.unlocked_content and jiexi_obj.is_unlocked else None,
|
||||
"is_unlocked": bool(jiexi_obj.is_unlocked),
|
||||
"unlock_price": float(jiexi_obj.unlock_price or 19.90)
|
||||
}
|
||||
|
||||
return CaiyunJiexiAnalyzeResponseSchema(**response_data)
|
||||
|
||||
@classmethod
|
||||
async def get_caiyun_jiexi_result_by_report_service(cls, auth: AuthSchema, report_id: int) -> CaiyunJiexiAnalyzeResponseSchema:
|
||||
"""根据报告ID获取财运解析结果"""
|
||||
jiexi_crud = YifanCaiyunJiexiCRUD(auth)
|
||||
jiexi_obj = await jiexi_crud.get_by_report_id_crud(report_id)
|
||||
|
||||
if not jiexi_obj:
|
||||
raise CustomException(msg="该报告暂无财运解析记录")
|
||||
|
||||
if jiexi_obj.task_status != 5:
|
||||
# 返回任务状态信息而不是抛出异常
|
||||
status_messages = {
|
||||
1: "财运解析已创建,等待处理",
|
||||
2: "财运解析正在进行中",
|
||||
3: "财运解析超时",
|
||||
0: "财运解析失败"
|
||||
}
|
||||
raise CustomException(msg=status_messages.get(jiexi_obj.task_status, "财运解析状态未知"))
|
||||
|
||||
# 构建响应数据(与原方法相同的逻辑)
|
||||
response_data = {
|
||||
"jiexi_id": jiexi_obj.id,
|
||||
"report_id": jiexi_obj.report_id,
|
||||
"name": jiexi_obj.name or "",
|
||||
"wealth_score": jiexi_obj.wealth_score or 0,
|
||||
"wealth_level": jiexi_obj.wealth_level or "平财",
|
||||
"wealth_trend": jiexi_obj.wealth_trend or "稳定",
|
||||
"mingpan_jingpi": jiexi_obj.mingpan_jingpi or "{}",
|
||||
"liunian_zongyun": jiexi_obj.liunian_zongyun or "{}",
|
||||
"fengshui_jinnang": jiexi_obj.fengshui_jinnang or "{}",
|
||||
"yuedo_xiangpi": jiexi_obj.yuedo_xiangpi or "{}",
|
||||
"meiri_yuncheng": jiexi_obj.meiri_yuncheng or "{}",
|
||||
"unlocked": jiexi_obj.unlocked_content if jiexi_obj.unlocked_content and jiexi_obj.is_unlocked else None,
|
||||
"is_unlocked": bool(jiexi_obj.is_unlocked),
|
||||
"unlock_price": float(jiexi_obj.unlock_price or 19.90)
|
||||
}
|
||||
|
||||
return CaiyunJiexiAnalyzeResponseSchema(**response_data)
|
||||
|
||||
@classmethod
|
||||
async def get_caiyun_jiexi_status_service(cls, auth: AuthSchema, jiexi_id: int) -> CaiyunJiexiTaskStatusSchema:
|
||||
"""获取财运解析任务状态"""
|
||||
jiexi_obj = await YifanCaiyunJiexiCRUD(auth).get_yifan_caiyun_jiexi_crud(jiexi_id)
|
||||
|
||||
if not jiexi_obj:
|
||||
raise CustomException(msg="财运解析记录不存在")
|
||||
|
||||
status_messages = {
|
||||
1: "已创建,等待处理",
|
||||
2: "正在解析中",
|
||||
5: "解析完成",
|
||||
3: "解析超时",
|
||||
0: "解析失败"
|
||||
}
|
||||
|
||||
progress_map = {1: 10, 2: 50, 5: 100, 3: 0, 0: 0}
|
||||
|
||||
return CaiyunJiexiTaskStatusSchema(
|
||||
jiexi_id=jiexi_obj.id,
|
||||
status=jiexi_obj.task_status or 1,
|
||||
message=status_messages.get(jiexi_obj.task_status or 1, "未知状态"),
|
||||
progress=progress_map.get(jiexi_obj.task_status or 1, 0)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def get_caiyun_jiexi_status_by_report_service(cls, auth: AuthSchema, report_id: int) -> CaiyunJiexiTaskStatusSchema:
|
||||
"""根据报告ID获取财运解析任务状态"""
|
||||
jiexi_crud = YifanCaiyunJiexiCRUD(auth)
|
||||
jiexi_obj = await jiexi_crud.get_by_report_id_crud(report_id)
|
||||
|
||||
if not jiexi_obj:
|
||||
# 如果没有财运解析记录,返回未创建状态
|
||||
return CaiyunJiexiTaskStatusSchema(
|
||||
jiexi_id=0,
|
||||
status=0,
|
||||
message="该报告暂无财运解析记录",
|
||||
progress=0
|
||||
)
|
||||
|
||||
status_messages = {
|
||||
1: "已创建,等待处理",
|
||||
2: "正在解析中",
|
||||
5: "解析完成",
|
||||
3: "解析超时",
|
||||
0: "解析失败"
|
||||
}
|
||||
|
||||
progress_map = {1: 10, 2: 50, 5: 100, 3: 0, 0: 0}
|
||||
|
||||
return CaiyunJiexiTaskStatusSchema(
|
||||
jiexi_id=jiexi_obj.id,
|
||||
status=jiexi_obj.task_status or 1,
|
||||
message=status_messages.get(jiexi_obj.task_status or 1, "未知状态"),
|
||||
progress=progress_map.get(jiexi_obj.task_status or 1, 0)
|
||||
)
|
||||
|
||||
# ==================== CRUD服务方法 ====================
|
||||
|
||||
@classmethod
|
||||
async def create_yifan_caiyun_jiexi_service(cls, auth: AuthSchema, data: YifanCaiyunJiexiCreateSchema) -> Dict[str, Any]:
|
||||
"""创建财运解析记录"""
|
||||
result = await YifanCaiyunJiexiCRUD(auth).create_yifan_caiyun_jiexi_crud(data=data)
|
||||
return {"id": result.id}
|
||||
|
||||
@classmethod
|
||||
async def get_yifan_caiyun_jiexi_service(cls, auth: AuthSchema, jiexi_id: int) -> Dict[str, Any]:
|
||||
"""获取财运解析详情"""
|
||||
result = await YifanCaiyunJiexiCRUD(auth).get_yifan_caiyun_jiexi_crud(jiexi_id=jiexi_id)
|
||||
return result.__dict__ if result else {}
|
||||
|
||||
@classmethod
|
||||
async def update_yifan_caiyun_jiexi_service(cls, auth: AuthSchema, jiexi_id: int, data: YifanCaiyunJiexiUpdateSchema) -> Dict[str, Any]:
|
||||
"""更新财运解析记录"""
|
||||
result = await YifanCaiyunJiexiCRUD(auth).update_yifan_caiyun_jiexi_crud(jiexi_id=jiexi_id, data=data)
|
||||
return {"id": result.id}
|
||||
|
||||
@classmethod
|
||||
async def delete_yifan_caiyun_jiexi_service(cls, auth: AuthSchema, jiexi_id: int) -> Dict[str, Any]:
|
||||
"""删除财运解析记录"""
|
||||
result = await YifanCaiyunJiexiCRUD(auth).delete_yifan_caiyun_jiexi_crud(jiexi_id=jiexi_id)
|
||||
return {"success": result}
|
||||
|
||||
@classmethod
|
||||
async def page_yifan_caiyun_jiexi_service(
|
||||
cls,
|
||||
auth: AuthSchema,
|
||||
page_no: int,
|
||||
page_size: int,
|
||||
search: YifanCaiyunJiexiQueryParam,
|
||||
order_by: str = None
|
||||
) -> Dict[str, Any]:
|
||||
"""分页查询财运解析列表"""
|
||||
return await YifanCaiyunJiexiCRUD(auth).page_yifan_caiyun_jiexi_crud(
|
||||
page_no=page_no,
|
||||
page_size=page_size,
|
||||
search=search,
|
||||
order_by=order_by
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
@@ -0,0 +1,153 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from fastapi import APIRouter, Depends, UploadFile, Body, Path, Query
|
||||
from fastapi.responses import StreamingResponse, JSONResponse
|
||||
|
||||
from app.common.response import SuccessResponse, StreamResponse
|
||||
from app.core.dependencies import AuthPermission, db_getter
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from app.core.base_params import PaginationQueryParam
|
||||
from app.utils.common_util import bytes2file_response
|
||||
from app.core.logger import log
|
||||
from app.core.base_schema import BatchSetAvailable
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from .service import YifanFaqService
|
||||
from .schema import YifanFaqCreateSchema, YifanFaqUpdateSchema, YifanFaqQueryParam
|
||||
|
||||
YifanFaqRouter = APIRouter(prefix='/yifan_faq', tags=["常见问题模块"])
|
||||
|
||||
|
||||
@YifanFaqRouter.get("/mini/grouped", summary="小程序-按分类获取常见问题", description="按分类分组返回常见问题列表,用于小程序展示")
|
||||
async def get_faq_grouped_for_mini(
|
||||
db: AsyncSession = Depends(db_getter)
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
小程序获取常见问题接口(按分类分组)
|
||||
|
||||
返回格式:
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"category": "general",
|
||||
"category_name": "通用",
|
||||
"items": [
|
||||
{"id": 1, "question": "...", "answer": "..."},
|
||||
...
|
||||
]
|
||||
},
|
||||
...
|
||||
]
|
||||
}
|
||||
"""
|
||||
result = await YifanFaqService.get_faq_grouped_service(db=db)
|
||||
return SuccessResponse(data=result, msg="获取成功")
|
||||
|
||||
@YifanFaqRouter.get("/detail/{id}", summary="获取常见问题详情", description="获取常见问题详情")
|
||||
async def get_yifan_faq_detail_controller(
|
||||
id: int = Path(..., description="ID"),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_faq:query"]))
|
||||
) -> JSONResponse:
|
||||
"""获取常见问题详情接口"""
|
||||
result_dict = await YifanFaqService.detail_yifan_faq_service(auth=auth, id=id)
|
||||
log.info(f"获取常见问题详情成功 {id}")
|
||||
return SuccessResponse(data=result_dict, msg="获取常见问题详情成功")
|
||||
|
||||
@YifanFaqRouter.get("/list", summary="查询常见问题列表", description="查询常见问题列表")
|
||||
async def get_yifan_faq_list_controller(
|
||||
page: PaginationQueryParam = Depends(),
|
||||
search: YifanFaqQueryParam = Depends(),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_faq:query"]))
|
||||
) -> JSONResponse:
|
||||
"""查询常见问题列表接口(数据库分页)"""
|
||||
result_dict = await YifanFaqService.page_yifan_faq_service(
|
||||
auth=auth,
|
||||
page_no=page.page_no if page.page_no is not None else 1,
|
||||
page_size=page.page_size if page.page_size is not None else 10,
|
||||
search=search,
|
||||
order_by=page.order_by
|
||||
)
|
||||
log.info("查询常见问题列表成功")
|
||||
return SuccessResponse(data=result_dict, msg="查询常见问题列表成功")
|
||||
|
||||
@YifanFaqRouter.post("/create", summary="创建常见问题", description="创建常见问题")
|
||||
async def create_yifan_faq_controller(
|
||||
data: YifanFaqCreateSchema,
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_faq:create"]))
|
||||
) -> JSONResponse:
|
||||
"""创建常见问题接口"""
|
||||
result_dict = await YifanFaqService.create_yifan_faq_service(auth=auth, data=data)
|
||||
log.info("创建常见问题成功")
|
||||
return SuccessResponse(data=result_dict, msg="创建常见问题成功")
|
||||
|
||||
@YifanFaqRouter.put("/update/{id}", summary="修改常见问题", description="修改常见问题")
|
||||
async def update_yifan_faq_controller(
|
||||
data: YifanFaqUpdateSchema,
|
||||
id: int = Path(..., description="ID"),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_faq:update"]))
|
||||
) -> JSONResponse:
|
||||
"""修改常见问题接口"""
|
||||
result_dict = await YifanFaqService.update_yifan_faq_service(auth=auth, id=id, data=data)
|
||||
log.info("修改常见问题成功")
|
||||
return SuccessResponse(data=result_dict, msg="修改常见问题成功")
|
||||
|
||||
@YifanFaqRouter.delete("/delete", summary="删除常见问题", description="删除常见问题")
|
||||
async def delete_yifan_faq_controller(
|
||||
ids: list[int] = Body(..., description="ID列表"),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_faq:delete"]))
|
||||
) -> JSONResponse:
|
||||
"""删除常见问题接口"""
|
||||
await YifanFaqService.delete_yifan_faq_service(auth=auth, ids=ids)
|
||||
log.info(f"删除常见问题成功: {ids}")
|
||||
return SuccessResponse(msg="删除常见问题成功")
|
||||
|
||||
@YifanFaqRouter.patch("/available/setting", summary="批量修改常见问题状态", description="批量修改常见问题状态")
|
||||
async def batch_set_available_yifan_faq_controller(
|
||||
data: BatchSetAvailable,
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_faq:patch"]))
|
||||
) -> JSONResponse:
|
||||
"""批量修改常见问题状态接口"""
|
||||
await YifanFaqService.set_available_yifan_faq_service(auth=auth, data=data)
|
||||
log.info(f"批量修改常见问题状态成功: {data.ids}")
|
||||
return SuccessResponse(msg="批量修改常见问题状态成功")
|
||||
|
||||
@YifanFaqRouter.post('/export', summary="导出常见问题", description="导出常见问题")
|
||||
async def export_yifan_faq_list_controller(
|
||||
search: YifanFaqQueryParam = Depends(),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_faq:export"]))
|
||||
) -> StreamingResponse:
|
||||
"""导出常见问题接口"""
|
||||
result_dict_list = await YifanFaqService.list_yifan_faq_service(search=search, auth=auth)
|
||||
export_result = await YifanFaqService.batch_export_yifan_faq_service(obj_list=result_dict_list)
|
||||
log.info('导出常见问题成功')
|
||||
|
||||
return StreamResponse(
|
||||
data=bytes2file_response(export_result),
|
||||
media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
headers={
|
||||
'Content-Disposition': 'attachment; filename=yifan_faq.xlsx'
|
||||
}
|
||||
)
|
||||
|
||||
@YifanFaqRouter.post('/import', summary="导入常见问题", description="导入常见问题")
|
||||
async def import_yifan_faq_list_controller(
|
||||
file: UploadFile,
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_faq:import"]))
|
||||
) -> JSONResponse:
|
||||
"""导入常见问题接口"""
|
||||
batch_import_result = await YifanFaqService.batch_import_yifan_faq_service(file=file, auth=auth, update_support=True)
|
||||
log.info("导入常见问题成功")
|
||||
|
||||
return SuccessResponse(data=batch_import_result, msg="导入常见问题成功")
|
||||
|
||||
@YifanFaqRouter.post('/download/template', summary="获取常见问题导入模板", description="获取常见问题导入模板", dependencies=[Depends(AuthPermission(["module_yifan:yifan_faq:download"]))])
|
||||
async def export_yifan_faq_template_controller() -> StreamingResponse:
|
||||
"""获取常见问题导入模板接口"""
|
||||
import_template_result = await YifanFaqService.import_template_download_yifan_faq_service()
|
||||
log.info('获取常见问题导入模板成功')
|
||||
|
||||
return StreamResponse(
|
||||
data=bytes2file_response(import_template_result),
|
||||
media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
headers={'Content-Disposition': 'attachment; filename=yifan_faq_template.xlsx'}
|
||||
)
|
||||
@@ -0,0 +1,123 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from typing import Sequence
|
||||
|
||||
from app.core.base_crud import CRUDBase
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from .model import YifanFaqModel
|
||||
from .schema import YifanFaqCreateSchema, YifanFaqUpdateSchema, YifanFaqOutSchema
|
||||
|
||||
|
||||
class YifanFaqCRUD(CRUDBase[YifanFaqModel, YifanFaqCreateSchema, YifanFaqUpdateSchema]):
|
||||
"""常见问题数据层"""
|
||||
|
||||
def __init__(self, auth: AuthSchema) -> None:
|
||||
"""
|
||||
初始化CRUD数据层
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
"""
|
||||
super().__init__(model=YifanFaqModel, auth=auth)
|
||||
|
||||
async def get_by_id_yifan_faq_crud(self, id: int, preload: list | None = None) -> YifanFaqModel | None:
|
||||
"""
|
||||
详情
|
||||
|
||||
参数:
|
||||
- id (int): 对象ID
|
||||
- preload (list | None): 预加载关系,未提供时使用模型默认项
|
||||
|
||||
返回:
|
||||
- YifanFaqModel | None: 模型实例或None
|
||||
"""
|
||||
return await self.get(id=id, preload=preload)
|
||||
|
||||
async def list_yifan_faq_crud(self, search: dict | None = None, order_by: list[dict] | None = None, preload: list | None = None) -> Sequence[YifanFaqModel]:
|
||||
"""
|
||||
列表查询
|
||||
|
||||
参数:
|
||||
- search (dict | None): 查询参数
|
||||
- order_by (list[dict] | None): 排序参数,未提供时使用模型默认项
|
||||
- preload (list | None): 预加载关系,未提供时使用模型默认项
|
||||
|
||||
返回:
|
||||
- Sequence[YifanFaqModel]: 模型实例序列
|
||||
"""
|
||||
return await self.list(search=search, order_by=order_by, preload=preload)
|
||||
|
||||
async def create_yifan_faq_crud(self, data: YifanFaqCreateSchema) -> YifanFaqModel | None:
|
||||
"""
|
||||
创建
|
||||
|
||||
参数:
|
||||
- data (YifanFaqCreateSchema): 创建模型
|
||||
|
||||
返回:
|
||||
- YifanFaqModel | None: 模型实例或None
|
||||
"""
|
||||
return await self.create(data=data)
|
||||
|
||||
async def update_yifan_faq_crud(self, id: int, data: YifanFaqUpdateSchema) -> YifanFaqModel | None:
|
||||
"""
|
||||
更新
|
||||
|
||||
参数:
|
||||
- id (int): 对象ID
|
||||
- data (YifanFaqUpdateSchema): 更新模型
|
||||
|
||||
返回:
|
||||
- YifanFaqModel | None: 模型实例或None
|
||||
"""
|
||||
return await self.update(id=id, data=data)
|
||||
|
||||
async def delete_yifan_faq_crud(self, ids: list[int]) -> None:
|
||||
"""
|
||||
批量删除
|
||||
|
||||
参数:
|
||||
- ids (list[int]): 对象ID列表
|
||||
|
||||
返回:
|
||||
- None
|
||||
"""
|
||||
return await self.delete(ids=ids)
|
||||
|
||||
async def set_available_yifan_faq_crud(self, ids: list[int], status: str) -> None:
|
||||
"""
|
||||
批量设置可用状态
|
||||
|
||||
参数:
|
||||
- ids (list[int]): 对象ID列表
|
||||
- status (str): 可用状态
|
||||
|
||||
返回:
|
||||
- None
|
||||
"""
|
||||
return await self.set(ids=ids, status=status)
|
||||
|
||||
async def page_yifan_faq_crud(self, offset: int, limit: int, order_by: list[dict] | None = None, search: dict | None = None, preload: list | None = None) -> dict:
|
||||
"""
|
||||
分页查询
|
||||
|
||||
参数:
|
||||
- offset (int): 偏移量
|
||||
- limit (int): 每页数量
|
||||
- order_by (list[dict] | None): 排序参数,未提供时使用模型默认项
|
||||
- search (dict | None): 查询参数,未提供时查询所有
|
||||
- preload (list | None): 预加载关系,未提供时使用模型默认项
|
||||
|
||||
返回:
|
||||
- Dict: 分页数据
|
||||
"""
|
||||
order_by_list = order_by or [{'id': 'asc'}]
|
||||
search_dict = search or {}
|
||||
return await self.page(
|
||||
offset=offset,
|
||||
limit=limit,
|
||||
order_by=order_by_list,
|
||||
search=search_dict,
|
||||
out_schema=YifanFaqOutSchema,
|
||||
preload=preload
|
||||
)
|
||||
@@ -0,0 +1,25 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from sqlalchemy import Integer, DateTime, SmallInteger, Text, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.base_model import ModelMixin, UserMixin
|
||||
|
||||
|
||||
class YifanFaqModel(ModelMixin, UserMixin):
|
||||
"""
|
||||
常见问题表
|
||||
"""
|
||||
__tablename__: str = 'yifan_faq'
|
||||
__table_args__: dict[str, str] = {'comment': '常见问题'}
|
||||
__loader_options__: list[str] = ["created_by", "updated_by"]
|
||||
|
||||
is_deleted: Mapped[int | None] = mapped_column(SmallInteger, nullable=True, comment='是否删除(0否 1是)')
|
||||
question: Mapped[str | None] = mapped_column(String(500), nullable=True, comment='问题标题')
|
||||
answer: Mapped[str | None] = mapped_column(Text, nullable=True, comment='答案内容')
|
||||
category: Mapped[str | None] = mapped_column(String(32), nullable=True, comment='分类(general:通用 payment:支付 account:账户 service:服务 other:其他)')
|
||||
sort_order: Mapped[int | None] = mapped_column(Integer, nullable=True, comment='排序(数值越小越靠前)')
|
||||
view_count: Mapped[int | None] = mapped_column(Integer, nullable=True, comment='浏览次数')
|
||||
is_hot: Mapped[int | None] = mapped_column(SmallInteger, nullable=True, comment='是否热门(0否 1是)')
|
||||
remark: Mapped[str | None] = mapped_column(String(255), nullable=True, comment='备注')
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from fastapi import Query
|
||||
|
||||
from app.core.validator import DateTimeStr
|
||||
from app.core.base_schema import BaseSchema, UserBySchema
|
||||
|
||||
class YifanFaqCreateSchema(BaseModel):
|
||||
"""
|
||||
常见问题新增模型
|
||||
"""
|
||||
is_deleted: int = Field(default=0, description='是否删除(0否 1是)')
|
||||
status: int = Field(default=1, description='状态(0禁用 1启用)')
|
||||
question: str = Field(default=..., description='问题标题')
|
||||
answer: str = Field(default=..., description='答案内容')
|
||||
category: str = Field(default='general', description='分类(general:通用 payment:支付 account:账户 service:服务 other:其他)')
|
||||
sort_order: int = Field(default=0, description='排序(数值越小越靠前)')
|
||||
view_count: int = Field(default=0, description='浏览次数')
|
||||
is_hot: int = Field(default=0, description='是否热门(0否 1是)')
|
||||
remark: str | None = Field(default=None, description='备注')
|
||||
|
||||
|
||||
class YifanFaqUpdateSchema(YifanFaqCreateSchema):
|
||||
"""
|
||||
常见问题更新模型
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
class YifanFaqOutSchema(YifanFaqCreateSchema, BaseSchema, UserBySchema):
|
||||
"""
|
||||
常见问题响应模型
|
||||
"""
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class YifanFaqQueryParam:
|
||||
"""常见问题查询参数"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
question: str | None = Query(None, description="问题标题"),
|
||||
answer: str | None = Query(None, description="答案内容"),
|
||||
category: str | None = Query(None, description="分类(general:通用 payment:支付 account:账户 service:服务 other:其他)"),
|
||||
remark: str | None = Query(None, description="备注"),
|
||||
created_id: int | None = Query(None, description="创建人ID"),
|
||||
updated_id: int | None = Query(None, description="更新人ID"),
|
||||
is_deleted: int | None = Query(None, description="是否删除(0否 1是)"),
|
||||
status: int | None = Query(None, description="状态(0禁用 1启用)"),
|
||||
sort_order: int | None = Query(None, description="排序(数值越小越靠前)"),
|
||||
view_count: int | None = Query(None, description="浏览次数"),
|
||||
is_hot: int | None = Query(None, description="是否热门(0否 1是)"),
|
||||
created_time: list[DateTimeStr] | None = Query(None, description="创建时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]),
|
||||
updated_time: list[DateTimeStr] | None = Query(None, description="更新时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]),
|
||||
|
||||
) -> None:
|
||||
|
||||
# 精确查询字段
|
||||
self.created_id = created_id
|
||||
# 精确查询字段
|
||||
self.updated_id = updated_id
|
||||
# 精确查询字段
|
||||
self.is_deleted = is_deleted
|
||||
# 精确查询字段
|
||||
self.status = status
|
||||
# 模糊查询字段
|
||||
self.question = ("like", question)
|
||||
# 模糊查询字段
|
||||
self.answer = ("like", answer)
|
||||
# 模糊查询字段
|
||||
self.category = ("like", category)
|
||||
# 精确查询字段
|
||||
self.sort_order = sort_order
|
||||
# 精确查询字段
|
||||
self.view_count = view_count
|
||||
# 精确查询字段
|
||||
self.is_hot = is_hot
|
||||
# 模糊查询字段
|
||||
self.remark = ("like", remark)
|
||||
# 时间范围查询
|
||||
if created_time and len(created_time) == 2:
|
||||
self.created_time = ("between", (created_time[0], created_time[1]))
|
||||
if updated_time and len(updated_time) == 2:
|
||||
self.updated_time = ("between", (updated_time[0], updated_time[1]))
|
||||
@@ -0,0 +1,292 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import io
|
||||
from fastapi import UploadFile
|
||||
import pandas as pd
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.base_schema import BatchSetAvailable
|
||||
from app.core.exceptions import CustomException
|
||||
from app.utils.excel_util import ExcelUtil
|
||||
from app.core.logger import log
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from .schema import YifanFaqCreateSchema, YifanFaqUpdateSchema, YifanFaqOutSchema, YifanFaqQueryParam
|
||||
from .crud import YifanFaqCRUD
|
||||
from .model import YifanFaqModel
|
||||
|
||||
|
||||
# 分类映射
|
||||
CATEGORY_MAP = {
|
||||
'general': '通用',
|
||||
'payment': '支付',
|
||||
'account': '账户',
|
||||
'service': '服务',
|
||||
'other': '其他',
|
||||
}
|
||||
|
||||
# 分类排序
|
||||
CATEGORY_ORDER = ['general', 'payment', 'account', 'service', 'other']
|
||||
|
||||
|
||||
class YifanFaqService:
|
||||
"""
|
||||
常见问题服务层
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
async def get_faq_grouped_service(cls, db: AsyncSession) -> list[dict]:
|
||||
"""
|
||||
按分类分组获取常见问题(小程序专用)
|
||||
"""
|
||||
# 查询所有启用的FAQ,按分类和排序字段排序
|
||||
stmt = (
|
||||
select(YifanFaqModel)
|
||||
.where(YifanFaqModel.status == '1')
|
||||
.order_by(YifanFaqModel.category, YifanFaqModel.sort_order)
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
faq_list = result.scalars().all()
|
||||
|
||||
# 按分类分组
|
||||
grouped = {}
|
||||
for faq in faq_list:
|
||||
category = faq.category or 'other'
|
||||
if category not in grouped:
|
||||
grouped[category] = []
|
||||
grouped[category].append({
|
||||
'id': faq.id,
|
||||
'question': faq.question,
|
||||
'answer': faq.answer,
|
||||
'is_hot': faq.is_hot,
|
||||
})
|
||||
|
||||
# 按预定义顺序返回
|
||||
result_list = []
|
||||
for cat in CATEGORY_ORDER:
|
||||
if cat in grouped:
|
||||
result_list.append({
|
||||
'category': cat,
|
||||
'category_name': CATEGORY_MAP.get(cat, cat),
|
||||
'items': grouped[cat]
|
||||
})
|
||||
|
||||
# 添加未知分类
|
||||
for cat, items in grouped.items():
|
||||
if cat not in CATEGORY_ORDER:
|
||||
result_list.append({
|
||||
'category': cat,
|
||||
'category_name': CATEGORY_MAP.get(cat, cat),
|
||||
'items': items
|
||||
})
|
||||
|
||||
return result_list
|
||||
|
||||
@classmethod
|
||||
async def detail_yifan_faq_service(cls, auth: AuthSchema, id: int) -> dict:
|
||||
"""详情"""
|
||||
obj = await YifanFaqCRUD(auth).get_by_id_yifan_faq_crud(id=id)
|
||||
if not obj:
|
||||
raise CustomException(msg="该数据不存在")
|
||||
return YifanFaqOutSchema.model_validate(obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def list_yifan_faq_service(cls, auth: AuthSchema, search: YifanFaqQueryParam | None = None, order_by: list[dict] | None = None) -> list[dict]:
|
||||
"""列表查询"""
|
||||
search_dict = search.__dict__ if search else None
|
||||
obj_list = await YifanFaqCRUD(auth).list_yifan_faq_crud(search=search_dict, order_by=order_by)
|
||||
return [YifanFaqOutSchema.model_validate(obj).model_dump() for obj in obj_list]
|
||||
|
||||
@classmethod
|
||||
async def page_yifan_faq_service(cls, auth: AuthSchema, page_no: int, page_size: int, search: YifanFaqQueryParam | None = None, order_by: list[dict] | None = None) -> dict:
|
||||
"""分页查询(数据库分页)"""
|
||||
search_dict = search.__dict__ if search else {}
|
||||
order_by_list = order_by or [{'id': 'asc'}]
|
||||
offset = (page_no - 1) * page_size
|
||||
result = await YifanFaqCRUD(auth).page_yifan_faq_crud(
|
||||
offset=offset,
|
||||
limit=page_size,
|
||||
order_by=order_by_list,
|
||||
search=search_dict
|
||||
)
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
async def create_yifan_faq_service(cls, auth: AuthSchema, data: YifanFaqCreateSchema) -> dict:
|
||||
"""创建"""
|
||||
# 检查唯一性约束
|
||||
obj = await YifanFaqCRUD(auth).create_yifan_faq_crud(data=data)
|
||||
return YifanFaqOutSchema.model_validate(obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def update_yifan_faq_service(cls, auth: AuthSchema, id: int, data: YifanFaqUpdateSchema) -> dict:
|
||||
"""更新"""
|
||||
# 检查数据是否存在
|
||||
obj = await YifanFaqCRUD(auth).get_by_id_yifan_faq_crud(id=id)
|
||||
if not obj:
|
||||
raise CustomException(msg='更新失败,该数据不存在')
|
||||
|
||||
# 检查唯一性约束
|
||||
|
||||
obj = await YifanFaqCRUD(auth).update_yifan_faq_crud(id=id, data=data)
|
||||
return YifanFaqOutSchema.model_validate(obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def delete_yifan_faq_service(cls, auth: AuthSchema, ids: list[int]) -> None:
|
||||
"""删除"""
|
||||
if len(ids) < 1:
|
||||
raise CustomException(msg='删除失败,删除对象不能为空')
|
||||
for id in ids:
|
||||
obj = await YifanFaqCRUD(auth).get_by_id_yifan_faq_crud(id=id)
|
||||
if not obj:
|
||||
raise CustomException(msg=f'删除失败,ID为{id}的数据不存在')
|
||||
await YifanFaqCRUD(auth).delete_yifan_faq_crud(ids=ids)
|
||||
|
||||
@classmethod
|
||||
async def set_available_yifan_faq_service(cls, auth: AuthSchema, data: BatchSetAvailable) -> None:
|
||||
"""批量设置状态"""
|
||||
await YifanFaqCRUD(auth).set_available_yifan_faq_crud(ids=data.ids, status=data.status)
|
||||
|
||||
@classmethod
|
||||
async def batch_export_yifan_faq_service(cls, obj_list: list[dict]) -> bytes:
|
||||
"""批量导出"""
|
||||
mapping_dict = {
|
||||
'id': '主键ID',
|
||||
'created_time': '创建时间',
|
||||
'updated_time': '更新时间',
|
||||
'created_id': '创建人ID',
|
||||
'updated_id': '更新人ID',
|
||||
'is_deleted': '是否删除(0否 1是)',
|
||||
'status': '状态(0禁用 1启用)',
|
||||
'question': '问题标题',
|
||||
'answer': '答案内容',
|
||||
'category': '分类(general:通用 payment:支付 account:账户 service:服务 other:其他)',
|
||||
'sort_order': '排序(数值越小越靠前)',
|
||||
'view_count': '浏览次数',
|
||||
'is_hot': '是否热门(0否 1是)',
|
||||
'remark': '备注',
|
||||
'updated_id': '更新者ID',
|
||||
}
|
||||
|
||||
data = obj_list.copy()
|
||||
for item in data:
|
||||
# 状态转换
|
||||
if 'status' in item:
|
||||
item['status'] = '启用' if item.get('status') == '0' else '停用'
|
||||
# 创建者转换
|
||||
creator_info = item.get('creator')
|
||||
if isinstance(creator_info, dict):
|
||||
item['creator'] = creator_info.get('name', '未知')
|
||||
elif creator_info is None:
|
||||
item['creator'] = '未知'
|
||||
|
||||
return ExcelUtil.export_list2excel(list_data=data, mapping_dict=mapping_dict)
|
||||
|
||||
@classmethod
|
||||
async def batch_import_yifan_faq_service(cls, auth: AuthSchema, file: UploadFile, update_support: bool = False) -> str:
|
||||
"""批量导入"""
|
||||
header_dict = {
|
||||
'主键ID': 'id',
|
||||
'创建时间': 'created_time',
|
||||
'更新时间': 'updated_time',
|
||||
'创建人ID': 'created_id',
|
||||
'更新人ID': 'updated_id',
|
||||
'是否删除(0否 1是)': 'is_deleted',
|
||||
'状态(0禁用 1启用)': 'status',
|
||||
'问题标题': 'question',
|
||||
'答案内容': 'answer',
|
||||
'分类(general:通用 payment:支付 account:账户 service:服务 other:其他)': 'category',
|
||||
'排序(数值越小越靠前)': 'sort_order',
|
||||
'浏览次数': 'view_count',
|
||||
'是否热门(0否 1是)': 'is_hot',
|
||||
'备注': 'remark',
|
||||
}
|
||||
|
||||
try:
|
||||
contents = await file.read()
|
||||
df = pd.read_excel(io.BytesIO(contents))
|
||||
await file.close()
|
||||
|
||||
if df.empty:
|
||||
raise CustomException(msg="导入文件为空")
|
||||
|
||||
missing_headers = [header for header in header_dict.keys() if header not in df.columns]
|
||||
if missing_headers:
|
||||
raise CustomException(msg=f"导入文件缺少必要的列: {', '.join(missing_headers)}")
|
||||
|
||||
df.rename(columns=header_dict, inplace=True)
|
||||
|
||||
# 验证必填字段
|
||||
|
||||
error_msgs = []
|
||||
success_count = 0
|
||||
count = 0
|
||||
|
||||
for index, row in df.iterrows():
|
||||
count += 1
|
||||
try:
|
||||
data = {
|
||||
"id": row['id'],
|
||||
"created_time": row['created_time'],
|
||||
"updated_time": row['updated_time'],
|
||||
"created_id": row['created_id'],
|
||||
"updated_id": row['updated_id'],
|
||||
"is_deleted": row['is_deleted'],
|
||||
"status": row['status'],
|
||||
"question": row['question'],
|
||||
"answer": row['answer'],
|
||||
"category": row['category'],
|
||||
"sort_order": row['sort_order'],
|
||||
"view_count": row['view_count'],
|
||||
"is_hot": row['is_hot'],
|
||||
"remark": row['remark'],
|
||||
}
|
||||
# 使用CreateSchema做校验后入库
|
||||
create_schema = YifanFaqCreateSchema.model_validate(data)
|
||||
|
||||
# 检查唯一性约束
|
||||
|
||||
await YifanFaqCRUD(auth).create_yifan_faq_crud(data=create_schema)
|
||||
success_count += 1
|
||||
except Exception as e:
|
||||
error_msgs.append(f"第{count}行: {str(e)}")
|
||||
continue
|
||||
|
||||
result = f"成功导入 {success_count} 条数据"
|
||||
if error_msgs:
|
||||
result += "\n错误信息:\n" + "\n".join(error_msgs)
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
log.error(f"批量导入失败: {str(e)}")
|
||||
raise CustomException(msg=f"导入失败: {str(e)}")
|
||||
|
||||
@classmethod
|
||||
async def import_template_download_yifan_faq_service(cls) -> bytes:
|
||||
"""下载导入模板"""
|
||||
header_list = [
|
||||
'主键ID',
|
||||
'创建时间',
|
||||
'更新时间',
|
||||
'创建人ID',
|
||||
'更新人ID',
|
||||
'是否删除(0否 1是)',
|
||||
'状态(0禁用 1启用)',
|
||||
'问题标题',
|
||||
'答案内容',
|
||||
'分类(general:通用 payment:支付 account:账户 service:服务 other:其他)',
|
||||
'排序(数值越小越靠前)',
|
||||
'浏览次数',
|
||||
'是否热门(0否 1是)',
|
||||
'备注',
|
||||
]
|
||||
selector_header_list = []
|
||||
option_list = []
|
||||
|
||||
# 添加下拉选项
|
||||
|
||||
return ExcelUtil.get_excel_template(
|
||||
header_list=header_list,
|
||||
selector_header_list=selector_header_list,
|
||||
option_list=option_list
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
@@ -0,0 +1,144 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from fastapi import APIRouter, Depends, UploadFile, Body, Path, Query, Request
|
||||
from fastapi.responses import StreamingResponse, JSONResponse
|
||||
|
||||
from app.common.response import SuccessResponse, StreamResponse, ErrorResponse
|
||||
from app.core.dependencies import AuthPermission, db_getter, get_current_user
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from app.core.base_params import PaginationQueryParam
|
||||
from app.utils.common_util import bytes2file_response
|
||||
from app.core.logger import log
|
||||
from app.core.base_schema import BatchSetAvailable
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from .service import YifanFeedbackService
|
||||
from .schema import YifanFeedbackCreateSchema, YifanFeedbackUpdateSchema, YifanFeedbackQueryParam, MiniFeedbackSubmitSchema
|
||||
|
||||
YifanFeedbackRouter = APIRouter(prefix='/yifan_feedback', tags=["意见反馈模块"])
|
||||
|
||||
|
||||
@YifanFeedbackRouter.post("/mini/submit", summary="小程序-提交意见反馈", description="小程序用户提交意见反馈")
|
||||
async def submit_feedback_for_mini(
|
||||
data: MiniFeedbackSubmitSchema,
|
||||
auth: AuthSchema = Depends(get_current_user)
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
小程序提交意见反馈接口
|
||||
|
||||
需要登录,自动获取当前用户ID
|
||||
"""
|
||||
user_id = auth.user.id
|
||||
|
||||
result = await YifanFeedbackService.submit_feedback_service(auth=auth, user_id=user_id, data=data)
|
||||
log.info(f"用户 {user_id} 提交意见反馈成功")
|
||||
return SuccessResponse(data=result, msg="提交成功,感谢您的反馈!")
|
||||
|
||||
@YifanFeedbackRouter.get("/detail/{id}", summary="获取意见反馈详情", description="获取意见反馈详情")
|
||||
async def get_yifan_feedback_detail_controller(
|
||||
id: int = Path(..., description="ID"),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_feedback:query"]))
|
||||
) -> JSONResponse:
|
||||
"""获取意见反馈详情接口"""
|
||||
result_dict = await YifanFeedbackService.detail_yifan_feedback_service(auth=auth, id=id)
|
||||
log.info(f"获取意见反馈详情成功 {id}")
|
||||
return SuccessResponse(data=result_dict, msg="获取意见反馈详情成功")
|
||||
|
||||
@YifanFeedbackRouter.get("/list", summary="查询意见反馈列表", description="查询意见反馈列表")
|
||||
async def get_yifan_feedback_list_controller(
|
||||
page: PaginationQueryParam = Depends(),
|
||||
search: YifanFeedbackQueryParam = Depends(),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_feedback:query"]))
|
||||
) -> JSONResponse:
|
||||
"""查询意见反馈列表接口(数据库分页)"""
|
||||
result_dict = await YifanFeedbackService.page_yifan_feedback_service(
|
||||
auth=auth,
|
||||
page_no=page.page_no if page.page_no is not None else 1,
|
||||
page_size=page.page_size if page.page_size is not None else 10,
|
||||
search=search,
|
||||
order_by=page.order_by
|
||||
)
|
||||
log.info("查询意见反馈列表成功")
|
||||
return SuccessResponse(data=result_dict, msg="查询意见反馈列表成功")
|
||||
|
||||
@YifanFeedbackRouter.post("/create", summary="创建意见反馈", description="创建意见反馈")
|
||||
async def create_yifan_feedback_controller(
|
||||
data: YifanFeedbackCreateSchema,
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_feedback:create"]))
|
||||
) -> JSONResponse:
|
||||
"""创建意见反馈接口"""
|
||||
result_dict = await YifanFeedbackService.create_yifan_feedback_service(auth=auth, data=data)
|
||||
log.info("创建意见反馈成功")
|
||||
return SuccessResponse(data=result_dict, msg="创建意见反馈成功")
|
||||
|
||||
@YifanFeedbackRouter.put("/update/{id}", summary="修改意见反馈", description="修改意见反馈")
|
||||
async def update_yifan_feedback_controller(
|
||||
data: YifanFeedbackUpdateSchema,
|
||||
id: int = Path(..., description="ID"),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_feedback:update"]))
|
||||
) -> JSONResponse:
|
||||
"""修改意见反馈接口"""
|
||||
result_dict = await YifanFeedbackService.update_yifan_feedback_service(auth=auth, id=id, data=data)
|
||||
log.info("修改意见反馈成功")
|
||||
return SuccessResponse(data=result_dict, msg="修改意见反馈成功")
|
||||
|
||||
@YifanFeedbackRouter.delete("/delete", summary="删除意见反馈", description="删除意见反馈")
|
||||
async def delete_yifan_feedback_controller(
|
||||
ids: list[int] = Body(..., description="ID列表"),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_feedback:delete"]))
|
||||
) -> JSONResponse:
|
||||
"""删除意见反馈接口"""
|
||||
await YifanFeedbackService.delete_yifan_feedback_service(auth=auth, ids=ids)
|
||||
log.info(f"删除意见反馈成功: {ids}")
|
||||
return SuccessResponse(msg="删除意见反馈成功")
|
||||
|
||||
@YifanFeedbackRouter.patch("/available/setting", summary="批量修改意见反馈状态", description="批量修改意见反馈状态")
|
||||
async def batch_set_available_yifan_feedback_controller(
|
||||
data: BatchSetAvailable,
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_feedback:patch"]))
|
||||
) -> JSONResponse:
|
||||
"""批量修改意见反馈状态接口"""
|
||||
await YifanFeedbackService.set_available_yifan_feedback_service(auth=auth, data=data)
|
||||
log.info(f"批量修改意见反馈状态成功: {data.ids}")
|
||||
return SuccessResponse(msg="批量修改意见反馈状态成功")
|
||||
|
||||
@YifanFeedbackRouter.post('/export', summary="导出意见反馈", description="导出意见反馈")
|
||||
async def export_yifan_feedback_list_controller(
|
||||
search: YifanFeedbackQueryParam = Depends(),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_feedback:export"]))
|
||||
) -> StreamingResponse:
|
||||
"""导出意见反馈接口"""
|
||||
result_dict_list = await YifanFeedbackService.list_yifan_feedback_service(search=search, auth=auth)
|
||||
export_result = await YifanFeedbackService.batch_export_yifan_feedback_service(obj_list=result_dict_list)
|
||||
log.info('导出意见反馈成功')
|
||||
|
||||
return StreamResponse(
|
||||
data=bytes2file_response(export_result),
|
||||
media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
headers={
|
||||
'Content-Disposition': 'attachment; filename=yifan_feedback.xlsx'
|
||||
}
|
||||
)
|
||||
|
||||
@YifanFeedbackRouter.post('/import', summary="导入意见反馈", description="导入意见反馈")
|
||||
async def import_yifan_feedback_list_controller(
|
||||
file: UploadFile,
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_feedback:import"]))
|
||||
) -> JSONResponse:
|
||||
"""导入意见反馈接口"""
|
||||
batch_import_result = await YifanFeedbackService.batch_import_yifan_feedback_service(file=file, auth=auth, update_support=True)
|
||||
log.info("导入意见反馈成功")
|
||||
|
||||
return SuccessResponse(data=batch_import_result, msg="导入意见反馈成功")
|
||||
|
||||
@YifanFeedbackRouter.post('/download/template', summary="获取意见反馈导入模板", description="获取意见反馈导入模板", dependencies=[Depends(AuthPermission(["module_yifan:yifan_feedback:download"]))])
|
||||
async def export_yifan_feedback_template_controller() -> StreamingResponse:
|
||||
"""获取意见反馈导入模板接口"""
|
||||
import_template_result = await YifanFeedbackService.import_template_download_yifan_feedback_service()
|
||||
log.info('获取意见反馈导入模板成功')
|
||||
|
||||
return StreamResponse(
|
||||
data=bytes2file_response(import_template_result),
|
||||
media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
headers={'Content-Disposition': 'attachment; filename=yifan_feedback_template.xlsx'}
|
||||
)
|
||||
@@ -0,0 +1,123 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from typing import Sequence
|
||||
|
||||
from app.core.base_crud import CRUDBase
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from .model import YifanFeedbackModel
|
||||
from .schema import YifanFeedbackCreateSchema, YifanFeedbackUpdateSchema, YifanFeedbackOutSchema
|
||||
|
||||
|
||||
class YifanFeedbackCRUD(CRUDBase[YifanFeedbackModel, YifanFeedbackCreateSchema, YifanFeedbackUpdateSchema]):
|
||||
"""意见反馈数据层"""
|
||||
|
||||
def __init__(self, auth: AuthSchema) -> None:
|
||||
"""
|
||||
初始化CRUD数据层
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
"""
|
||||
super().__init__(model=YifanFeedbackModel, auth=auth)
|
||||
|
||||
async def get_by_id_yifan_feedback_crud(self, id: int, preload: list | None = None) -> YifanFeedbackModel | None:
|
||||
"""
|
||||
详情
|
||||
|
||||
参数:
|
||||
- id (int): 对象ID
|
||||
- preload (list | None): 预加载关系,未提供时使用模型默认项
|
||||
|
||||
返回:
|
||||
- YifanFeedbackModel | None: 模型实例或None
|
||||
"""
|
||||
return await self.get(id=id, preload=preload)
|
||||
|
||||
async def list_yifan_feedback_crud(self, search: dict | None = None, order_by: list[dict] | None = None, preload: list | None = None) -> Sequence[YifanFeedbackModel]:
|
||||
"""
|
||||
列表查询
|
||||
|
||||
参数:
|
||||
- search (dict | None): 查询参数
|
||||
- order_by (list[dict] | None): 排序参数,未提供时使用模型默认项
|
||||
- preload (list | None): 预加载关系,未提供时使用模型默认项
|
||||
|
||||
返回:
|
||||
- Sequence[YifanFeedbackModel]: 模型实例序列
|
||||
"""
|
||||
return await self.list(search=search, order_by=order_by, preload=preload)
|
||||
|
||||
async def create_yifan_feedback_crud(self, data: YifanFeedbackCreateSchema) -> YifanFeedbackModel | None:
|
||||
"""
|
||||
创建
|
||||
|
||||
参数:
|
||||
- data (YifanFeedbackCreateSchema): 创建模型
|
||||
|
||||
返回:
|
||||
- YifanFeedbackModel | None: 模型实例或None
|
||||
"""
|
||||
return await self.create(data=data)
|
||||
|
||||
async def update_yifan_feedback_crud(self, id: int, data: YifanFeedbackUpdateSchema) -> YifanFeedbackModel | None:
|
||||
"""
|
||||
更新
|
||||
|
||||
参数:
|
||||
- id (int): 对象ID
|
||||
- data (YifanFeedbackUpdateSchema): 更新模型
|
||||
|
||||
返回:
|
||||
- YifanFeedbackModel | None: 模型实例或None
|
||||
"""
|
||||
return await self.update(id=id, data=data)
|
||||
|
||||
async def delete_yifan_feedback_crud(self, ids: list[int]) -> None:
|
||||
"""
|
||||
批量删除
|
||||
|
||||
参数:
|
||||
- ids (list[int]): 对象ID列表
|
||||
|
||||
返回:
|
||||
- None
|
||||
"""
|
||||
return await self.delete(ids=ids)
|
||||
|
||||
async def set_available_yifan_feedback_crud(self, ids: list[int], status: str) -> None:
|
||||
"""
|
||||
批量设置可用状态
|
||||
|
||||
参数:
|
||||
- ids (list[int]): 对象ID列表
|
||||
- status (str): 可用状态
|
||||
|
||||
返回:
|
||||
- None
|
||||
"""
|
||||
return await self.set(ids=ids, status=status)
|
||||
|
||||
async def page_yifan_feedback_crud(self, offset: int, limit: int, order_by: list[dict] | None = None, search: dict | None = None, preload: list | None = None) -> dict:
|
||||
"""
|
||||
分页查询
|
||||
|
||||
参数:
|
||||
- offset (int): 偏移量
|
||||
- limit (int): 每页数量
|
||||
- order_by (list[dict] | None): 排序参数,未提供时使用模型默认项
|
||||
- search (dict | None): 查询参数,未提供时查询所有
|
||||
- preload (list | None): 预加载关系,未提供时使用模型默认项
|
||||
|
||||
返回:
|
||||
- Dict: 分页数据
|
||||
"""
|
||||
order_by_list = order_by or [{'id': 'asc'}]
|
||||
search_dict = search or {}
|
||||
return await self.page(
|
||||
offset=offset,
|
||||
limit=limit,
|
||||
order_by=order_by_list,
|
||||
search=search_dict,
|
||||
out_schema=YifanFeedbackOutSchema,
|
||||
preload=preload
|
||||
)
|
||||
@@ -0,0 +1,29 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import datetime
|
||||
from sqlalchemy import Integer, DateTime, SmallInteger, Text, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.base_model import ModelMixin, UserMixin
|
||||
|
||||
|
||||
class YifanFeedbackModel(ModelMixin, UserMixin):
|
||||
"""
|
||||
意见反馈表
|
||||
"""
|
||||
__tablename__: str = 'yifan_feedback'
|
||||
__table_args__: dict[str, str] = {'comment': '意见反馈'}
|
||||
__loader_options__: list[str] = ["created_by", "updated_by"]
|
||||
|
||||
is_deleted: Mapped[int | None] = mapped_column(SmallInteger, nullable=True, comment='是否删除(0否 1是)')
|
||||
user_id: Mapped[int | None] = mapped_column(Integer, nullable=True, comment='用户ID')
|
||||
content: Mapped[str | None] = mapped_column(Text, nullable=True, comment='反馈内容')
|
||||
images: Mapped[str | None] = mapped_column(String(1000), nullable=True, comment='图片URL(多个用逗号分隔)')
|
||||
contact: Mapped[str | None] = mapped_column(String(100), nullable=True, comment='联系方式')
|
||||
feedback_type: Mapped[str | None] = mapped_column(String(32), nullable=True, comment='反馈类型(suggestion:建议 bug:问题 complaint:投诉 other:其他)')
|
||||
handle_status: Mapped[int | None] = mapped_column(SmallInteger, nullable=True, comment='处理状态(0待处理 1处理中 2已处理 3已关闭)')
|
||||
handle_result: Mapped[str | None] = mapped_column(Text, nullable=True, comment='处理结果')
|
||||
handle_time: Mapped[datetime.datetime | None] = mapped_column(DateTime, nullable=True, comment='处理时间')
|
||||
handler_id: Mapped[int | None] = mapped_column(Integer, nullable=True, comment='处理人ID')
|
||||
remark: Mapped[str | None] = mapped_column(String(255), nullable=True, comment='备注')
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import datetime
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from fastapi import Query
|
||||
|
||||
from app.core.validator import DateTimeStr
|
||||
from app.core.base_schema import BaseSchema, UserBySchema
|
||||
|
||||
|
||||
class MiniFeedbackSubmitSchema(BaseModel):
|
||||
"""
|
||||
小程序提交意见反馈模型
|
||||
"""
|
||||
content: str = Field(..., min_length=1, max_length=2000, description='反馈内容')
|
||||
images: str | None = Field(default=None, description='图片URL(多个用逗号分隔)')
|
||||
contact: str | None = Field(default=None, max_length=100, description='联系方式')
|
||||
feedback_type: str = Field(default='other', description='反馈类型(suggestion:建议 bug:问题 complaint:投诉 other:其他)')
|
||||
|
||||
|
||||
class YifanFeedbackCreateSchema(BaseModel):
|
||||
"""
|
||||
意见反馈新增模型
|
||||
"""
|
||||
is_deleted: int = Field(default=0, description='是否删除(0否 1是)')
|
||||
status: int = Field(default=1, description='状态(0禁用 1启用)')
|
||||
user_id: int | None = Field(default=None, description='用户ID')
|
||||
content: str = Field(default=..., description='反馈内容')
|
||||
images: str | None = Field(default=None, description='图片URL(多个用逗号分隔)')
|
||||
contact: str | None = Field(default=None, description='联系方式')
|
||||
feedback_type: str = Field(default='other', description='反馈类型(suggestion:建议 bug:问题 complaint:投诉 other:其他)')
|
||||
handle_status: int = Field(default=0, description='处理状态(0待处理 1处理中 2已处理 3已关闭)')
|
||||
handle_result: str | None = Field(default=None, description='处理结果')
|
||||
handle_time: DateTimeStr | None = Field(default=None, description='处理时间')
|
||||
handler_id: int | None = Field(default=None, description='处理人ID')
|
||||
remark: str | None = Field(default=None, description='备注')
|
||||
|
||||
|
||||
class YifanFeedbackUpdateSchema(YifanFeedbackCreateSchema):
|
||||
"""
|
||||
意见反馈更新模型
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
class YifanFeedbackOutSchema(YifanFeedbackCreateSchema, BaseSchema, UserBySchema):
|
||||
"""
|
||||
意见反馈响应模型
|
||||
"""
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class YifanFeedbackQueryParam:
|
||||
"""意见反馈查询参数"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
content: str | None = Query(None, description="反馈内容"),
|
||||
images: str | None = Query(None, description="图片URL(多个用逗号分隔)"),
|
||||
contact: str | None = Query(None, description="联系方式"),
|
||||
feedback_type: str | None = Query(None, description="反馈类型(suggestion:建议 bug:问题 complaint:投诉 other:其他)"),
|
||||
handle_result: str | None = Query(None, description="处理结果"),
|
||||
remark: str | None = Query(None, description="备注"),
|
||||
created_id: int | None = Query(None, description="创建人ID"),
|
||||
updated_id: int | None = Query(None, description="更新人ID"),
|
||||
is_deleted: int | None = Query(None, description="是否删除(0否 1是)"),
|
||||
status: int | None = Query(None, description="状态(0禁用 1启用)"),
|
||||
user_id: int | None = Query(None, description="用户ID"),
|
||||
handle_status: int | None = Query(None, description="处理状态(0待处理 1处理中 2已处理 3已关闭)"),
|
||||
handle_time: datetime.datetime | None = Query(None, description="处理时间"),
|
||||
handler_id: int | None = Query(None, description="处理人ID"),
|
||||
created_time: list[DateTimeStr] | None = Query(None, description="创建时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]),
|
||||
updated_time: list[DateTimeStr] | None = Query(None, description="更新时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]),
|
||||
|
||||
) -> None:
|
||||
|
||||
# 精确查询字段
|
||||
self.created_id = created_id
|
||||
# 精确查询字段
|
||||
self.updated_id = updated_id
|
||||
# 精确查询字段
|
||||
self.is_deleted = is_deleted
|
||||
# 精确查询字段
|
||||
self.status = status
|
||||
# 精确查询字段
|
||||
self.user_id = user_id
|
||||
# 模糊查询字段
|
||||
self.content = ("like", content)
|
||||
# 模糊查询字段
|
||||
self.images = ("like", images)
|
||||
# 模糊查询字段
|
||||
self.contact = ("like", contact)
|
||||
# 模糊查询字段
|
||||
self.feedback_type = ("like", feedback_type)
|
||||
# 精确查询字段
|
||||
self.handle_status = handle_status
|
||||
# 模糊查询字段
|
||||
self.handle_result = ("like", handle_result)
|
||||
# 精确查询字段
|
||||
self.handle_time = handle_time
|
||||
# 精确查询字段
|
||||
self.handler_id = handler_id
|
||||
# 模糊查询字段
|
||||
self.remark = ("like", remark)
|
||||
# 时间范围查询
|
||||
if created_time and len(created_time) == 2:
|
||||
self.created_time = ("between", (created_time[0], created_time[1]))
|
||||
if updated_time and len(updated_time) == 2:
|
||||
self.updated_time = ("between", (updated_time[0], updated_time[1]))
|
||||
@@ -0,0 +1,267 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import io
|
||||
from fastapi import UploadFile
|
||||
import pandas as pd
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.base_schema import BatchSetAvailable
|
||||
from app.core.exceptions import CustomException
|
||||
from app.utils.excel_util import ExcelUtil
|
||||
from app.core.logger import log
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from .schema import YifanFeedbackCreateSchema, YifanFeedbackUpdateSchema, YifanFeedbackOutSchema, YifanFeedbackQueryParam, MiniFeedbackSubmitSchema
|
||||
from .crud import YifanFeedbackCRUD
|
||||
from .model import YifanFeedbackModel
|
||||
|
||||
|
||||
class YifanFeedbackService:
|
||||
"""
|
||||
意见反馈服务层
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
async def submit_feedback_service(cls, auth: AuthSchema, user_id: int, data: MiniFeedbackSubmitSchema) -> dict:
|
||||
"""
|
||||
小程序提交意见反馈
|
||||
"""
|
||||
# 构造创建数据
|
||||
create_data = {
|
||||
'user_id': user_id,
|
||||
'content': data.content,
|
||||
'images': data.images,
|
||||
'contact': data.contact,
|
||||
'feedback_type': data.feedback_type,
|
||||
'handle_status': 0, # 待处理
|
||||
'status': 1, # 启用
|
||||
'is_deleted': 0,
|
||||
}
|
||||
|
||||
obj = await YifanFeedbackCRUD(auth).create(data=create_data)
|
||||
|
||||
return {
|
||||
'id': obj.id,
|
||||
'feedback_type': obj.feedback_type,
|
||||
'created_time': str(obj.created_time),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
async def detail_yifan_feedback_service(cls, auth: AuthSchema, id: int) -> dict:
|
||||
"""详情"""
|
||||
obj = await YifanFeedbackCRUD(auth).get_by_id_yifan_feedback_crud(id=id)
|
||||
if not obj:
|
||||
raise CustomException(msg="该数据不存在")
|
||||
return YifanFeedbackOutSchema.model_validate(obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def list_yifan_feedback_service(cls, auth: AuthSchema, search: YifanFeedbackQueryParam | None = None, order_by: list[dict] | None = None) -> list[dict]:
|
||||
"""列表查询"""
|
||||
search_dict = search.__dict__ if search else None
|
||||
obj_list = await YifanFeedbackCRUD(auth).list_yifan_feedback_crud(search=search_dict, order_by=order_by)
|
||||
return [YifanFeedbackOutSchema.model_validate(obj).model_dump() for obj in obj_list]
|
||||
|
||||
@classmethod
|
||||
async def page_yifan_feedback_service(cls, auth: AuthSchema, page_no: int, page_size: int, search: YifanFeedbackQueryParam | None = None, order_by: list[dict] | None = None) -> dict:
|
||||
"""分页查询(数据库分页)"""
|
||||
search_dict = search.__dict__ if search else {}
|
||||
order_by_list = order_by or [{'id': 'asc'}]
|
||||
offset = (page_no - 1) * page_size
|
||||
result = await YifanFeedbackCRUD(auth).page_yifan_feedback_crud(
|
||||
offset=offset,
|
||||
limit=page_size,
|
||||
order_by=order_by_list,
|
||||
search=search_dict
|
||||
)
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
async def create_yifan_feedback_service(cls, auth: AuthSchema, data: YifanFeedbackCreateSchema) -> dict:
|
||||
"""创建"""
|
||||
# 检查唯一性约束
|
||||
obj = await YifanFeedbackCRUD(auth).create_yifan_feedback_crud(data=data)
|
||||
return YifanFeedbackOutSchema.model_validate(obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def update_yifan_feedback_service(cls, auth: AuthSchema, id: int, data: YifanFeedbackUpdateSchema) -> dict:
|
||||
"""更新"""
|
||||
# 检查数据是否存在
|
||||
obj = await YifanFeedbackCRUD(auth).get_by_id_yifan_feedback_crud(id=id)
|
||||
if not obj:
|
||||
raise CustomException(msg='更新失败,该数据不存在')
|
||||
|
||||
# 检查唯一性约束
|
||||
|
||||
obj = await YifanFeedbackCRUD(auth).update_yifan_feedback_crud(id=id, data=data)
|
||||
return YifanFeedbackOutSchema.model_validate(obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def delete_yifan_feedback_service(cls, auth: AuthSchema, ids: list[int]) -> None:
|
||||
"""删除"""
|
||||
if len(ids) < 1:
|
||||
raise CustomException(msg='删除失败,删除对象不能为空')
|
||||
for id in ids:
|
||||
obj = await YifanFeedbackCRUD(auth).get_by_id_yifan_feedback_crud(id=id)
|
||||
if not obj:
|
||||
raise CustomException(msg=f'删除失败,ID为{id}的数据不存在')
|
||||
await YifanFeedbackCRUD(auth).delete_yifan_feedback_crud(ids=ids)
|
||||
|
||||
@classmethod
|
||||
async def set_available_yifan_feedback_service(cls, auth: AuthSchema, data: BatchSetAvailable) -> None:
|
||||
"""批量设置状态"""
|
||||
await YifanFeedbackCRUD(auth).set_available_yifan_feedback_crud(ids=data.ids, status=data.status)
|
||||
|
||||
@classmethod
|
||||
async def batch_export_yifan_feedback_service(cls, obj_list: list[dict]) -> bytes:
|
||||
"""批量导出"""
|
||||
mapping_dict = {
|
||||
'id': '主键ID',
|
||||
'created_time': '创建时间',
|
||||
'updated_time': '更新时间',
|
||||
'created_id': '创建人ID',
|
||||
'updated_id': '更新人ID',
|
||||
'is_deleted': '是否删除(0否 1是)',
|
||||
'status': '状态(0禁用 1启用)',
|
||||
'user_id': '用户ID',
|
||||
'content': '反馈内容',
|
||||
'images': '图片URL(多个用逗号分隔)',
|
||||
'contact': '联系方式',
|
||||
'feedback_type': '反馈类型(suggestion:建议 bug:问题 complaint:投诉 other:其他)',
|
||||
'handle_status': '处理状态(0待处理 1处理中 2已处理 3已关闭)',
|
||||
'handle_result': '处理结果',
|
||||
'handle_time': '处理时间',
|
||||
'handler_id': '处理人ID',
|
||||
'remark': '备注',
|
||||
'updated_id': '更新者ID',
|
||||
}
|
||||
|
||||
data = obj_list.copy()
|
||||
for item in data:
|
||||
# 状态转换
|
||||
if 'status' in item:
|
||||
item['status'] = '启用' if item.get('status') == '0' else '停用'
|
||||
# 创建者转换
|
||||
creator_info = item.get('creator')
|
||||
if isinstance(creator_info, dict):
|
||||
item['creator'] = creator_info.get('name', '未知')
|
||||
elif creator_info is None:
|
||||
item['creator'] = '未知'
|
||||
|
||||
return ExcelUtil.export_list2excel(list_data=data, mapping_dict=mapping_dict)
|
||||
|
||||
@classmethod
|
||||
async def batch_import_yifan_feedback_service(cls, auth: AuthSchema, file: UploadFile, update_support: bool = False) -> str:
|
||||
"""批量导入"""
|
||||
header_dict = {
|
||||
'主键ID': 'id',
|
||||
'创建时间': 'created_time',
|
||||
'更新时间': 'updated_time',
|
||||
'创建人ID': 'created_id',
|
||||
'更新人ID': 'updated_id',
|
||||
'是否删除(0否 1是)': 'is_deleted',
|
||||
'状态(0禁用 1启用)': 'status',
|
||||
'用户ID': 'user_id',
|
||||
'反馈内容': 'content',
|
||||
'图片URL(多个用逗号分隔)': 'images',
|
||||
'联系方式': 'contact',
|
||||
'反馈类型(suggestion:建议 bug:问题 complaint:投诉 other:其他)': 'feedback_type',
|
||||
'处理状态(0待处理 1处理中 2已处理 3已关闭)': 'handle_status',
|
||||
'处理结果': 'handle_result',
|
||||
'处理时间': 'handle_time',
|
||||
'处理人ID': 'handler_id',
|
||||
'备注': 'remark',
|
||||
}
|
||||
|
||||
try:
|
||||
contents = await file.read()
|
||||
df = pd.read_excel(io.BytesIO(contents))
|
||||
await file.close()
|
||||
|
||||
if df.empty:
|
||||
raise CustomException(msg="导入文件为空")
|
||||
|
||||
missing_headers = [header for header in header_dict.keys() if header not in df.columns]
|
||||
if missing_headers:
|
||||
raise CustomException(msg=f"导入文件缺少必要的列: {', '.join(missing_headers)}")
|
||||
|
||||
df.rename(columns=header_dict, inplace=True)
|
||||
|
||||
# 验证必填字段
|
||||
|
||||
error_msgs = []
|
||||
success_count = 0
|
||||
count = 0
|
||||
|
||||
for index, row in df.iterrows():
|
||||
count += 1
|
||||
try:
|
||||
data = {
|
||||
"id": row['id'],
|
||||
"created_time": row['created_time'],
|
||||
"updated_time": row['updated_time'],
|
||||
"created_id": row['created_id'],
|
||||
"updated_id": row['updated_id'],
|
||||
"is_deleted": row['is_deleted'],
|
||||
"status": row['status'],
|
||||
"user_id": row['user_id'],
|
||||
"content": row['content'],
|
||||
"images": row['images'],
|
||||
"contact": row['contact'],
|
||||
"feedback_type": row['feedback_type'],
|
||||
"handle_status": row['handle_status'],
|
||||
"handle_result": row['handle_result'],
|
||||
"handle_time": row['handle_time'],
|
||||
"handler_id": row['handler_id'],
|
||||
"remark": row['remark'],
|
||||
}
|
||||
# 使用CreateSchema做校验后入库
|
||||
create_schema = YifanFeedbackCreateSchema.model_validate(data)
|
||||
|
||||
# 检查唯一性约束
|
||||
|
||||
await YifanFeedbackCRUD(auth).create_yifan_feedback_crud(data=create_schema)
|
||||
success_count += 1
|
||||
except Exception as e:
|
||||
error_msgs.append(f"第{count}行: {str(e)}")
|
||||
continue
|
||||
|
||||
result = f"成功导入 {success_count} 条数据"
|
||||
if error_msgs:
|
||||
result += "\n错误信息:\n" + "\n".join(error_msgs)
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
log.error(f"批量导入失败: {str(e)}")
|
||||
raise CustomException(msg=f"导入失败: {str(e)}")
|
||||
|
||||
@classmethod
|
||||
async def import_template_download_yifan_feedback_service(cls) -> bytes:
|
||||
"""下载导入模板"""
|
||||
header_list = [
|
||||
'主键ID',
|
||||
'创建时间',
|
||||
'更新时间',
|
||||
'创建人ID',
|
||||
'更新人ID',
|
||||
'是否删除(0否 1是)',
|
||||
'状态(0禁用 1启用)',
|
||||
'用户ID',
|
||||
'反馈内容',
|
||||
'图片URL(多个用逗号分隔)',
|
||||
'联系方式',
|
||||
'反馈类型(suggestion:建议 bug:问题 complaint:投诉 other:其他)',
|
||||
'处理状态(0待处理 1处理中 2已处理 3已关闭)',
|
||||
'处理结果',
|
||||
'处理时间',
|
||||
'处理人ID',
|
||||
'备注',
|
||||
]
|
||||
selector_header_list = []
|
||||
option_list = []
|
||||
|
||||
# 添加下拉选项
|
||||
|
||||
return ExcelUtil.get_excel_template(
|
||||
header_list=header_list,
|
||||
selector_header_list=selector_header_list,
|
||||
option_list=option_list
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
@@ -0,0 +1,179 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from fastapi import APIRouter, Depends, UploadFile, Body, Path, Query
|
||||
from fastapi.responses import StreamingResponse, JSONResponse
|
||||
|
||||
from app.common.response import SuccessResponse, StreamResponse
|
||||
from app.core.dependencies import AuthPermission
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from app.core.base_params import PaginationQueryParam
|
||||
from app.utils.common_util import bytes2file_response
|
||||
from app.core.logger import log
|
||||
from app.core.base_schema import BatchSetAvailable
|
||||
|
||||
from .service import YifanNamingFavoritesService
|
||||
from .schema import YifanNamingFavoritesCreateSchema, YifanNamingFavoritesUpdateSchema, YifanNamingFavoritesQueryParam, YifanNamingFavoriteSchema, YifanNamingUnfavoriteSchema, MyFavoritesQueryParam
|
||||
|
||||
YifanNamingFavoritesRouter = APIRouter(prefix='/yifan_naming_favorites', tags=["命名方案收藏模块"])
|
||||
|
||||
|
||||
@YifanNamingFavoritesRouter.get("/my_favorites", summary="获取我的收藏列表", description="获取当前用户的收藏列表,支持按分类筛选")
|
||||
async def get_my_favorites_controller(
|
||||
page_no: int = Query(1, description="页码"),
|
||||
page_size: int = Query(10, description="每页数量"),
|
||||
search: MyFavoritesQueryParam = Depends(),
|
||||
auth: AuthSchema = Depends(AuthPermission([]))
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
获取我的收藏列表接口
|
||||
|
||||
返回数据格式:
|
||||
- total: 总数
|
||||
- items: 收藏列表
|
||||
- id: 收藏ID
|
||||
- solution_id: 方案ID
|
||||
- category: 分类(personal:个人名 company:商号)
|
||||
- name: 名字
|
||||
- pinyin: 拼音
|
||||
- tags: 标签列表
|
||||
- created_time: 收藏时间
|
||||
"""
|
||||
result = await YifanNamingFavoritesService.get_my_favorites_service(
|
||||
auth=auth,
|
||||
category=search.category,
|
||||
page_no=page_no,
|
||||
page_size=page_size
|
||||
)
|
||||
log.info(f"获取我的收藏列表成功, 用户ID: {auth.user.id}")
|
||||
return SuccessResponse(data=result, msg="获取收藏列表成功")
|
||||
|
||||
@YifanNamingFavoritesRouter.get("/detail/{id}", summary="获取命名方案收藏详情", description="获取命名方案收藏详情")
|
||||
async def get_yifan_naming_favorites_detail_controller(
|
||||
id: int = Path(..., description="ID"),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_naming_favorites:query"]))
|
||||
) -> JSONResponse:
|
||||
"""获取命名方案收藏详情接口"""
|
||||
result_dict = await YifanNamingFavoritesService.detail_yifan_naming_favorites_service(auth=auth, id=id)
|
||||
log.info(f"获取命名方案收藏详情成功 {id}")
|
||||
return SuccessResponse(data=result_dict, msg="获取命名方案收藏详情成功")
|
||||
|
||||
@YifanNamingFavoritesRouter.get("/list", summary="查询命名方案收藏列表", description="查询命名方案收藏列表")
|
||||
async def get_yifan_naming_favorites_list_controller(
|
||||
page: PaginationQueryParam = Depends(),
|
||||
search: YifanNamingFavoritesQueryParam = Depends(),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_naming_favorites:query"]))
|
||||
) -> JSONResponse:
|
||||
"""查询命名方案收藏列表接口(数据库分页)"""
|
||||
result_dict = await YifanNamingFavoritesService.page_yifan_naming_favorites_service(
|
||||
auth=auth,
|
||||
page_no=page.page_no if page.page_no is not None else 1,
|
||||
page_size=page.page_size if page.page_size is not None else 10,
|
||||
search=search,
|
||||
order_by=page.order_by
|
||||
)
|
||||
log.info("查询命名方案收藏列表成功")
|
||||
return SuccessResponse(data=result_dict, msg="查询命名方案收藏列表成功")
|
||||
|
||||
@YifanNamingFavoritesRouter.post("/create", summary="创建命名方案收藏", description="创建命名方案收藏")
|
||||
async def create_yifan_naming_favorites_controller(
|
||||
data: YifanNamingFavoritesCreateSchema,
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_naming_favorites:create"]))
|
||||
) -> JSONResponse:
|
||||
"""创建命名方案收藏接口"""
|
||||
result_dict = await YifanNamingFavoritesService.create_yifan_naming_favorites_service(auth=auth, data=data)
|
||||
log.info("创建命名方案收藏成功")
|
||||
return SuccessResponse(data=result_dict, msg="创建命名方案收藏成功")
|
||||
|
||||
@YifanNamingFavoritesRouter.put("/update/{id}", summary="修改命名方案收藏", description="修改命名方案收藏")
|
||||
async def update_yifan_naming_favorites_controller(
|
||||
data: YifanNamingFavoritesUpdateSchema,
|
||||
id: int = Path(..., description="ID"),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_naming_favorites:update"]))
|
||||
) -> JSONResponse:
|
||||
"""修改命名方案收藏接口"""
|
||||
result_dict = await YifanNamingFavoritesService.update_yifan_naming_favorites_service(auth=auth, id=id, data=data)
|
||||
log.info("修改命名方案收藏成功")
|
||||
return SuccessResponse(data=result_dict, msg="修改命名方案收藏成功")
|
||||
|
||||
@YifanNamingFavoritesRouter.delete("/delete", summary="删除命名方案收藏", description="删除命名方案收藏")
|
||||
async def delete_yifan_naming_favorites_controller(
|
||||
ids: list[int] = Body(..., description="ID列表"),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_naming_favorites:delete"]))
|
||||
) -> JSONResponse:
|
||||
"""删除命名方案收藏接口"""
|
||||
await YifanNamingFavoritesService.delete_yifan_naming_favorites_service(auth=auth, ids=ids)
|
||||
log.info(f"删除命名方案收藏成功: {ids}")
|
||||
return SuccessResponse(msg="删除命名方案收藏成功")
|
||||
|
||||
@YifanNamingFavoritesRouter.patch("/available/setting", summary="批量修改命名方案收藏状态", description="批量修改命名方案收藏状态")
|
||||
async def batch_set_available_yifan_naming_favorites_controller(
|
||||
data: BatchSetAvailable,
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_naming_favorites:patch"]))
|
||||
) -> JSONResponse:
|
||||
"""批量修改命名方案收藏状态接口"""
|
||||
await YifanNamingFavoritesService.set_available_yifan_naming_favorites_service(auth=auth, data=data)
|
||||
log.info(f"批量修改命名方案收藏状态成功: {data.ids}")
|
||||
return SuccessResponse(msg="批量修改命名方案收藏状态成功")
|
||||
|
||||
@YifanNamingFavoritesRouter.post('/export', summary="导出命名方案收藏", description="导出命名方案收藏")
|
||||
async def export_yifan_naming_favorites_list_controller(
|
||||
search: YifanNamingFavoritesQueryParam = Depends(),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_naming_favorites:export"]))
|
||||
) -> StreamingResponse:
|
||||
"""导出命名方案收藏接口"""
|
||||
result_dict_list = await YifanNamingFavoritesService.list_yifan_naming_favorites_service(search=search, auth=auth)
|
||||
export_result = await YifanNamingFavoritesService.batch_export_yifan_naming_favorites_service(obj_list=result_dict_list)
|
||||
log.info('导出命名方案收藏成功')
|
||||
|
||||
return StreamResponse(
|
||||
data=bytes2file_response(export_result),
|
||||
media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
headers={
|
||||
'Content-Disposition': 'attachment; filename=yifan_naming_favorites.xlsx'
|
||||
}
|
||||
)
|
||||
|
||||
@YifanNamingFavoritesRouter.post('/import', summary="导入命名方案收藏", description="导入命名方案收藏")
|
||||
async def import_yifan_naming_favorites_list_controller(
|
||||
file: UploadFile,
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_naming_favorites:import"]))
|
||||
) -> JSONResponse:
|
||||
"""导入命名方案收藏接口"""
|
||||
batch_import_result = await YifanNamingFavoritesService.batch_import_yifan_naming_favorites_service(file=file, auth=auth, update_support=True)
|
||||
log.info("导入命名方案收藏成功")
|
||||
|
||||
return SuccessResponse(data=batch_import_result, msg="导入命名方案收藏成功")
|
||||
|
||||
@YifanNamingFavoritesRouter.post('/download/template', summary="获取命名方案收藏导入模板", description="获取命名方案收藏导入模板", dependencies=[Depends(AuthPermission(["module_yifan:yifan_naming_favorites:download"]))])
|
||||
async def export_yifan_naming_favorites_template_controller() -> StreamingResponse:
|
||||
"""获取命名方案收藏导入模板接口"""
|
||||
import_template_result = await YifanNamingFavoritesService.import_template_download_yifan_naming_favorites_service()
|
||||
log.info('获取命名方案收藏导入模板成功')
|
||||
|
||||
return StreamResponse(
|
||||
data=bytes2file_response(import_template_result),
|
||||
media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
headers={'Content-Disposition': 'attachment; filename=yifan_naming_favorites_template.xlsx'}
|
||||
)
|
||||
|
||||
|
||||
@YifanNamingFavoritesRouter.post("/favorite", summary="收藏命名方案", description="收藏命名方案")
|
||||
async def add_favorite_controller(
|
||||
data: YifanNamingFavoriteSchema,
|
||||
auth: AuthSchema = Depends(AuthPermission([]))
|
||||
) -> JSONResponse:
|
||||
"""收藏命名方案接口"""
|
||||
result_dict = await YifanNamingFavoritesService.add_favorite_service(auth=auth, data=data)
|
||||
log.info(f"收藏命名方案成功: solution_id={data.solution_id}")
|
||||
return SuccessResponse(data=result_dict, msg="收藏成功")
|
||||
|
||||
|
||||
@YifanNamingFavoritesRouter.post("/unfavorite", summary="取消收藏命名方案", description="取消收藏命名方案")
|
||||
async def remove_favorite_controller(
|
||||
data: YifanNamingUnfavoriteSchema,
|
||||
auth: AuthSchema = Depends(AuthPermission([]))
|
||||
) -> JSONResponse:
|
||||
"""取消收藏命名方案接口"""
|
||||
await YifanNamingFavoritesService.remove_favorite_service(auth=auth, data=data)
|
||||
log.info(f"取消收藏命名方案成功: solution_id={data.solution_id}")
|
||||
return SuccessResponse(msg="取消收藏成功")
|
||||
@@ -0,0 +1,212 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from typing import Sequence
|
||||
|
||||
from app.core.base_crud import CRUDBase
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from .model import YifanNamingFavoritesModel
|
||||
from .schema import YifanNamingFavoritesCreateSchema, YifanNamingFavoritesUpdateSchema, YifanNamingFavoritesOutSchema
|
||||
|
||||
|
||||
class YifanNamingFavoritesCRUD(CRUDBase[YifanNamingFavoritesModel, YifanNamingFavoritesCreateSchema, YifanNamingFavoritesUpdateSchema]):
|
||||
"""命名方案收藏数据层"""
|
||||
|
||||
def __init__(self, auth: AuthSchema) -> None:
|
||||
"""
|
||||
初始化CRUD数据层
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
"""
|
||||
super().__init__(model=YifanNamingFavoritesModel, auth=auth)
|
||||
|
||||
async def get_by_id_yifan_naming_favorites_crud(self, id: int, preload: list | None = None) -> YifanNamingFavoritesModel | None:
|
||||
"""
|
||||
详情
|
||||
|
||||
参数:
|
||||
- id (int): 对象ID
|
||||
- preload (list | None): 预加载关系,未提供时使用模型默认项
|
||||
|
||||
返回:
|
||||
- YifanNamingFavoritesModel | None: 模型实例或None
|
||||
"""
|
||||
return await self.get(id=id, preload=preload)
|
||||
|
||||
async def list_yifan_naming_favorites_crud(self, search: dict | None = None, order_by: list[dict] | None = None, preload: list | None = None) -> Sequence[YifanNamingFavoritesModel]:
|
||||
"""
|
||||
列表查询
|
||||
|
||||
参数:
|
||||
- search (dict | None): 查询参数
|
||||
- order_by (list[dict] | None): 排序参数,未提供时使用模型默认项
|
||||
- preload (list | None): 预加载关系,未提供时使用模型默认项
|
||||
|
||||
返回:
|
||||
- Sequence[YifanNamingFavoritesModel]: 模型实例序列
|
||||
"""
|
||||
return await self.list(search=search, order_by=order_by, preload=preload)
|
||||
|
||||
async def create_yifan_naming_favorites_crud(self, data: YifanNamingFavoritesCreateSchema) -> YifanNamingFavoritesModel | None:
|
||||
"""
|
||||
创建
|
||||
|
||||
参数:
|
||||
- data (YifanNamingFavoritesCreateSchema): 创建模型
|
||||
|
||||
返回:
|
||||
- YifanNamingFavoritesModel | None: 模型实例或None
|
||||
"""
|
||||
return await self.create(data=data)
|
||||
|
||||
async def update_yifan_naming_favorites_crud(self, id: int, data: YifanNamingFavoritesUpdateSchema) -> YifanNamingFavoritesModel | None:
|
||||
"""
|
||||
更新
|
||||
|
||||
参数:
|
||||
- id (int): 对象ID
|
||||
- data (YifanNamingFavoritesUpdateSchema): 更新模型
|
||||
|
||||
返回:
|
||||
- YifanNamingFavoritesModel | None: 模型实例或None
|
||||
"""
|
||||
return await self.update(id=id, data=data)
|
||||
|
||||
async def delete_yifan_naming_favorites_crud(self, ids: list[int]) -> None:
|
||||
"""
|
||||
批量删除
|
||||
|
||||
参数:
|
||||
- ids (list[int]): 对象ID列表
|
||||
|
||||
返回:
|
||||
- None
|
||||
"""
|
||||
return await self.delete(ids=ids)
|
||||
|
||||
async def set_available_yifan_naming_favorites_crud(self, ids: list[int], status: str) -> None:
|
||||
"""
|
||||
批量设置可用状态
|
||||
|
||||
参数:
|
||||
- ids (list[int]): 对象ID列表
|
||||
- status (str): 可用状态
|
||||
|
||||
返回:
|
||||
- None
|
||||
"""
|
||||
return await self.set(ids=ids, status=status)
|
||||
|
||||
async def page_yifan_naming_favorites_crud(self, offset: int, limit: int, order_by: list[dict] | None = None, search: dict | None = None, preload: list | None = None) -> dict:
|
||||
"""
|
||||
分页查询
|
||||
|
||||
参数:
|
||||
- offset (int): 偏移量
|
||||
- limit (int): 每页数量
|
||||
- order_by (list[dict] | None): 排序参数,未提供时使用模型默认项
|
||||
- search (dict | None): 查询参数,未提供时查询所有
|
||||
- preload (list | None): 预加载关系,未提供时使用模型默认项
|
||||
|
||||
返回:
|
||||
- Dict: 分页数据
|
||||
"""
|
||||
order_by_list = order_by or [{'id': 'asc'}]
|
||||
search_dict = search or {}
|
||||
return await self.page(
|
||||
offset=offset,
|
||||
limit=limit,
|
||||
order_by=order_by_list,
|
||||
search=search_dict,
|
||||
out_schema=YifanNamingFavoritesOutSchema,
|
||||
preload=preload
|
||||
)
|
||||
|
||||
async def get_by_condition_yifan_naming_favorites_crud(self, user_id: int, solution_id: int, category: str | None = None) -> YifanNamingFavoritesModel | None:
|
||||
"""
|
||||
根据条件查询收藏记录
|
||||
|
||||
参数:
|
||||
- user_id (int): 用户ID
|
||||
- solution_id (int): 方案ID
|
||||
- category (str | None): 分类(可选)
|
||||
|
||||
返回:
|
||||
- YifanNamingFavoritesModel | None: 模型实例或None
|
||||
"""
|
||||
search = {
|
||||
'user_id': user_id,
|
||||
'solution_id': solution_id,
|
||||
'is_deleted': 0
|
||||
}
|
||||
if category:
|
||||
search['category'] = category
|
||||
result = await self.list(search=search)
|
||||
return result[0] if result else None
|
||||
|
||||
async def get_my_favorites_crud(self, user_id: int, category: str | None = None, page_no: int = 1, page_size: int = 10) -> dict:
|
||||
"""
|
||||
获取我的收藏列表(关联方案信息)
|
||||
|
||||
参数:
|
||||
- user_id (int): 用户ID
|
||||
- category (str | None): 分类筛选
|
||||
- page_no (int): 页码
|
||||
- page_size (int): 每页数量
|
||||
|
||||
返回:
|
||||
- dict: 分页数据
|
||||
"""
|
||||
from sqlalchemy import select, func
|
||||
from sqlalchemy.orm import Session
|
||||
from app.api.v1.module_yifan.yifan_naming_solutions.model import YifanNamingSolutionsModel
|
||||
|
||||
# 构建查询条件
|
||||
conditions = [
|
||||
YifanNamingFavoritesModel.user_id == user_id,
|
||||
YifanNamingFavoritesModel.is_deleted == 0
|
||||
]
|
||||
if category:
|
||||
conditions.append(YifanNamingFavoritesModel.category == category)
|
||||
|
||||
# 查询总数
|
||||
count_stmt = select(func.count()).select_from(YifanNamingFavoritesModel).where(*conditions)
|
||||
total = await self.auth.db.scalar(count_stmt)
|
||||
|
||||
# 分页查询收藏记录,关联方案信息
|
||||
offset = (page_no - 1) * page_size
|
||||
stmt = (
|
||||
select(
|
||||
YifanNamingFavoritesModel.id,
|
||||
YifanNamingFavoritesModel.solution_id,
|
||||
YifanNamingFavoritesModel.category,
|
||||
YifanNamingFavoritesModel.created_time,
|
||||
YifanNamingSolutionsModel.name,
|
||||
YifanNamingSolutionsModel.pinyin,
|
||||
YifanNamingSolutionsModel.tags
|
||||
)
|
||||
.join(YifanNamingSolutionsModel, YifanNamingFavoritesModel.solution_id == YifanNamingSolutionsModel.id)
|
||||
.where(*conditions)
|
||||
.order_by(YifanNamingFavoritesModel.created_time.desc())
|
||||
.offset(offset)
|
||||
.limit(page_size)
|
||||
)
|
||||
result = await self.auth.db.execute(stmt)
|
||||
rows = result.all()
|
||||
|
||||
items = []
|
||||
for row in rows:
|
||||
items.append({
|
||||
'id': row.id,
|
||||
'solution_id': row.solution_id,
|
||||
'category': row.category,
|
||||
'created_time': row.created_time.strftime('%Y-%m-%d') if row.created_time else '',
|
||||
'name': row.name,
|
||||
'pinyin': row.pinyin,
|
||||
'tags': row.tags or []
|
||||
})
|
||||
|
||||
return {
|
||||
'total': total or 0,
|
||||
'items': items
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from sqlalchemy import DateTime, Integer, String, SmallInteger, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.base_model import ModelMixin, UserMixin
|
||||
|
||||
|
||||
class YifanNamingFavoritesModel(ModelMixin, UserMixin):
|
||||
"""
|
||||
命名方案收藏表
|
||||
"""
|
||||
__tablename__: str = 'yifan_naming_favorites'
|
||||
__table_args__: dict[str, str] = {'comment': '命名方案收藏'}
|
||||
__loader_options__: list[str] = ["created_by", "updated_by"]
|
||||
|
||||
is_deleted: Mapped[int | None] = mapped_column(SmallInteger, nullable=True, comment='是否删除(0否 1是)')
|
||||
user_id: Mapped[int | None] = mapped_column(Integer, nullable=True, comment='用户ID')
|
||||
solution_id: Mapped[int | None] = mapped_column(Integer, nullable=True, comment='方案ID(关联yifan_naming_solutions表)')
|
||||
category: Mapped[str | None] = mapped_column(String(20), nullable=True, comment='分类(personal:个人 company:商号)')
|
||||
remark: Mapped[str | None] = mapped_column(String(255), nullable=True, comment='备注')
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from fastapi import Query
|
||||
from typing import Optional
|
||||
|
||||
from app.core.validator import DateTimeStr
|
||||
from app.core.base_schema import BaseSchema, UserBySchema
|
||||
|
||||
|
||||
class MyFavoriteItemSchema(BaseModel):
|
||||
"""
|
||||
我的收藏列表项
|
||||
"""
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int = Field(description='收藏ID')
|
||||
solution_id: int = Field(description='方案ID')
|
||||
category: str = Field(description='分类(personal:个人名 company:商号)')
|
||||
name: str = Field(description='名字')
|
||||
pinyin: Optional[str] = Field(default=None, description='拼音')
|
||||
tags: Optional[list] = Field(default=None, description='标签列表')
|
||||
created_time: str = Field(description='收藏时间')
|
||||
|
||||
|
||||
class MyFavoritesListSchema(BaseModel):
|
||||
"""
|
||||
我的收藏列表响应
|
||||
"""
|
||||
total: int = Field(description='总数')
|
||||
items: list[MyFavoriteItemSchema] = Field(description='收藏列表')
|
||||
|
||||
|
||||
class MyFavoritesQueryParam:
|
||||
"""我的收藏查询参数"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
category: str | None = Query(None, description="分类筛选(personal:个人名 company:商号,不传则查全部)"),
|
||||
) -> None:
|
||||
self.category = category
|
||||
|
||||
|
||||
class YifanNamingFavoritesCreateSchema(BaseModel):
|
||||
"""
|
||||
命名方案收藏新增模型
|
||||
"""
|
||||
is_deleted: int = Field(default=..., description='是否删除(0否 1是)')
|
||||
user_id: int = Field(default=..., description='用户ID')
|
||||
solution_id: int = Field(default=..., description='方案ID(关联yifan_naming_solutions表)')
|
||||
category: str = Field(default=..., description='分类(personal:个人 company:商号)')
|
||||
remark: str = Field(default=..., description='备注')
|
||||
description: str | None = Field(default=None, max_length=255, description='备注/描述')
|
||||
|
||||
|
||||
class YifanNamingFavoriteSchema(BaseModel):
|
||||
"""
|
||||
收藏请求模型
|
||||
"""
|
||||
solution_id: int = Field(default=..., description='方案ID(关联yifan_naming_solutions表)')
|
||||
category: str = Field(default=..., description='分类(personal:个人 company:商号)')
|
||||
|
||||
|
||||
class YifanNamingUnfavoriteSchema(BaseModel):
|
||||
"""
|
||||
取消收藏请求模型
|
||||
"""
|
||||
solution_id: int = Field(default=..., description='方案ID(关联yifan_naming_solutions表)')
|
||||
|
||||
|
||||
class YifanNamingFavoritesUpdateSchema(YifanNamingFavoritesCreateSchema):
|
||||
"""
|
||||
命名方案收藏更新模型
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
class YifanNamingFavoritesOutSchema(YifanNamingFavoritesCreateSchema, BaseSchema, UserBySchema):
|
||||
"""
|
||||
命名方案收藏响应模型
|
||||
"""
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class YifanNamingFavoritesQueryParam:
|
||||
"""命名方案收藏查询参数"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
category: str | None = Query(None, description="分类(personal:个人 company:商号)"),
|
||||
remark: str | None = Query(None, description="备注"),
|
||||
created_id: int | None = Query(None, description="创建人ID"),
|
||||
updated_id: int | None = Query(None, description="更新人ID"),
|
||||
is_deleted: int | None = Query(None, description="是否删除(0否 1是)"),
|
||||
user_id: int | None = Query(None, description="用户ID"),
|
||||
solution_id: int | None = Query(None, description="方案ID(关联yifan_naming_solutions表)"),
|
||||
created_time: list[DateTimeStr] | None = Query(None, description="创建时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]),
|
||||
updated_time: list[DateTimeStr] | None = Query(None, description="更新时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]),
|
||||
|
||||
) -> None:
|
||||
|
||||
# 精确查询字段
|
||||
self.created_id = created_id
|
||||
# 精确查询字段
|
||||
self.updated_id = updated_id
|
||||
# 精确查询字段
|
||||
self.is_deleted = is_deleted
|
||||
# 精确查询字段
|
||||
self.user_id = user_id
|
||||
# 精确查询字段
|
||||
self.solution_id = solution_id
|
||||
# 模糊查询字段
|
||||
self.category = ("like", category)
|
||||
# 模糊查询字段
|
||||
self.remark = ("like", remark)
|
||||
# 时间范围查询
|
||||
if created_time and len(created_time) == 2:
|
||||
self.created_time = ("between", (created_time[0], created_time[1]))
|
||||
if updated_time and len(updated_time) == 2:
|
||||
self.updated_time = ("between", (updated_time[0], updated_time[1]))
|
||||
@@ -0,0 +1,278 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import io
|
||||
from fastapi import UploadFile
|
||||
import pandas as pd
|
||||
|
||||
from app.core.base_schema import BatchSetAvailable
|
||||
from app.core.exceptions import CustomException
|
||||
from app.utils.excel_util import ExcelUtil
|
||||
from app.core.logger import log
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from .schema import YifanNamingFavoritesCreateSchema, YifanNamingFavoritesUpdateSchema, YifanNamingFavoritesOutSchema, YifanNamingFavoritesQueryParam, YifanNamingFavoriteSchema, YifanNamingUnfavoriteSchema
|
||||
from .crud import YifanNamingFavoritesCRUD
|
||||
|
||||
|
||||
class YifanNamingFavoritesService:
|
||||
"""
|
||||
命名方案收藏服务层
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
async def detail_yifan_naming_favorites_service(cls, auth: AuthSchema, id: int) -> dict:
|
||||
"""详情"""
|
||||
obj = await YifanNamingFavoritesCRUD(auth).get_by_id_yifan_naming_favorites_crud(id=id)
|
||||
if not obj:
|
||||
raise CustomException(msg="该数据不存在")
|
||||
return YifanNamingFavoritesOutSchema.model_validate(obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def list_yifan_naming_favorites_service(cls, auth: AuthSchema, search: YifanNamingFavoritesQueryParam | None = None, order_by: list[dict] | None = None) -> list[dict]:
|
||||
"""列表查询"""
|
||||
search_dict = search.__dict__ if search else None
|
||||
obj_list = await YifanNamingFavoritesCRUD(auth).list_yifan_naming_favorites_crud(search=search_dict, order_by=order_by)
|
||||
return [YifanNamingFavoritesOutSchema.model_validate(obj).model_dump() for obj in obj_list]
|
||||
|
||||
@classmethod
|
||||
async def page_yifan_naming_favorites_service(cls, auth: AuthSchema, page_no: int, page_size: int, search: YifanNamingFavoritesQueryParam | None = None, order_by: list[dict] | None = None) -> dict:
|
||||
"""分页查询(数据库分页)"""
|
||||
search_dict = search.__dict__ if search else {}
|
||||
order_by_list = order_by or [{'id': 'asc'}]
|
||||
offset = (page_no - 1) * page_size
|
||||
result = await YifanNamingFavoritesCRUD(auth).page_yifan_naming_favorites_crud(
|
||||
offset=offset,
|
||||
limit=page_size,
|
||||
order_by=order_by_list,
|
||||
search=search_dict
|
||||
)
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
async def create_yifan_naming_favorites_service(cls, auth: AuthSchema, data: YifanNamingFavoritesCreateSchema) -> dict:
|
||||
"""创建"""
|
||||
# 检查唯一性约束
|
||||
obj = await YifanNamingFavoritesCRUD(auth).create_yifan_naming_favorites_crud(data=data)
|
||||
return YifanNamingFavoritesOutSchema.model_validate(obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def update_yifan_naming_favorites_service(cls, auth: AuthSchema, id: int, data: YifanNamingFavoritesUpdateSchema) -> dict:
|
||||
"""更新"""
|
||||
# 检查数据是否存在
|
||||
obj = await YifanNamingFavoritesCRUD(auth).get_by_id_yifan_naming_favorites_crud(id=id)
|
||||
if not obj:
|
||||
raise CustomException(msg='更新失败,该数据不存在')
|
||||
|
||||
# 检查唯一性约束
|
||||
|
||||
obj = await YifanNamingFavoritesCRUD(auth).update_yifan_naming_favorites_crud(id=id, data=data)
|
||||
return YifanNamingFavoritesOutSchema.model_validate(obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def delete_yifan_naming_favorites_service(cls, auth: AuthSchema, ids: list[int]) -> None:
|
||||
"""删除"""
|
||||
if len(ids) < 1:
|
||||
raise CustomException(msg='删除失败,删除对象不能为空')
|
||||
for id in ids:
|
||||
obj = await YifanNamingFavoritesCRUD(auth).get_by_id_yifan_naming_favorites_crud(id=id)
|
||||
if not obj:
|
||||
raise CustomException(msg=f'删除失败,ID为{id}的数据不存在')
|
||||
await YifanNamingFavoritesCRUD(auth).delete_yifan_naming_favorites_crud(ids=ids)
|
||||
|
||||
@classmethod
|
||||
async def set_available_yifan_naming_favorites_service(cls, auth: AuthSchema, data: BatchSetAvailable) -> None:
|
||||
"""批量设置状态"""
|
||||
await YifanNamingFavoritesCRUD(auth).set_available_yifan_naming_favorites_crud(ids=data.ids, status=data.status)
|
||||
|
||||
@classmethod
|
||||
async def batch_export_yifan_naming_favorites_service(cls, obj_list: list[dict]) -> bytes:
|
||||
"""批量导出"""
|
||||
mapping_dict = {
|
||||
'id': '主键ID',
|
||||
'created_time': '创建时间',
|
||||
'updated_time': '更新时间',
|
||||
'created_id': '创建人ID',
|
||||
'updated_id': '更新人ID',
|
||||
'is_deleted': '是否删除(0否 1是)',
|
||||
'user_id': '用户ID',
|
||||
'solution_id': '方案ID(关联yifan_naming_solutions表)',
|
||||
'category': '分类(personal:个人 company:商号)',
|
||||
'remark': '备注',
|
||||
'uuid': 'UUID全局唯一标识',
|
||||
'description': '备注/描述',
|
||||
'updated_id': '更新者ID',
|
||||
}
|
||||
|
||||
data = obj_list.copy()
|
||||
for item in data:
|
||||
# 状态转换
|
||||
if 'status' in item:
|
||||
item['status'] = '启用' if item.get('status') == '0' else '停用'
|
||||
# 创建者转换
|
||||
creator_info = item.get('creator')
|
||||
if isinstance(creator_info, dict):
|
||||
item['creator'] = creator_info.get('name', '未知')
|
||||
elif creator_info is None:
|
||||
item['creator'] = '未知'
|
||||
|
||||
return ExcelUtil.export_list2excel(list_data=data, mapping_dict=mapping_dict)
|
||||
|
||||
@classmethod
|
||||
async def batch_import_yifan_naming_favorites_service(cls, auth: AuthSchema, file: UploadFile, update_support: bool = False) -> str:
|
||||
"""批量导入"""
|
||||
header_dict = {
|
||||
'主键ID': 'id',
|
||||
'创建时间': 'created_time',
|
||||
'更新时间': 'updated_time',
|
||||
'创建人ID': 'created_id',
|
||||
'更新人ID': 'updated_id',
|
||||
'是否删除(0否 1是)': 'is_deleted',
|
||||
'用户ID': 'user_id',
|
||||
'方案ID(关联yifan_naming_solutions表)': 'solution_id',
|
||||
'分类(personal:个人 company:商号)': 'category',
|
||||
'备注': 'remark',
|
||||
'UUID全局唯一标识': 'uuid',
|
||||
'备注/描述': 'description',
|
||||
}
|
||||
|
||||
try:
|
||||
contents = await file.read()
|
||||
df = pd.read_excel(io.BytesIO(contents))
|
||||
await file.close()
|
||||
|
||||
if df.empty:
|
||||
raise CustomException(msg="导入文件为空")
|
||||
|
||||
missing_headers = [header for header in header_dict.keys() if header not in df.columns]
|
||||
if missing_headers:
|
||||
raise CustomException(msg=f"导入文件缺少必要的列: {', '.join(missing_headers)}")
|
||||
|
||||
df.rename(columns=header_dict, inplace=True)
|
||||
|
||||
# 验证必填字段
|
||||
|
||||
error_msgs = []
|
||||
success_count = 0
|
||||
count = 0
|
||||
|
||||
for index, row in df.iterrows():
|
||||
count += 1
|
||||
try:
|
||||
data = {
|
||||
"id": row['id'],
|
||||
"created_time": row['created_time'],
|
||||
"updated_time": row['updated_time'],
|
||||
"created_id": row['created_id'],
|
||||
"updated_id": row['updated_id'],
|
||||
"is_deleted": row['is_deleted'],
|
||||
"user_id": row['user_id'],
|
||||
"solution_id": row['solution_id'],
|
||||
"category": row['category'],
|
||||
"remark": row['remark'],
|
||||
"uuid": row['uuid'],
|
||||
"description": row['description'],
|
||||
}
|
||||
# 使用CreateSchema做校验后入库
|
||||
create_schema = YifanNamingFavoritesCreateSchema.model_validate(data)
|
||||
|
||||
# 检查唯一性约束
|
||||
|
||||
await YifanNamingFavoritesCRUD(auth).create_yifan_naming_favorites_crud(data=create_schema)
|
||||
success_count += 1
|
||||
except Exception as e:
|
||||
error_msgs.append(f"第{count}行: {str(e)}")
|
||||
continue
|
||||
|
||||
result = f"成功导入 {success_count} 条数据"
|
||||
if error_msgs:
|
||||
result += "\n错误信息:\n" + "\n".join(error_msgs)
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
log.error(f"批量导入失败: {str(e)}")
|
||||
raise CustomException(msg=f"导入失败: {str(e)}")
|
||||
|
||||
@classmethod
|
||||
async def import_template_download_yifan_naming_favorites_service(cls) -> bytes:
|
||||
"""下载导入模板"""
|
||||
header_list = [
|
||||
'主键ID',
|
||||
'创建时间',
|
||||
'更新时间',
|
||||
'创建人ID',
|
||||
'更新人ID',
|
||||
'是否删除(0否 1是)',
|
||||
'用户ID',
|
||||
'方案ID(关联yifan_naming_solutions表)',
|
||||
'分类(personal:个人 company:商号)',
|
||||
'备注',
|
||||
'UUID全局唯一标识',
|
||||
'备注/描述',
|
||||
]
|
||||
selector_header_list = []
|
||||
option_list = []
|
||||
|
||||
# 添加下拉选项
|
||||
|
||||
return ExcelUtil.get_excel_template(
|
||||
header_list=header_list,
|
||||
selector_header_list=selector_header_list,
|
||||
option_list=option_list
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def add_favorite_service(cls, auth: AuthSchema, data: YifanNamingFavoriteSchema) -> dict:
|
||||
"""添加收藏"""
|
||||
# 检查是否已收藏
|
||||
existing = await YifanNamingFavoritesCRUD(auth).get_by_condition_yifan_naming_favorites_crud(
|
||||
user_id=auth.user.id,
|
||||
solution_id=data.solution_id,
|
||||
category=data.category
|
||||
)
|
||||
if existing:
|
||||
raise CustomException(msg="该方案已收藏")
|
||||
|
||||
# 创建收藏记录
|
||||
create_data = YifanNamingFavoritesCreateSchema(
|
||||
is_deleted=0,
|
||||
user_id=auth.user.id,
|
||||
solution_id=data.solution_id,
|
||||
category=data.category,
|
||||
remark=""
|
||||
)
|
||||
obj = await YifanNamingFavoritesCRUD(auth).create_yifan_naming_favorites_crud(data=create_data)
|
||||
return YifanNamingFavoritesOutSchema.model_validate(obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def remove_favorite_service(cls, auth: AuthSchema, data: YifanNamingUnfavoriteSchema) -> None:
|
||||
"""取消收藏"""
|
||||
# 查找收藏记录
|
||||
existing = await YifanNamingFavoritesCRUD(auth).get_by_condition_yifan_naming_favorites_crud(
|
||||
user_id=auth.user.id,
|
||||
solution_id=data.solution_id
|
||||
)
|
||||
if not existing:
|
||||
raise CustomException(msg="该方案未收藏")
|
||||
|
||||
# 删除收藏记录
|
||||
await YifanNamingFavoritesCRUD(auth).delete_yifan_naming_favorites_crud(ids=[existing.id])
|
||||
|
||||
@classmethod
|
||||
async def get_my_favorites_service(cls, auth: AuthSchema, category: str | None = None, page_no: int = 1, page_size: int = 10) -> dict:
|
||||
"""
|
||||
获取我的收藏列表
|
||||
|
||||
参数:
|
||||
- auth: 认证信息
|
||||
- category: 分类筛选(personal/company)
|
||||
- page_no: 页码
|
||||
- page_size: 每页数量
|
||||
|
||||
返回:
|
||||
- dict: 包含total和items的分页数据
|
||||
"""
|
||||
return await YifanNamingFavoritesCRUD(auth).get_my_favorites_crud(
|
||||
user_id=auth.user.id,
|
||||
category=category,
|
||||
page_no=page_no,
|
||||
page_size=page_size
|
||||
)
|
||||
@@ -0,0 +1,223 @@
|
||||
# 公司起名接口使用说明
|
||||
|
||||
## 接口地址
|
||||
`POST /api/v1/yifan/yifan_naming_reports/company-naming`
|
||||
|
||||
## 功能说明
|
||||
该接口用于公司起名服务,会:
|
||||
1. 保存创始人和公司的基本信息到 `yifan_naming_reports` 表
|
||||
2. 调用 AI 模型(enterprise_naming)生成起名建议
|
||||
3. 解析 AI 响应并保存到以下表:
|
||||
- `yifan_naming_solutions` - 保存推荐的公司名称方案
|
||||
- `yifan_solution_sections` - 保存各个板块的摘要数据
|
||||
- `yifan_section_details` - 保存板块的详细内容
|
||||
4. 返回报告ID和AI完整响应
|
||||
|
||||
## 数据流转
|
||||
|
||||
```
|
||||
用户输入
|
||||
↓
|
||||
yifan_naming_reports (报告表)
|
||||
↓
|
||||
AI 处理
|
||||
↓
|
||||
yifan_naming_solutions (方案表) - 多个推荐名称
|
||||
↓
|
||||
yifan_solution_sections (板块表) - 每个方案的板块
|
||||
↓
|
||||
yifan_section_details (详情表) - 每个板块的详细内容
|
||||
```
|
||||
|
||||
## 请求参数
|
||||
|
||||
```json
|
||||
{
|
||||
"industry": "科技、互联网",
|
||||
"address": "北京",
|
||||
"founder_name": "张三",
|
||||
"founder_birthday": "1990-01-01 08:00:00",
|
||||
"founder_birth_time": "辰时",
|
||||
"founder_gender": "male",
|
||||
"core_members": "[{\"name\":\"李四\",\"birthday\":\"1992-05-15\"}]",
|
||||
"vision": "成为行业领先的科技公司",
|
||||
"preference": "亲和温暖",
|
||||
"user_id": 123
|
||||
}
|
||||
```
|
||||
|
||||
### 参数说明
|
||||
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| industry | string | 是 | 所属行业与主营业务 |
|
||||
| address | string | 是 | 注册地 |
|
||||
| founder_name | string | 是 | 创始人姓名 |
|
||||
| founder_birthday | datetime | 是 | 创始人出生日期(格式:YYYY-MM-DD HH:mm:ss) |
|
||||
| founder_birth_time | string | 否 | 出生时辰 |
|
||||
| founder_gender | string | 是 | 性别(male:男 female:女) |
|
||||
| core_members | string | 否 | 核心成员信息(JSON格式字符串) |
|
||||
| vision | string | 否 | 企业愿景/品牌调性 |
|
||||
| preference | string | 否 | 您希望名字传递的感觉 |
|
||||
| user_id | int | 否 | 用户ID |
|
||||
|
||||
## 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "公司起名完成",
|
||||
"data": {
|
||||
"report_id": 1001,
|
||||
"ai_response": "根据您提供的信息,为您推荐以下公司名称:\n\n1. 【鼎新科技】\n寓意:鼎,象征权威与稳固;新,代表创新与发展...\n\n2. 【云启智联】\n寓意:云,代表云计算与互联网;启,寓意开启新篇章..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## AI 输入格式
|
||||
|
||||
接口会将用户输入的信息格式化为以下文本,传递给 AI 模型:
|
||||
|
||||
```
|
||||
创始人:张三
|
||||
出生日期:1990年01月01日
|
||||
出生时辰:辰时
|
||||
性别:男
|
||||
所属行业与主营业务:科技、互联网
|
||||
企业愿景/品牌调性:成为行业领先的科技公司
|
||||
您希望名字传递的感觉:亲和温暖
|
||||
注册地:北京
|
||||
```
|
||||
|
||||
## 前端调用示例
|
||||
|
||||
```typescript
|
||||
import YifanNamingAPI from "@/api/module_yifan/yifan_naming";
|
||||
|
||||
// 调用公司起名接口
|
||||
const response = await YifanNamingAPI.companyNaming({
|
||||
industry: "科技、互联网",
|
||||
address: "北京",
|
||||
founder_name: "张三",
|
||||
founder_birthday: "1990-01-01 08:00:00",
|
||||
founder_birth_time: "辰时",
|
||||
founder_gender: "male",
|
||||
vision: "成为行业领先的科技公司",
|
||||
preference: "亲和温暖"
|
||||
});
|
||||
|
||||
console.log("报告ID:", response.data.report_id);
|
||||
console.log("AI响应:", response.data.ai_response);
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 该接口无需登录即可调用
|
||||
2. 出生日期必须包含时间部分(HH:mm:ss)
|
||||
3. AI 响应内容会完整保存在 `ai_source_data` 字段中
|
||||
4. 返回的 `report_id` 可用于后续查询报告详情
|
||||
|
||||
|
||||
## AI 响应解析
|
||||
|
||||
接口会尝试解析 AI 返回的内容,支持以下格式:
|
||||
|
||||
### 格式1:JSON 格式
|
||||
```json
|
||||
{
|
||||
"solutions": [
|
||||
{
|
||||
"name": "鼎新科技",
|
||||
"pinyin": "ding xin ke ji",
|
||||
"total_score": 95,
|
||||
"star_rating": 5,
|
||||
"wuxing": "火土",
|
||||
"tags": ["创新", "稳重"],
|
||||
"name_meaning": "鼎,象征权威与稳固...",
|
||||
"poetry_source": "出自《诗经》...",
|
||||
"sections": [
|
||||
{
|
||||
"section_type": "ditiantai",
|
||||
"title": "地天泰",
|
||||
"summary_data": {...},
|
||||
"details": [...]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 格式2:文本格式
|
||||
```
|
||||
1. 【鼎新科技】
|
||||
拼音:ding xin ke ji
|
||||
寓意:鼎,象征权威与稳固;新,代表创新与发展...
|
||||
五行:火土
|
||||
诗词出处:出自《诗经》...
|
||||
|
||||
2. 【云启智联】
|
||||
拼音:yun qi zhi lian
|
||||
寓意:云,代表云计算与互联网...
|
||||
```
|
||||
|
||||
如果 AI 返回的格式无法解析,系统会创建一个默认方案,将完整响应保存在 `name_meaning` 字段中。
|
||||
|
||||
## 板块类型说明
|
||||
|
||||
`section_type` 支持以下类型:
|
||||
- `ditiantai` - 地天泰
|
||||
- `kaiyun_jingnang` - 开运锦囊
|
||||
- `shici` - 诗词出处
|
||||
- `wuxing` - 五行分布
|
||||
- `liuyao` - 六维格局
|
||||
- `jiazu` - 家族起名
|
||||
- `bihua` - 笔画数理
|
||||
- `ziyi_shengxiao` - 字义生肖
|
||||
- `zonghe_pingfen` - 综合评分
|
||||
|
||||
## 详情类型说明
|
||||
|
||||
`detail_type` 支持以下类型:
|
||||
- `life_guide` - 生活开运指南
|
||||
- `poetry_base` - 诗词底蕴
|
||||
- `hexagram_meaning` - 卦象释义
|
||||
- `depth_analysis` - 深度字义解读
|
||||
- `zodiac_analysis` - 生肖喜忌分析
|
||||
- `master_comment` - 大师总评
|
||||
|
||||
## 数据库表关系
|
||||
|
||||
```sql
|
||||
-- 1对多关系
|
||||
yifan_naming_reports (1) → yifan_naming_solutions (N)
|
||||
|
||||
-- 1对多关系
|
||||
yifan_naming_solutions (1) → yifan_solution_sections (N)
|
||||
|
||||
-- 1对多关系
|
||||
yifan_solution_sections (1) → yifan_section_details (N)
|
||||
```
|
||||
|
||||
## 查询示例
|
||||
|
||||
### 查询报告的所有方案
|
||||
```python
|
||||
solutions = await YifanNamingSolutionsCRUD(auth).get_list_crud(
|
||||
search={'report_id': report_id}
|
||||
)
|
||||
```
|
||||
|
||||
### 查询方案的所有板块
|
||||
```python
|
||||
sections = await YifanSolutionSectionsCRUD(auth).get_list_crud(
|
||||
search={'solution_id': solution_id}
|
||||
)
|
||||
```
|
||||
|
||||
### 查询板块的所有详情
|
||||
```python
|
||||
details = await YifanSectionDetailsCRUD(auth).get_list_crud(
|
||||
search={'section_id': section_id}
|
||||
)
|
||||
```
|
||||
@@ -0,0 +1 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
@@ -0,0 +1,312 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from fastapi import APIRouter, Depends, UploadFile, Body, Path, Query
|
||||
from fastapi.responses import StreamingResponse, JSONResponse
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.common.response import SuccessResponse, StreamResponse
|
||||
from app.core.dependencies import AuthPermission, db_getter
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from app.api.v1.module_system.notice.crud import NoticeCRUD
|
||||
from app.core.base_params import PaginationQueryParam
|
||||
from app.utils.common_util import bytes2file_response
|
||||
from app.core.logger import log
|
||||
from app.core.base_schema import BatchSetAvailable
|
||||
|
||||
from .service import YifanNamingReportsService
|
||||
from .schema import YifanNamingReportsCreateSchema, YifanNamingReportsUpdateSchema, YifanNamingReportsQueryParam, FortuneTellingRequestSchema, PersonalScoringTrialRequestSchema, CompanyNamingRequestSchema, PersonalNamingRequestSchema, PersonalRenamingRequestSchema, CompanyRenamingRequestSchema, CompanyScoringRequestSchema
|
||||
|
||||
YifanNamingReportsRouter = APIRouter(prefix='/yifan_naming_reports', tags=["命名服务案例模块"])
|
||||
|
||||
|
||||
@YifanNamingReportsRouter.get("/my_reports", summary="获取我的方案列表", description="获取当前用户的方案列表")
|
||||
async def get_my_reports_controller(
|
||||
page_no: int = Query(1, description="页码"),
|
||||
page_size: int = Query(10, description="每页数量"),
|
||||
auth: AuthSchema = Depends(AuthPermission([]))
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
获取我的方案列表接口
|
||||
|
||||
返回数据格式:
|
||||
- total: 总数
|
||||
- items: 方案列表
|
||||
- id: 报告ID
|
||||
- title: 标题
|
||||
- subtitle: 副标题/描述
|
||||
- category: 分类(personal:个人 company:公司)
|
||||
- service_type: 服务类型
|
||||
- has_solutions: 是否已生成方案
|
||||
- created_time: 创建时间
|
||||
- relative_time: 相对时间
|
||||
"""
|
||||
result = await YifanNamingReportsService.get_my_reports_service(
|
||||
auth=auth,
|
||||
page_no=page_no,
|
||||
page_size=page_size
|
||||
)
|
||||
log.info(f"获取我的方案列表成功, 用户ID: {auth.user.id}")
|
||||
return SuccessResponse(data=result, msg="获取方案列表成功")
|
||||
|
||||
|
||||
@YifanNamingReportsRouter.get("/{report_id}/solutions", summary="获取报告方案列表", description="根据报告ID获取方案卡片列表(小程序展示)")
|
||||
async def get_report_solutions_controller(
|
||||
report_id: int = Path(..., description="报告ID"),
|
||||
auth: AuthSchema = Depends(AuthPermission([], check_data_scope=False))
|
||||
) -> JSONResponse:
|
||||
result = await YifanNamingReportsService.get_report_solutions_service(auth=auth, report_id=report_id)
|
||||
log.info(f"获取报告方案列表成功, report_id: {report_id}")
|
||||
return SuccessResponse(data=result, msg="获取报告方案列表成功")
|
||||
|
||||
@YifanNamingReportsRouter.get("/admin/{report_id}/solutions", summary="管理后台获取报告方案列表", description="管理后台根据报告ID获取方案列表")
|
||||
async def admin_get_report_solutions_controller(
|
||||
report_id: int = Path(..., description="报告ID"),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_naming_reports:query"]))
|
||||
) -> JSONResponse:
|
||||
result = await YifanNamingReportsService.admin_get_report_solutions_service(auth=auth, report_id=report_id)
|
||||
log.info(f"管理后台获取报告方案列表成功, report_id: {report_id}")
|
||||
return SuccessResponse(data=result, msg="获取报告方案列表成功")
|
||||
|
||||
@YifanNamingReportsRouter.get("/detail/{id}", summary="获取命名服务案例详情", description="获取命名服务案例详情")
|
||||
async def get_yifan_naming_reports_detail_controller(
|
||||
id: int = Path(..., description="ID"),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_naming_reports:query"]))
|
||||
) -> JSONResponse:
|
||||
"""获取命名服务案例详情接口"""
|
||||
result_dict = await YifanNamingReportsService.detail_yifan_naming_reports_service(auth=auth, id=id)
|
||||
log.info(f"获取命名服务案例详情成功 {id}")
|
||||
return SuccessResponse(data=result_dict, msg="获取命名服务案例详情成功")
|
||||
|
||||
@YifanNamingReportsRouter.get("/list", summary="查询命名服务案例列表", description="查询命名服务案例列表")
|
||||
async def get_yifan_naming_reports_list_controller(
|
||||
page: PaginationQueryParam = Depends(),
|
||||
search: YifanNamingReportsQueryParam = Depends(),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_naming_reports:query"]))
|
||||
) -> JSONResponse:
|
||||
"""查询命名服务案例列表接口(数据库分页)"""
|
||||
result_dict = await YifanNamingReportsService.page_yifan_naming_reports_service(
|
||||
auth=auth,
|
||||
page_no=page.page_no if page.page_no is not None else 1,
|
||||
page_size=page.page_size if page.page_size is not None else 10,
|
||||
search=search,
|
||||
order_by=page.order_by
|
||||
)
|
||||
log.info("查询命名服务案例列表成功")
|
||||
return SuccessResponse(data=result_dict, msg="查询命名服务案例列表成功")
|
||||
|
||||
@YifanNamingReportsRouter.post("/create", summary="创建命名服务案例", description="创建命名服务案例")
|
||||
async def create_yifan_naming_reports_controller(
|
||||
data: YifanNamingReportsCreateSchema,
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_naming_reports:create"]))
|
||||
) -> JSONResponse:
|
||||
"""创建命名服务案例接口"""
|
||||
result_dict = await YifanNamingReportsService.create_yifan_naming_reports_service(auth=auth, data=data)
|
||||
log.info("创建命名服务案例成功")
|
||||
return SuccessResponse(data=result_dict, msg="创建命名服务案例成功")
|
||||
|
||||
@YifanNamingReportsRouter.put("/update/{id}", summary="修改命名服务案例", description="修改命名服务案例")
|
||||
async def update_yifan_naming_reports_controller(
|
||||
data: YifanNamingReportsUpdateSchema,
|
||||
id: int = Path(..., description="ID"),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_naming_reports:update"]))
|
||||
) -> JSONResponse:
|
||||
"""修改命名服务案例接口"""
|
||||
result_dict = await YifanNamingReportsService.update_yifan_naming_reports_service(auth=auth, id=id, data=data)
|
||||
log.info("修改命名服务案例成功")
|
||||
return SuccessResponse(data=result_dict, msg="修改命名服务案例成功")
|
||||
|
||||
@YifanNamingReportsRouter.delete("/delete", summary="删除命名服务案例", description="删除命名服务案例")
|
||||
async def delete_yifan_naming_reports_controller(
|
||||
ids: list[int] = Body(..., description="ID列表"),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_naming_reports:delete"]))
|
||||
) -> JSONResponse:
|
||||
"""删除命名服务案例接口"""
|
||||
await YifanNamingReportsService.delete_yifan_naming_reports_service(auth=auth, ids=ids)
|
||||
log.info(f"删除命名服务案例成功: {ids}")
|
||||
return SuccessResponse(msg="删除命名服务案例成功")
|
||||
|
||||
@YifanNamingReportsRouter.patch("/available/setting", summary="批量修改命名服务案例状态", description="批量修改命名服务案例状态")
|
||||
async def batch_set_available_yifan_naming_reports_controller(
|
||||
data: BatchSetAvailable,
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_naming_reports:patch"]))
|
||||
) -> JSONResponse:
|
||||
"""批量修改命名服务案例状态接口"""
|
||||
await YifanNamingReportsService.set_available_yifan_naming_reports_service(auth=auth, data=data)
|
||||
log.info(f"批量修改命名服务案例状态成功: {data.ids}")
|
||||
return SuccessResponse(msg="批量修改命名服务案例状态成功")
|
||||
|
||||
@YifanNamingReportsRouter.post('/export', summary="导出命名服务案例", description="导出命名服务案例")
|
||||
async def export_yifan_naming_reports_list_controller(
|
||||
search: YifanNamingReportsQueryParam = Depends(),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_naming_reports:export"]))
|
||||
) -> StreamingResponse:
|
||||
"""导出命名服务案例接口"""
|
||||
result_dict_list = await YifanNamingReportsService.list_yifan_naming_reports_service(search=search, auth=auth)
|
||||
export_result = await YifanNamingReportsService.batch_export_yifan_naming_reports_service(obj_list=result_dict_list)
|
||||
log.info('导出命名服务案例成功')
|
||||
|
||||
return StreamResponse(
|
||||
data=bytes2file_response(export_result),
|
||||
media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
headers={
|
||||
'Content-Disposition': 'attachment; filename=yifan_naming_reports.xlsx'
|
||||
}
|
||||
)
|
||||
|
||||
@YifanNamingReportsRouter.post('/import', summary="导入命名服务案例", description="导入命名服务案例")
|
||||
async def import_yifan_naming_reports_list_controller(
|
||||
file: UploadFile,
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_naming_reports:import"]))
|
||||
) -> JSONResponse:
|
||||
"""导入命名服务案例接口"""
|
||||
batch_import_result = await YifanNamingReportsService.batch_import_yifan_naming_reports_service(file=file, auth=auth, update_support=True)
|
||||
log.info("导入命名服务案例成功")
|
||||
|
||||
return SuccessResponse(data=batch_import_result, msg="导入命名服务案例成功")
|
||||
|
||||
@YifanNamingReportsRouter.post('/download/template', summary="获取命名服务案例导入模板", description="获取命名服务案例导入模板", dependencies=[Depends(AuthPermission(["module_yifan:yifan_naming_reports:download"]))])
|
||||
async def export_yifan_naming_reports_template_controller() -> StreamingResponse:
|
||||
"""获取命名服务案例导入模板接口"""
|
||||
import_template_result = await YifanNamingReportsService.import_template_download_yifan_naming_reports_service()
|
||||
log.info('获取命名服务案例导入模板成功')
|
||||
|
||||
return StreamResponse(
|
||||
data=bytes2file_response(import_template_result),
|
||||
media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
headers={'Content-Disposition': 'attachment; filename=yifan_naming_reports_template.xlsx'}
|
||||
)
|
||||
|
||||
|
||||
@YifanNamingReportsRouter.post("/fortune-telling", summary="测名服务", description="八字测名,生成分析报告(无需登录)")
|
||||
async def fortune_telling_controller(
|
||||
data: FortuneTellingRequestSchema,
|
||||
db: AsyncSession = Depends(db_getter)
|
||||
) -> JSONResponse:
|
||||
"""测名服务接口 - 保存基本信息并生成分析报告"""
|
||||
auth = AuthSchema(db=db, check_data_scope=False)
|
||||
result_dict = await YifanNamingReportsService.fortune_telling_service(auth=auth, data=data)
|
||||
log.info(f"测名服务成功: {data.surname}{data.given_name}")
|
||||
return SuccessResponse(data=result_dict, msg="测名分析完成")
|
||||
|
||||
|
||||
@YifanNamingReportsRouter.post("/personal-scoring", summary="个人测名服务(收费版)", description="个人测名(收费版),异步生成测名报告")
|
||||
async def personal_scoring_controller(
|
||||
data: PersonalScoringTrialRequestSchema,
|
||||
auth: AuthSchema = Depends(AuthPermission([]))
|
||||
) -> JSONResponse:
|
||||
"""个人测名(收费版)接口 - 创建任务,后台异步生成AI测名报告"""
|
||||
result_dict = await YifanNamingReportsService.personal_scoring_service(auth=auth, data=data)
|
||||
log.info(f"个人测名(收费)任务已创建: {data.surname}{data.given_name}")
|
||||
return SuccessResponse(data=result_dict, msg="测名任务已创建,正在测算中")
|
||||
|
||||
|
||||
@YifanNamingReportsRouter.post("/personal-scoring-trial", summary="个人测名服务(免费版)", description="个人测名(免费版),每用户首次免费,之后每周一次")
|
||||
async def personal_scoring_trial_controller(
|
||||
data: PersonalScoringTrialRequestSchema,
|
||||
auth: AuthSchema = Depends(AuthPermission([]))
|
||||
) -> JSONResponse:
|
||||
"""个人测名(免费版)接口"""
|
||||
result_dict = await YifanNamingReportsService.personal_scoring_trial_service(auth=auth, data=data)
|
||||
log.info(f"免费测名服务成功: {data.surname}{data.given_name}")
|
||||
return SuccessResponse(data=result_dict, msg="免费测名分析完成")
|
||||
|
||||
|
||||
@YifanNamingReportsRouter.post("/scoring-trial-share-callback", summary="分享回调-赠送免费测名次数", description="好友通过分享链接进入小程序后,调用此接口为分享者增加免费次数")
|
||||
async def scoring_trial_share_callback_controller(
|
||||
sharer_user_id: int = Body(..., embed=True, description="分享者用户ID"),
|
||||
db: AsyncSession = Depends(db_getter)
|
||||
) -> JSONResponse:
|
||||
"""分享回调接口 - 为分享者增加本周免费测名次数"""
|
||||
result = await YifanNamingReportsService.grant_share_trial_quota(sharer_user_id=sharer_user_id)
|
||||
return SuccessResponse(data=result, msg="分享奖励已发放")
|
||||
|
||||
|
||||
@YifanNamingReportsRouter.post("/company-scoring", summary="企业测名服务(收费版)", description="企业测名(收费版),异步生成测名报告")
|
||||
async def company_scoring_controller(
|
||||
data: CompanyScoringRequestSchema,
|
||||
auth: AuthSchema = Depends(AuthPermission([]))
|
||||
) -> JSONResponse:
|
||||
"""企业测名(收费版)接口 - 创建任务,后台异步生成AI测名报告"""
|
||||
result_dict = await YifanNamingReportsService.company_scoring_service(auth=auth, data=data)
|
||||
log.info(f"企业测名(收费)任务已创建: {data.company_name}")
|
||||
return SuccessResponse(data=result_dict, msg="测名任务已创建,正在测算中")
|
||||
|
||||
|
||||
@YifanNamingReportsRouter.post("/company-scoring-trial", summary="企业测名服务(免费版)", description="企业测名(免费版),每用户首次免费,之后每周一次")
|
||||
async def company_scoring_trial_controller(
|
||||
data: CompanyScoringRequestSchema,
|
||||
auth: AuthSchema = Depends(AuthPermission([]))
|
||||
) -> JSONResponse:
|
||||
"""企业测名(免费版)接口"""
|
||||
result_dict = await YifanNamingReportsService.company_scoring_trial_service(auth=auth, data=data)
|
||||
log.info(f"企业免费测名服务成功: {data.company_name}")
|
||||
return SuccessResponse(data=result_dict, msg="企业免费测名分析完成")
|
||||
|
||||
|
||||
@YifanNamingReportsRouter.post("/company-naming", summary="公司起名服务", description="公司起名,创建任务并异步生成起名建议")
|
||||
async def company_naming_controller(
|
||||
data: CompanyNamingRequestSchema,
|
||||
auth: AuthSchema = Depends(AuthPermission([]))
|
||||
) -> JSONResponse:
|
||||
"""公司起名服务接口 - 创建任务,后台异步生成起名建议"""
|
||||
result_dict = await YifanNamingReportsService.company_naming_service(auth=auth, data=data)
|
||||
founder_name = data.core_members[0].name if data.core_members else "未知"
|
||||
log.info(f"公司起名任务已创建: {founder_name} - {data.industry}")
|
||||
return SuccessResponse(data=result_dict, msg="起名任务已创建,正在测算中")
|
||||
|
||||
|
||||
@YifanNamingReportsRouter.post("/personal-naming", summary="个人起名服务", description="个人起名,创建任务并异步生成起名建议")
|
||||
async def personal_naming_controller(
|
||||
data: PersonalNamingRequestSchema,
|
||||
auth: AuthSchema = Depends(AuthPermission([]))
|
||||
) -> JSONResponse:
|
||||
"""个人起名服务接口 - 创建任务,后台异步生成起名建议"""
|
||||
result_dict = await YifanNamingReportsService.personal_naming_service(auth=auth, data=data)
|
||||
log.info(f"个人起名任务已创建: {data.last_name} - {data.style_label}")
|
||||
return SuccessResponse(data=result_dict, msg="起名任务已创建,正在测算中")
|
||||
|
||||
|
||||
@YifanNamingReportsRouter.post("/company-renaming", summary="企业改名服务", description="企业改名,创建任务并异步生成改名建议")
|
||||
async def company_renaming_controller(
|
||||
data: CompanyRenamingRequestSchema,
|
||||
auth: AuthSchema = Depends(AuthPermission([]))
|
||||
) -> JSONResponse:
|
||||
"""企业改名服务接口 - 创建任务,后台异步生成改名建议"""
|
||||
result_dict = await YifanNamingReportsService.company_renaming_service(auth=auth, data=data)
|
||||
founder_name = data.core_members[0].name if data.core_members else "未知"
|
||||
log.info(f"企业改名任务已创建: {founder_name} - {data.industry}")
|
||||
return SuccessResponse(data=result_dict, msg="改名任务已创建,正在测算中")
|
||||
|
||||
|
||||
@YifanNamingReportsRouter.post("/personal-renaming", summary="个人改名服务", description="个人改名,创建任务并异步生成改名建议")
|
||||
async def personal_renaming_controller(
|
||||
data: PersonalRenamingRequestSchema,
|
||||
auth: AuthSchema = Depends(AuthPermission([]))
|
||||
) -> JSONResponse:
|
||||
"""个人改名服务接口 - 创建任务,后台异步生成改名建议"""
|
||||
result_dict = await YifanNamingReportsService.personal_renaming_service(auth=auth, data=data)
|
||||
log.info(f"个人改名任务已创建: {data.last_name} - {data.original_name}")
|
||||
return SuccessResponse(data=result_dict, msg="改名任务已创建,正在测算中")
|
||||
|
||||
|
||||
@YifanNamingReportsRouter.get("/calendar", summary="获取当日黄历", description="获取当日黄历信息,包含农历、干支、宜忌等")
|
||||
async def get_calendar_info_controller(
|
||||
date: str | None = Query(None, description="日期(YYYY-MM-DD格式,不传则为当天)")
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
获取黄历信息接口(无需登录)
|
||||
|
||||
返回数据格式:
|
||||
- lunar_day: 农历日(如:十二)
|
||||
- lunar_month: 农历月(如:腊月)
|
||||
- lunar_year: 农历年(如:甲辰年)
|
||||
- solar_date: 公历日期(如:2024.01.22)
|
||||
- ganzhi_year: 干支年(如:甲辰)
|
||||
- zodiac: 生肖(如:龙)
|
||||
- yi: 宜(如:出行、开市)
|
||||
- ji: 忌(如:动土、安葬)
|
||||
- weekday: 星期
|
||||
"""
|
||||
result_dict = await YifanNamingReportsService.get_calendar_info_service(date=date)
|
||||
log.info(f"获取黄历信息成功: {date or '当天'}")
|
||||
return SuccessResponse(data=result_dict, msg="获取黄历信息成功")
|
||||
@@ -0,0 +1,324 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from typing import Sequence
|
||||
|
||||
from app.core.base_crud import CRUDBase
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from .model import YifanNamingReportsModel
|
||||
from .schema import YifanNamingReportsCreateSchema, YifanNamingReportsUpdateSchema, YifanNamingReportsOutSchema
|
||||
|
||||
|
||||
class YifanNamingReportsCRUD(CRUDBase[YifanNamingReportsModel, YifanNamingReportsCreateSchema, YifanNamingReportsUpdateSchema]):
|
||||
"""命名服务案例数据层"""
|
||||
|
||||
def __init__(self, auth: AuthSchema) -> None:
|
||||
"""
|
||||
初始化CRUD数据层
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
"""
|
||||
super().__init__(model=YifanNamingReportsModel, auth=auth)
|
||||
|
||||
async def get_by_id_yifan_naming_reports_crud(self, id: int, preload: list | None = None) -> YifanNamingReportsModel | None:
|
||||
"""
|
||||
详情
|
||||
|
||||
参数:
|
||||
- id (int): 对象ID
|
||||
- preload (list | None): 预加载关系,未提供时使用模型默认项
|
||||
|
||||
返回:
|
||||
- YifanNamingReportsModel | None: 模型实例或None
|
||||
"""
|
||||
return await self.get(id=id, preload=preload)
|
||||
|
||||
async def list_yifan_naming_reports_crud(self, search: dict | None = None, order_by: list[dict] | None = None, preload: list | None = None) -> Sequence[YifanNamingReportsModel]:
|
||||
"""
|
||||
列表查询
|
||||
|
||||
参数:
|
||||
- search (dict | None): 查询参数
|
||||
- order_by (list[dict] | None): 排序参数,未提供时使用模型默认项
|
||||
- preload (list | None): 预加载关系,未提供时使用模型默认项
|
||||
|
||||
返回:
|
||||
- Sequence[YifanNamingReportsModel]: 模型实例序列
|
||||
"""
|
||||
return await self.list(search=search, order_by=order_by, preload=preload)
|
||||
|
||||
async def create_yifan_naming_reports_crud(self, data: YifanNamingReportsCreateSchema) -> YifanNamingReportsModel | None:
|
||||
"""
|
||||
创建
|
||||
|
||||
参数:
|
||||
- data (YifanNamingReportsCreateSchema): 创建模型
|
||||
|
||||
返回:
|
||||
- YifanNamingReportsModel | None: 模型实例或None
|
||||
"""
|
||||
return await self.create(data=data)
|
||||
|
||||
async def update_yifan_naming_reports_crud(self, id: int, data: YifanNamingReportsUpdateSchema) -> YifanNamingReportsModel | None:
|
||||
"""
|
||||
更新
|
||||
|
||||
参数:
|
||||
- id (int): 对象ID
|
||||
- data (YifanNamingReportsUpdateSchema): 更新模型
|
||||
|
||||
返回:
|
||||
- YifanNamingReportsModel | None: 模型实例或None
|
||||
"""
|
||||
return await self.update(id=id, data=data)
|
||||
|
||||
async def delete_yifan_naming_reports_crud(self, ids: list[int]) -> None:
|
||||
"""
|
||||
批量删除
|
||||
|
||||
参数:
|
||||
- ids (list[int]): 对象ID列表
|
||||
|
||||
返回:
|
||||
- None
|
||||
"""
|
||||
return await self.delete(ids=ids)
|
||||
|
||||
async def set_available_yifan_naming_reports_crud(self, ids: list[int], status: str) -> None:
|
||||
"""
|
||||
批量设置可用状态
|
||||
|
||||
参数:
|
||||
- ids (list[int]): 对象ID列表
|
||||
- status (str): 可用状态
|
||||
|
||||
返回:
|
||||
- None
|
||||
"""
|
||||
return await self.set(ids=ids, status=status)
|
||||
|
||||
async def page_yifan_naming_reports_crud(self, offset: int, limit: int, order_by: list[dict] | None = None, search: dict | None = None, preload: list | None = None) -> dict:
|
||||
"""
|
||||
分页查询
|
||||
|
||||
参数:
|
||||
- offset (int): 偏移量
|
||||
- limit (int): 每页数量
|
||||
- order_by (list[dict] | None): 排序参数,未提供时使用模型默认项
|
||||
- search (dict | None): 查询参数,未提供时查询所有
|
||||
- preload (list | None): 预加载关系,未提供时使用模型默认项
|
||||
|
||||
返回:
|
||||
- Dict: 分页数据
|
||||
"""
|
||||
order_by_list = order_by or [{'id': 'asc'}]
|
||||
search_dict = search or {}
|
||||
return await self.page(
|
||||
offset=offset,
|
||||
limit=limit,
|
||||
order_by=order_by_list,
|
||||
search=search_dict,
|
||||
out_schema=YifanNamingReportsOutSchema,
|
||||
preload=preload
|
||||
)
|
||||
|
||||
async def find_existing_report_crud(
|
||||
self, surname: str, given_name: str, gender: str, birthday
|
||||
) -> 'YifanNamingReportsModel | None':
|
||||
"""按姓名+性别+生日查找已有的成功个人测名报告"""
|
||||
from sqlalchemy import select
|
||||
|
||||
stmt = (
|
||||
select(YifanNamingReportsModel)
|
||||
.where(
|
||||
YifanNamingReportsModel.surname == surname,
|
||||
YifanNamingReportsModel.given_name == given_name,
|
||||
YifanNamingReportsModel.gender == gender,
|
||||
YifanNamingReportsModel.birthday == birthday,
|
||||
YifanNamingReportsModel.status == 5,
|
||||
YifanNamingReportsModel.is_deleted == 0,
|
||||
YifanNamingReportsModel.category == "personal",
|
||||
)
|
||||
.order_by(YifanNamingReportsModel.created_time.desc())
|
||||
.limit(1)
|
||||
)
|
||||
result = await self.auth.db.execute(stmt)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
async def find_existing_company_report_crud(
|
||||
self, company_name: str, industry: str, birthday
|
||||
) -> 'YifanNamingReportsModel | None':
|
||||
"""按公司名称+行业+创始人生日查找已有的成功企业测名报告"""
|
||||
from sqlalchemy import select
|
||||
|
||||
stmt = (
|
||||
select(YifanNamingReportsModel)
|
||||
.where(
|
||||
YifanNamingReportsModel.given_name == company_name,
|
||||
YifanNamingReportsModel.industry == industry,
|
||||
YifanNamingReportsModel.birthday == birthday,
|
||||
YifanNamingReportsModel.status == 5,
|
||||
YifanNamingReportsModel.is_deleted == 0,
|
||||
YifanNamingReportsModel.category == "company",
|
||||
YifanNamingReportsModel.service_type == "fortune_telling",
|
||||
)
|
||||
.order_by(YifanNamingReportsModel.created_time.desc())
|
||||
.limit(1)
|
||||
)
|
||||
result = await self.auth.db.execute(stmt)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
async def get_my_reports_crud(self, user_id: int, page_no: int = 1, page_size: int = 10) -> dict:
|
||||
"""
|
||||
获取我的方案列表
|
||||
|
||||
参数:
|
||||
- user_id (int): 用户ID
|
||||
- page_no (int): 页码
|
||||
- page_size (int): 每页数量
|
||||
|
||||
返回:
|
||||
- dict: 分页数据
|
||||
"""
|
||||
from sqlalchemy import select, func, exists
|
||||
from app.api.v1.module_yifan.yifan_naming_solutions.model import YifanNamingSolutionsModel
|
||||
|
||||
# 构建查询条件
|
||||
conditions = [
|
||||
YifanNamingReportsModel.user_id == user_id,
|
||||
YifanNamingReportsModel.is_deleted == 0
|
||||
]
|
||||
|
||||
# 查询总数
|
||||
count_stmt = select(func.count()).select_from(YifanNamingReportsModel).where(*conditions)
|
||||
total = await self.auth.db.scalar(count_stmt)
|
||||
|
||||
# 分页查询
|
||||
offset = (page_no - 1) * page_size
|
||||
stmt = (
|
||||
select(YifanNamingReportsModel)
|
||||
.where(*conditions)
|
||||
.order_by(YifanNamingReportsModel.created_time.desc())
|
||||
.offset(offset)
|
||||
.limit(page_size)
|
||||
)
|
||||
result = await self.auth.db.execute(stmt)
|
||||
reports = result.scalars().all()
|
||||
|
||||
# 获取每个报告是否有方案
|
||||
report_ids = [r.id for r in reports]
|
||||
has_solutions_map = {}
|
||||
if report_ids:
|
||||
solutions_stmt = (
|
||||
select(YifanNamingSolutionsModel.report_id)
|
||||
.where(
|
||||
YifanNamingSolutionsModel.report_id.in_(report_ids),
|
||||
YifanNamingSolutionsModel.is_deleted == 0
|
||||
)
|
||||
.distinct()
|
||||
)
|
||||
solutions_result = await self.auth.db.execute(solutions_stmt)
|
||||
has_solutions_map = {r for r in solutions_result.scalars().all()}
|
||||
|
||||
items = []
|
||||
for report in reports:
|
||||
# 构建标题和副标题
|
||||
title, subtitle = self._build_report_display(report)
|
||||
|
||||
# 计算相对时间
|
||||
relative_time = self._get_relative_time(report.created_time)
|
||||
|
||||
items.append({
|
||||
'id': report.id,
|
||||
'title': title,
|
||||
'subtitle': subtitle,
|
||||
'category': report.category or '',
|
||||
'service_type': report.service_type or '',
|
||||
'has_solutions': report.id in has_solutions_map,
|
||||
'created_time': report.created_time.strftime('%Y-%m-%d %H:%M:%S') if report.created_time else '',
|
||||
'relative_time': relative_time
|
||||
})
|
||||
|
||||
return {
|
||||
'total': total or 0,
|
||||
'items': items
|
||||
}
|
||||
|
||||
def _build_report_display(self, report) -> tuple[str, str]:
|
||||
"""构建报告的标题和副标题"""
|
||||
category = report.category or ''
|
||||
service_type = report.service_type or ''
|
||||
|
||||
# 服务类型映射
|
||||
service_type_map = {
|
||||
'fortune_telling': '测名',
|
||||
'naming': '起名',
|
||||
'renaming': '改名'
|
||||
}
|
||||
service_name = service_type_map.get(service_type, '')
|
||||
|
||||
if category == 'personal':
|
||||
# 个人:李氏宝宝起名 / 个人改名
|
||||
surname = report.surname or ''
|
||||
if service_type == 'naming':
|
||||
title = f"{surname}氏宝宝起名" if surname else "宝宝起名"
|
||||
# 副标题:偏好 · 生日
|
||||
preference = report.preference or ''
|
||||
birthday = report.birthday.strftime('%Y-%m-%d') + '生' if report.birthday else ''
|
||||
subtitle = ' · '.join(filter(None, [preference, birthday]))
|
||||
elif service_type == 'renaming':
|
||||
title = "个人改名"
|
||||
# 副标题:原名:xxx
|
||||
original_name = report.original_name or ''
|
||||
subtitle = f"原名:{original_name}" if original_name else ''
|
||||
else:
|
||||
# 测名
|
||||
full_name = f"{report.surname or ''}{report.given_name or ''}"
|
||||
title = f"{full_name}测名" if full_name else "姓名测试"
|
||||
subtitle = ''
|
||||
else:
|
||||
# 公司
|
||||
if service_type == 'naming':
|
||||
title = f"{report.industry or ''}起名" if report.industry else "公司起名"
|
||||
# 副标题:行业 · 地址
|
||||
industry = report.industry or ''
|
||||
address = report.address or ''
|
||||
subtitle = ' · '.join(filter(None, [industry, address]))
|
||||
elif service_type == 'renaming':
|
||||
title = "公司改名"
|
||||
original_name = report.original_name or ''
|
||||
subtitle = f"原名:{original_name}" if original_name else ''
|
||||
else:
|
||||
title = "公司测名"
|
||||
subtitle = ''
|
||||
|
||||
return title, subtitle
|
||||
|
||||
def _get_relative_time(self, dt) -> str:
|
||||
"""计算相对时间"""
|
||||
import datetime
|
||||
if not dt:
|
||||
return ''
|
||||
|
||||
now = datetime.datetime.now()
|
||||
diff = now - dt
|
||||
|
||||
seconds = diff.total_seconds()
|
||||
if seconds < 60:
|
||||
return '刚刚'
|
||||
elif seconds < 3600:
|
||||
return f'{int(seconds // 60)}分钟前'
|
||||
elif seconds < 86400:
|
||||
return f'{int(seconds // 3600)}小时前'
|
||||
elif seconds < 604800:
|
||||
days = int(seconds // 86400)
|
||||
return f'{days}天前'
|
||||
elif seconds < 2592000:
|
||||
weeks = int(seconds // 604800)
|
||||
return f'{weeks}周前'
|
||||
elif seconds < 31536000:
|
||||
months = int(seconds // 2592000)
|
||||
return f'{months}个月前'
|
||||
else:
|
||||
years = int(seconds // 31536000)
|
||||
return f'{years}年前'
|
||||
@@ -0,0 +1,39 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import datetime
|
||||
from sqlalchemy import Date, DateTime, String, Text, Integer, SmallInteger
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.base_model import ModelMixin, UserMixin
|
||||
|
||||
|
||||
class YifanNamingReportsModel(ModelMixin, UserMixin):
|
||||
"""
|
||||
命名服务案例表
|
||||
"""
|
||||
__tablename__: str = 'yifan_naming_reports'
|
||||
__table_args__: dict[str, str] = {'comment': '命名服务案例'}
|
||||
__loader_options__: list[str] = ["created_by", "updated_by"]
|
||||
|
||||
is_deleted: Mapped[int | None] = mapped_column(SmallInteger, nullable=True, comment='是否删除(0:否 1:是)')
|
||||
category: Mapped[str | None] = mapped_column(String(20), nullable=True, comment='分类(personal:个人 company:公司)')
|
||||
service_type: Mapped[str | None] = mapped_column(String(20), nullable=True, comment='服务类型(fortune_telling:测名 naming:起名 renaming:改名)')
|
||||
surname: Mapped[str | None] = mapped_column(String(50), nullable=True, comment='姓氏')
|
||||
given_name: Mapped[str | None] = mapped_column(String(100), nullable=True, comment='名字')
|
||||
gender: Mapped[str | None] = mapped_column(String(10), nullable=True, comment='性别(male:男 female:女 unknown:未知)')
|
||||
birthday: Mapped[datetime.datetime | None] = mapped_column(DateTime, nullable=True, comment='生日')
|
||||
address: Mapped[str | None] = mapped_column(String(255), nullable=True, comment='地址')
|
||||
genealogy_character: Mapped[str | None] = mapped_column(String(50), nullable=True, comment='家谱字辈')
|
||||
father_name: Mapped[str | None] = mapped_column(String(100), nullable=True, comment='父亲姓名')
|
||||
father_birth_date: Mapped[datetime.date | None] = mapped_column(Date, nullable=True, comment='父亲出生日期')
|
||||
mother_name: Mapped[str | None] = mapped_column(String(100), nullable=True, comment='母亲姓名')
|
||||
mother_birth_date: Mapped[datetime.date | None] = mapped_column(Date, nullable=True, comment='母亲出生日期')
|
||||
preference: Mapped[str | None] = mapped_column(String(50), nullable=True, comment='偏好(字典值:elegant_classical:典雅古风 modern_simple:现代简约等)')
|
||||
original_name: Mapped[str | None] = mapped_column(String(100), nullable=True, comment='原名(改名时使用)')
|
||||
rename_reason: Mapped[str | None] = mapped_column(Text, nullable=True, comment='改名诉求')
|
||||
industry: Mapped[str | None] = mapped_column(String(100), nullable=True, comment='行业(公司命名时使用)')
|
||||
user_id: Mapped[int | None] = mapped_column(Integer, nullable=True, comment='用户id')
|
||||
core_members: Mapped[str | None] = mapped_column(String(1000), nullable=True, comment='公司核心成员')
|
||||
ai_source_data: Mapped[str | None] = mapped_column(Text, nullable=True, comment='AI原始数据')
|
||||
error_message: Mapped[str | None] = mapped_column(String(500), nullable=True, comment='错误信息')
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,336 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import datetime
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from fastapi import Query
|
||||
|
||||
from app.core.validator import DateTimeStr
|
||||
from app.core.base_schema import BaseSchema, UserBySchema
|
||||
|
||||
class YifanNamingReportsCreateSchema(BaseModel):
|
||||
"""
|
||||
命名服务案例新增模型
|
||||
"""
|
||||
is_deleted: int = Field(default=0, description='是否删除(0:否 1:是)')
|
||||
status: int = Field(default=1, description='状态(1:已创建 2:测算中 5:测算成功 3:测算超时 0:任务失败)')
|
||||
category: str = Field(default=..., description='分类(personal:个人 company:公司)')
|
||||
service_type: str = Field(default=..., description='服务类型(fortune_telling:测名 naming:起名 renaming:改名)')
|
||||
surname: str = Field(default=..., description='姓氏')
|
||||
given_name: str = Field(default=..., description='名字')
|
||||
gender: str = Field(default=..., description='性别(male:男 female:女 unknown:未知)')
|
||||
birthday: datetime.datetime = Field(default=..., description='生日')
|
||||
address: str = Field(default=..., description='地址')
|
||||
genealogy_character: str = Field(default=..., description='家谱字辈')
|
||||
father_name: str = Field(default=..., description='父亲姓名')
|
||||
father_birth_date: datetime.date | None = Field(default=None, description='父亲出生日期')
|
||||
mother_name: str = Field(default=..., description='母亲姓名')
|
||||
mother_birth_date: datetime.date | None = Field(default=None, description='母亲出生日期')
|
||||
preference: str = Field(default=..., description='偏好(字典值:elegant_classical:典雅古风 modern_simple:现代简约等)')
|
||||
original_name: str = Field(default=..., description='原名(改名时使用)')
|
||||
rename_reason: str = Field(default=..., description='改名诉求')
|
||||
industry: str = Field(default=..., description='行业(公司命名时使用)')
|
||||
user_id: int = Field(default=..., description='用户id')
|
||||
core_members: str = Field(default=..., description='公司核心成员')
|
||||
ai_source_data: str | None = Field(default=None, description='AI原始数据')
|
||||
error_message: str | None = Field(default=None, description='错误信息')
|
||||
|
||||
|
||||
class YifanNamingReportsUpdateSchema(YifanNamingReportsCreateSchema):
|
||||
"""
|
||||
命名服务案例更新模型
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
class YifanNamingReportsOutSchema(YifanNamingReportsCreateSchema, BaseSchema, UserBySchema):
|
||||
"""
|
||||
命名服务案例响应模型
|
||||
"""
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class YifanNamingReportsQueryParam:
|
||||
"""命名服务案例查询参数"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
category: str | None = Query(None, description="分类(personal:个人 company:公司)"),
|
||||
service_type: str | None = Query(None, description="服务类型(fortune_telling:测名 naming:起名 renaming:改名)"),
|
||||
surname: str | None = Query(None, description="姓氏"),
|
||||
given_name: str | None = Query(None, description="名字"),
|
||||
gender: str | None = Query(None, description="性别(male:男 female:女 unknown:未知)"),
|
||||
address: str | None = Query(None, description="地址"),
|
||||
genealogy_character: str | None = Query(None, description="家谱字辈"),
|
||||
father_name: str | None = Query(None, description="父亲姓名"),
|
||||
mother_name: str | None = Query(None, description="母亲姓名"),
|
||||
preference: str | None = Query(None, description="偏好(字典值:elegant_classical:典雅古风 modern_simple:现代简约等)"),
|
||||
original_name: str | None = Query(None, description="原名(改名时使用)"),
|
||||
rename_reason: str | None = Query(None, description="改名诉求"),
|
||||
industry: str | None = Query(None, description="行业(公司命名时使用)"),
|
||||
core_members: str | None = Query(None, description="公司核心成员"),
|
||||
ai_source_data: str | None = Query(None, description="AI原始数据"),
|
||||
created_id: int | None = Query(None, description="创建人ID"),
|
||||
updated_id: int | None = Query(None, description="更新人ID"),
|
||||
is_deleted: int | None = Query(None, description="是否删除(0:否 1:是)"),
|
||||
status: int | None = Query(None, description="状态(1:已创建 2:测算中 5:测算成功 3:测算超时 0:任务失败)"),
|
||||
birthday: datetime.datetime | None = Query(None, description="生日"),
|
||||
father_birth_date: datetime.date | None = Query(None, description="父亲出生日期"),
|
||||
mother_birth_date: datetime.date | None = Query(None, description="母亲出生日期"),
|
||||
user_id: int | None = Query(None, description="用户id"),
|
||||
created_time: list[DateTimeStr] | None = Query(None, description="创建时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]),
|
||||
updated_time: list[DateTimeStr] | None = Query(None, description="更新时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]),
|
||||
|
||||
) -> None:
|
||||
|
||||
# 精确查询字段
|
||||
self.created_id = created_id
|
||||
# 精确查询字段
|
||||
self.updated_id = updated_id
|
||||
# 精确查询字段
|
||||
self.is_deleted = is_deleted
|
||||
# 精确查询字段
|
||||
self.status = status
|
||||
# 模糊查询字段
|
||||
self.category = ("like", category)
|
||||
# 模糊查询字段
|
||||
self.service_type = ("like", service_type)
|
||||
# 模糊查询字段
|
||||
self.surname = ("like", surname)
|
||||
# 模糊查询字段
|
||||
self.given_name = ("like", given_name)
|
||||
# 模糊查询字段
|
||||
self.gender = ("like", gender)
|
||||
# 精确查询字段
|
||||
self.birthday = birthday
|
||||
# 模糊查询字段
|
||||
self.address = ("like", address)
|
||||
# 模糊查询字段
|
||||
self.genealogy_character = ("like", genealogy_character)
|
||||
# 模糊查询字段
|
||||
self.father_name = ("like", father_name)
|
||||
# 精确查询字段
|
||||
self.father_birth_date = father_birth_date
|
||||
# 模糊查询字段
|
||||
self.mother_name = ("like", mother_name)
|
||||
# 精确查询字段
|
||||
self.mother_birth_date = mother_birth_date
|
||||
# 模糊查询字段
|
||||
self.preference = ("like", preference)
|
||||
# 模糊查询字段
|
||||
self.original_name = ("like", original_name)
|
||||
# 模糊查询字段
|
||||
self.rename_reason = ("like", rename_reason)
|
||||
# 模糊查询字段
|
||||
self.industry = ("like", industry)
|
||||
# 精确查询字段
|
||||
self.user_id = user_id
|
||||
# 模糊查询字段
|
||||
self.core_members = ("like", core_members)
|
||||
# 模糊查询字段
|
||||
self.ai_source_data = ("like", ai_source_data)
|
||||
# 时间范围查询
|
||||
if created_time and len(created_time) == 2:
|
||||
self.created_time = ("between", (created_time[0], created_time[1]))
|
||||
if updated_time and len(updated_time) == 2:
|
||||
self.updated_time = ("between", (updated_time[0], updated_time[1]))
|
||||
|
||||
|
||||
class FortuneTellingRequestSchema(BaseModel):
|
||||
"""
|
||||
测名请求模型(简化版)
|
||||
"""
|
||||
category: str = Field(default="personal", description='分类(personal:个人 company:公司)')
|
||||
surname: str = Field(..., description='姓氏')
|
||||
given_name: str = Field(..., description='名字')
|
||||
gender: str = Field(default="unknown", description='性别(male:男 female:女 unknown:未知)')
|
||||
birthday: datetime.datetime = Field(..., description='生日')
|
||||
user_id: int | None = Field(default=None, description='用户ID(可选)')
|
||||
|
||||
|
||||
class FortuneTellingResponseSchema(BaseModel):
|
||||
"""
|
||||
测名响应模型
|
||||
"""
|
||||
report_id: int = Field(..., description='报告ID')
|
||||
full_name: str = Field(..., description='完整姓名')
|
||||
total_score: int = Field(..., description='总分')
|
||||
star_rating: int = Field(..., description='星级(1-5)')
|
||||
|
||||
# 六维格局
|
||||
hexagon_chart: dict = Field(..., description='六维格局数据')
|
||||
hexagon_comment: str = Field(..., description='大师批注')
|
||||
|
||||
# 周易卦象
|
||||
zhouyi_gua: dict = Field(..., description='周易卦象')
|
||||
|
||||
# 开运与运程
|
||||
lucky_colors: list[str] = Field(..., description='幸运色')
|
||||
lucky_numbers: list[int] = Field(..., description='幸运数字')
|
||||
life_motto: str = Field(..., description='人生格言')
|
||||
|
||||
# 单字详解
|
||||
character_analysis: list[dict] = Field(..., description='单字详解')
|
||||
|
||||
# 笔画数理分析
|
||||
stroke_analysis: dict = Field(..., description='笔画数理分析')
|
||||
|
||||
# 字义与生肖解析
|
||||
meaning_analysis: dict = Field(..., description='字义与生肖解析')
|
||||
zodiac_compatibility: dict = Field(..., description='生肖适配度')
|
||||
|
||||
|
||||
class PersonalScoringResponseSchema(BaseModel):
|
||||
"""
|
||||
个人测名(AI版)响应模型
|
||||
"""
|
||||
report_id: int = Field(..., description='报告ID')
|
||||
ai_response: str = Field(..., description='AI原始响应文本')
|
||||
|
||||
|
||||
class PersonalScoringTrialRequestSchema(BaseModel):
|
||||
"""免费版个人测名请求模型"""
|
||||
surname: str = Field(..., description='姓氏')
|
||||
given_name: str = Field(..., description='名字')
|
||||
gender: str = Field(..., description='性别(male/female)')
|
||||
birthday: datetime.datetime = Field(..., description='生日')
|
||||
|
||||
|
||||
class CoreMemberSchema(BaseModel):
|
||||
"""核心成员信息"""
|
||||
name: str = Field(..., description='姓名')
|
||||
birthday: datetime.datetime = Field(..., description='出生日期')
|
||||
|
||||
|
||||
class CompanyScoringRequestSchema(BaseModel):
|
||||
"""企业测名请求模型"""
|
||||
company_name: str = Field(..., description='公司名称')
|
||||
industry: str = Field(..., description='行业')
|
||||
address: str = Field(..., description='注册地/城市')
|
||||
target_audience: str = Field(..., description='目标受众')
|
||||
members: list[CoreMemberSchema] = Field(..., min_length=1, description='成员列表,至少1个')
|
||||
|
||||
|
||||
class CompanyNamingRequestSchema(BaseModel):
|
||||
"""
|
||||
公司起名请求模型
|
||||
"""
|
||||
# 基本信息
|
||||
industry: str = Field(..., description='所属行业与主营业务')
|
||||
address: str = Field(..., description='所在城市/注册地')
|
||||
|
||||
# 核心成员信息(第一个为创始人)
|
||||
core_members: list[CoreMemberSchema] = Field(..., description='核心成员列表,第一个为创始人')
|
||||
|
||||
# 企业愿景(可选)
|
||||
vision: str | None = Field(None, description='企业愿景/品牌调性')
|
||||
preference: str | None = Field(None, description='您希望名字传递的感觉')
|
||||
|
||||
|
||||
class CompanyNamingSolutionItemSchema(BaseModel):
|
||||
"""
|
||||
公司起名方案列表项(简化版)
|
||||
"""
|
||||
id: int = Field(..., description='方案ID')
|
||||
name: str = Field(..., description='公司名称')
|
||||
pinyin: str = Field(default='', description='拼音')
|
||||
wuxing: str = Field(default='', description='五行标签(如:五行属金)')
|
||||
shuxiang: str = Field(default='', description='属相(如:龙)')
|
||||
tags: list[str] = Field(default=[], description='行业标签(如:利金融/科技)')
|
||||
name_meaning: str = Field(default='', description='名字寓意')
|
||||
poetry_source: str = Field(default='', description='诗词出处')
|
||||
total_score: int | None = Field(default=None, description='总分')
|
||||
star_rating: int | None = Field(default=None, description='星级(1-5)')
|
||||
is_recommended: int = Field(default=0, description='是否推荐(0否 1是)')
|
||||
|
||||
|
||||
class CompanyNamingResponseSchema(BaseModel):
|
||||
"""
|
||||
公司起名响应模型(异步,立即返回)
|
||||
"""
|
||||
report_id: int = Field(..., description='报告ID')
|
||||
status: int = Field(..., description='状态(1:已创建 2:测算中 5:测算成功 3:测算超时 0:任务失败)')
|
||||
|
||||
|
||||
class PersonalNamingRequestSchema(BaseModel):
|
||||
"""
|
||||
个人起名请求模型
|
||||
"""
|
||||
last_name: str = Field(..., description='姓氏')
|
||||
gender: str = Field(..., description='性别(male:男 female:女)')
|
||||
birth_date: datetime.datetime = Field(..., description='生辰(格式: 1990-04-05 09:00:00)')
|
||||
birth_place: str = Field(..., description='出生地')
|
||||
style_label: str = Field(..., description='偏好风格标签(如: 典雅古风)')
|
||||
father_name: str | None = Field(None, description='父亲姓名')
|
||||
father_birth_date: datetime.datetime | None = Field(None, description='父亲出生日期')
|
||||
mother_name: str | None = Field(None, description='母亲姓名')
|
||||
mother_birth_date: datetime.datetime | None = Field(None, description='母亲出生日期')
|
||||
family_book: str | None = Field(None, description='家谱字辈')
|
||||
|
||||
|
||||
class PersonalRenamingRequestSchema(PersonalNamingRequestSchema):
|
||||
"""
|
||||
个人改名请求模型
|
||||
"""
|
||||
original_name: str = Field(..., description='曾用名/原名')
|
||||
reason: str = Field(..., description='改名诉求')
|
||||
|
||||
|
||||
class CompanyRenamingRequestSchema(CompanyNamingRequestSchema):
|
||||
"""
|
||||
企业改名请求模型
|
||||
"""
|
||||
original_name: str = Field(..., description='原公司名称')
|
||||
reason: str = Field(..., description='改名诉求')
|
||||
|
||||
|
||||
class MyReportItemSchema(BaseModel):
|
||||
"""
|
||||
我的方案列表项
|
||||
"""
|
||||
id: int = Field(description='报告ID')
|
||||
title: str = Field(description='标题')
|
||||
subtitle: str = Field(description='副标题/描述')
|
||||
category: str = Field(description='分类(personal:个人 company:公司)')
|
||||
service_type: str = Field(description='服务类型(fortune_telling:测名 naming:起名 renaming:改名)')
|
||||
has_solutions: bool = Field(description='是否已生成方案')
|
||||
created_time: str = Field(description='创建时间')
|
||||
relative_time: str = Field(description='相对时间(如: 刚刚、1天前)')
|
||||
|
||||
|
||||
class MyReportsListSchema(BaseModel):
|
||||
"""
|
||||
我的方案列表响应
|
||||
"""
|
||||
total: int = Field(description='总数')
|
||||
items: list[MyReportItemSchema] = Field(description='方案列表')
|
||||
|
||||
|
||||
class CalendarInfoSchema(BaseModel):
|
||||
"""
|
||||
黄历信息响应模型
|
||||
"""
|
||||
# 农历信息
|
||||
lunar_day: str = Field(..., description='农历日(如:十二)')
|
||||
lunar_month: str = Field(..., description='农历月(如:腊月)')
|
||||
lunar_year: str = Field(..., description='农历年(如:甲辰年)')
|
||||
|
||||
# 公历信息
|
||||
solar_date: str = Field(..., description='公历日期(如:2024.01.22)')
|
||||
solar_year: int = Field(..., description='公历年')
|
||||
solar_month: int = Field(..., description='公历月')
|
||||
solar_day: int = Field(..., description='公历日')
|
||||
|
||||
# 干支
|
||||
ganzhi_year: str = Field(..., description='干支年(如:甲辰)')
|
||||
ganzhi_month: str = Field(..., description='干支月')
|
||||
ganzhi_day: str = Field(..., description='干支日')
|
||||
|
||||
# 生肖
|
||||
zodiac: str = Field(..., description='生肖(如:龙)')
|
||||
|
||||
# 宜忌
|
||||
yi: list[str] = Field(..., description='宜(如:出行、开市)')
|
||||
ji: list[str] = Field(..., description='忌(如:动土、安葬)')
|
||||
|
||||
# 星期
|
||||
weekday: str = Field(..., description='星期(如:星期一)')
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
@@ -0,0 +1,240 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from fastapi import APIRouter, Depends, UploadFile, Body, Path, Query
|
||||
from fastapi.responses import StreamingResponse, JSONResponse
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.common.response import SuccessResponse, StreamResponse
|
||||
from app.core.dependencies import AuthPermission, db_getter
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from app.core.base_params import PaginationQueryParam
|
||||
from app.utils.common_util import bytes2file_response
|
||||
from app.core.logger import log
|
||||
from app.core.base_schema import BatchSetAvailable
|
||||
|
||||
from .service import YifanNamingSolutionsService
|
||||
from .schema import YifanNamingSolutionsCreateSchema, YifanNamingSolutionsUpdateSchema, YifanNamingSolutionsQueryParam
|
||||
|
||||
YifanNamingSolutionsRouter = APIRouter(prefix='/yifan_naming_solutions', tags=["改名方案模块"])
|
||||
|
||||
|
||||
@YifanNamingSolutionsRouter.get("/recommended", summary="获取推荐方案列表", description="获取佳名赏析列表(is_recommended=1的方案)")
|
||||
async def get_recommended_solutions_controller(
|
||||
page_no: int = Query(1, description="页码"),
|
||||
page_size: int = Query(10, description="每页数量"),
|
||||
db: AsyncSession = Depends(db_getter)
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
获取推荐方案列表接口(无需登录)
|
||||
|
||||
返回数据格式:
|
||||
- total: 总数
|
||||
- items: 方案列表
|
||||
- id: 方案ID
|
||||
- name: 名称
|
||||
- pinyin: 拼音
|
||||
- name_meaning: 名字寓意
|
||||
- poetry_source: 诗词出处
|
||||
"""
|
||||
auth = AuthSchema(db=db, check_data_scope=False)
|
||||
result = await YifanNamingSolutionsService.get_recommended_solutions_service(
|
||||
auth=auth,
|
||||
page_no=page_no,
|
||||
page_size=page_size
|
||||
)
|
||||
log.info(f"获取推荐方案列表成功")
|
||||
return SuccessResponse(data=result, msg="获取推荐方案列表成功")
|
||||
|
||||
|
||||
@YifanNamingSolutionsRouter.get("/pdf/{id}", summary="下载方案PDF报告", description="根据方案ID生成并下载PDF报告")
|
||||
async def download_solution_pdf_controller(
|
||||
id: int = Path(..., description="方案ID"),
|
||||
auth: AuthSchema = Depends(AuthPermission([]))
|
||||
) -> StreamingResponse:
|
||||
"""
|
||||
下载方案PDF报告接口
|
||||
|
||||
小程序调用此接口获取PDF文件流,然后通过wx.downloadFile下载
|
||||
"""
|
||||
pdf_bytes = await YifanNamingSolutionsService.generate_solution_pdf_service(
|
||||
auth=auth,
|
||||
solution_id=id
|
||||
)
|
||||
log.info(f"下载方案PDF成功, 方案ID: {id}")
|
||||
|
||||
return StreamResponse(
|
||||
data=bytes2file_response(pdf_bytes),
|
||||
media_type='application/pdf',
|
||||
headers={
|
||||
'Content-Disposition': f'attachment; filename=solution_{id}.pdf',
|
||||
'Content-Type': 'application/pdf'
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@YifanNamingSolutionsRouter.get("/full_detail/{id}", summary="获取方案完整详情", description="获取方案完整详情,包含所有板块和详情数据")
|
||||
async def get_solution_full_detail_controller(
|
||||
id: int = Path(..., description="方案ID"),
|
||||
auth: AuthSchema = Depends(AuthPermission([], check_data_scope=False))
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
获取方案完整详情接口
|
||||
|
||||
返回数据格式:
|
||||
- id: 方案ID
|
||||
- report_id: 关联报告ID
|
||||
- name: 姓名/公司名
|
||||
- pinyin: 拼音
|
||||
- total_score: 总分
|
||||
- star_rating: 星级
|
||||
- wuxing: 五行
|
||||
- shuxiang: 属相
|
||||
- tags: 标签列表
|
||||
- name_meaning: 名字寓意
|
||||
- poetry_source: 诗词出处
|
||||
- is_recommended: 是否推荐
|
||||
- is_favorited: 是否已收藏
|
||||
- sections: 板块列表
|
||||
- id: 板块ID
|
||||
- section_type: 板块类型
|
||||
- title: 板块标题
|
||||
- summary_data: 摘要数据
|
||||
- details: 详情列表
|
||||
"""
|
||||
result = await YifanNamingSolutionsService.get_solution_full_detail_service(
|
||||
auth=auth,
|
||||
solution_id=id
|
||||
)
|
||||
log.info(f"获取方案完整详情成功, 方案ID: {id}")
|
||||
return SuccessResponse(data=result, msg="获取方案详情成功")
|
||||
|
||||
@YifanNamingSolutionsRouter.get("/detail/{id}", summary="获取改名方案详情", description="获取改名方案详情")
|
||||
async def get_yifan_naming_solutions_detail_controller(
|
||||
id: int = Path(..., description="ID"),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_naming_solutions:query"], check_data_scope=False))
|
||||
) -> JSONResponse:
|
||||
"""获取改名方案详情接口"""
|
||||
result_dict = await YifanNamingSolutionsService.detail_yifan_naming_solutions_service(auth=auth, id=id)
|
||||
log.info(f"获取改名方案详情成功 {id}")
|
||||
return SuccessResponse(data=result_dict, msg="获取改名方案详情成功")
|
||||
|
||||
@YifanNamingSolutionsRouter.get("/list", summary="查询改名方案列表", description="查询改名方案列表")
|
||||
async def get_yifan_naming_solutions_list_controller(
|
||||
page: PaginationQueryParam = Depends(),
|
||||
search: YifanNamingSolutionsQueryParam = Depends(),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_naming_solutions:query"], check_data_scope=False))
|
||||
) -> JSONResponse:
|
||||
"""查询改名方案列表接口(数据库分页)"""
|
||||
log.info(f"=== 改名方案查询开始 ===")
|
||||
log.info(f"用户信息: user_id={auth.user.id if auth.user else None}, username={auth.user.username if auth.user else None}")
|
||||
log.info(f"数据权限检查: check_data_scope={auth.check_data_scope}")
|
||||
log.info(f"查询参数: page_no={page.page_no}, page_size={page.page_size}")
|
||||
log.info(f"搜索条件: {search.__dict__ if search else None}")
|
||||
|
||||
result_dict = await YifanNamingSolutionsService.page_yifan_naming_solutions_service(
|
||||
auth=auth,
|
||||
page_no=page.page_no if page.page_no is not None else 1,
|
||||
page_size=page.page_size if page.page_size is not None else 10,
|
||||
search=search,
|
||||
order_by=page.order_by
|
||||
)
|
||||
|
||||
log.info(f"=== 查询结果 ===")
|
||||
log.info(f"总数: {result_dict.get('total')}")
|
||||
log.info(f"返回数据量: {len(result_dict.get('items', []))}")
|
||||
log.info(f"查询改名方案列表成功")
|
||||
|
||||
return SuccessResponse(data=result_dict, msg="查询改名方案列表成功")
|
||||
|
||||
@YifanNamingSolutionsRouter.post("/create", summary="创建改名方案", description="创建改名方案")
|
||||
async def create_yifan_naming_solutions_controller(
|
||||
data: YifanNamingSolutionsCreateSchema,
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_naming_solutions:create"]))
|
||||
) -> JSONResponse:
|
||||
"""创建改名方案接口"""
|
||||
result_dict = await YifanNamingSolutionsService.create_yifan_naming_solutions_service(auth=auth, data=data)
|
||||
log.info("创建改名方案成功")
|
||||
return SuccessResponse(data=result_dict, msg="创建改名方案成功")
|
||||
|
||||
@YifanNamingSolutionsRouter.put("/update/{id}", summary="修改改名方案", description="修改改名方案")
|
||||
async def update_yifan_naming_solutions_controller(
|
||||
data: YifanNamingSolutionsUpdateSchema,
|
||||
id: int = Path(..., description="ID"),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_naming_solutions:update"]))
|
||||
) -> JSONResponse:
|
||||
"""修改改名方案接口"""
|
||||
result_dict = await YifanNamingSolutionsService.update_yifan_naming_solutions_service(auth=auth, id=id, data=data)
|
||||
log.info("修改改名方案成功")
|
||||
return SuccessResponse(data=result_dict, msg="修改改名方案成功")
|
||||
|
||||
@YifanNamingSolutionsRouter.delete("/delete", summary="删除改名方案", description="删除改名方案")
|
||||
async def delete_yifan_naming_solutions_controller(
|
||||
ids: list[int] = Body(..., description="ID列表"),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_naming_solutions:delete"]))
|
||||
) -> JSONResponse:
|
||||
"""删除改名方案接口"""
|
||||
await YifanNamingSolutionsService.delete_yifan_naming_solutions_service(auth=auth, ids=ids)
|
||||
log.info(f"删除改名方案成功: {ids}")
|
||||
return SuccessResponse(msg="删除改名方案成功")
|
||||
|
||||
@YifanNamingSolutionsRouter.patch("/available/setting", summary="批量修改改名方案状态", description="批量修改改名方案状态")
|
||||
async def batch_set_available_yifan_naming_solutions_controller(
|
||||
data: BatchSetAvailable,
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_naming_solutions:patch"]))
|
||||
) -> JSONResponse:
|
||||
"""批量修改改名方案状态接口"""
|
||||
await YifanNamingSolutionsService.set_available_yifan_naming_solutions_service(auth=auth, data=data)
|
||||
log.info(f"批量修改改名方案状态成功: {data.ids}")
|
||||
return SuccessResponse(msg="批量修改改名方案状态成功")
|
||||
|
||||
|
||||
@YifanNamingSolutionsRouter.patch("/toggle-recommend/{id}", summary="切换推荐状态", description="切换方案的推荐状态")
|
||||
async def toggle_recommend_controller(
|
||||
id: int = Path(..., description="方案ID"),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_naming_solutions:patch"]))
|
||||
) -> JSONResponse:
|
||||
"""切换推荐状态接口"""
|
||||
result = await YifanNamingSolutionsService.toggle_recommend_service(auth=auth, id=id)
|
||||
log.info(f"切换推荐状态成功, 方案ID: {id}")
|
||||
return SuccessResponse(data=result, msg="操作成功")
|
||||
|
||||
@YifanNamingSolutionsRouter.post('/export', summary="导出改名方案", description="导出改名方案")
|
||||
async def export_yifan_naming_solutions_list_controller(
|
||||
search: YifanNamingSolutionsQueryParam = Depends(),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_naming_solutions:export"]))
|
||||
) -> StreamingResponse:
|
||||
"""导出改名方案接口"""
|
||||
result_dict_list = await YifanNamingSolutionsService.list_yifan_naming_solutions_service(search=search, auth=auth)
|
||||
export_result = await YifanNamingSolutionsService.batch_export_yifan_naming_solutions_service(obj_list=result_dict_list)
|
||||
log.info('导出改名方案成功')
|
||||
|
||||
return StreamResponse(
|
||||
data=bytes2file_response(export_result),
|
||||
media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
headers={
|
||||
'Content-Disposition': 'attachment; filename=yifan_naming_solutions.xlsx'
|
||||
}
|
||||
)
|
||||
|
||||
@YifanNamingSolutionsRouter.post('/import', summary="导入改名方案", description="导入改名方案")
|
||||
async def import_yifan_naming_solutions_list_controller(
|
||||
file: UploadFile,
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_naming_solutions:import"]))
|
||||
) -> JSONResponse:
|
||||
"""导入改名方案接口"""
|
||||
batch_import_result = await YifanNamingSolutionsService.batch_import_yifan_naming_solutions_service(file=file, auth=auth, update_support=True)
|
||||
log.info("导入改名方案成功")
|
||||
|
||||
return SuccessResponse(data=batch_import_result, msg="导入改名方案成功")
|
||||
|
||||
@YifanNamingSolutionsRouter.post('/download/template', summary="获取改名方案导入模板", description="获取改名方案导入模板", dependencies=[Depends(AuthPermission(["module_yifan:yifan_naming_solutions:download"]))])
|
||||
async def export_yifan_naming_solutions_template_controller() -> StreamingResponse:
|
||||
"""获取改名方案导入模板接口"""
|
||||
import_template_result = await YifanNamingSolutionsService.import_template_download_yifan_naming_solutions_service()
|
||||
log.info('获取改名方案导入模板成功')
|
||||
|
||||
return StreamResponse(
|
||||
data=bytes2file_response(import_template_result),
|
||||
media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
headers={'Content-Disposition': 'attachment; filename=yifan_naming_solutions_template.xlsx'}
|
||||
)
|
||||
@@ -0,0 +1,242 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from sqlalchemy import select, func
|
||||
from app.core.base_crud import CRUDBase
|
||||
from .model import YifanNamingSolutionsModel
|
||||
import json
|
||||
import ast
|
||||
|
||||
class YifanNamingSolutionsCRUD(CRUDBase[YifanNamingSolutionsModel, None, None]):
|
||||
"""改名方案 CRUD"""
|
||||
|
||||
def __init__(self, auth):
|
||||
super().__init__(model=YifanNamingSolutionsModel, auth=auth)
|
||||
|
||||
async def get_by_id_yifan_naming_solutions_crud(self, id: int) -> YifanNamingSolutionsModel | None:
|
||||
"""根据ID获取"""
|
||||
return await self.get(id=id)
|
||||
|
||||
async def list_yifan_naming_solutions_crud(self, search: dict | None = None, order_by: list[dict] | None = None) -> list[YifanNamingSolutionsModel]:
|
||||
"""列表查询"""
|
||||
return await self.list(search=search, order_by=order_by)
|
||||
|
||||
async def page_yifan_naming_solutions_crud(self, offset: int, limit: int, search: dict | None = None, order_by: list[dict] | None = None) -> dict:
|
||||
"""分页查询"""
|
||||
from .schema import YifanNamingSolutionsOutSchema
|
||||
return await self.page(offset=offset, limit=limit, search=search, order_by=order_by, out_schema=YifanNamingSolutionsOutSchema)
|
||||
|
||||
async def create_yifan_naming_solutions_crud(self, data) -> YifanNamingSolutionsModel:
|
||||
"""创建"""
|
||||
return await self.create(data=data)
|
||||
|
||||
async def update_yifan_naming_solutions_crud(self, id: int, data) -> YifanNamingSolutionsModel:
|
||||
"""更新"""
|
||||
return await self.update(id=id, data=data)
|
||||
|
||||
async def delete_yifan_naming_solutions_crud(self, ids: list[int]) -> None:
|
||||
"""删除"""
|
||||
await self.delete(ids=ids)
|
||||
|
||||
async def set_available_yifan_naming_solutions_crud(self, ids: list[int], status: int) -> None:
|
||||
"""批量设置状态"""
|
||||
await self.set(ids=ids, status=status)
|
||||
|
||||
async def list_by_report_id(self, report_id: int) -> list[YifanNamingSolutionsModel]:
|
||||
"""
|
||||
根据报告ID获取方案列表
|
||||
|
||||
参数:
|
||||
- report_id (int): 报告ID
|
||||
|
||||
返回:
|
||||
- list[YifanNamingSolutionsModel]: 方案列表
|
||||
"""
|
||||
stmt = (
|
||||
select(YifanNamingSolutionsModel)
|
||||
.where(
|
||||
YifanNamingSolutionsModel.report_id == report_id,
|
||||
YifanNamingSolutionsModel.is_deleted == 0
|
||||
)
|
||||
.order_by(YifanNamingSolutionsModel.sort_order)
|
||||
)
|
||||
result = await self.auth.db.execute(stmt)
|
||||
return result.scalars().all()
|
||||
|
||||
async def get_solution_full_detail_crud(self, solution_id: int, user_id: int | None = None) -> dict | None:
|
||||
"""
|
||||
获取方案完整详情(包含板块和详情)
|
||||
|
||||
参数:
|
||||
- solution_id (int): 方案ID
|
||||
- user_id (int | None): 用户ID,用于判断是否已收藏
|
||||
|
||||
返回:
|
||||
- dict | None: 方案完整详情
|
||||
"""
|
||||
from sqlalchemy import select
|
||||
from app.api.v1.module_yifan.yifan_solution_sections.model import YifanSolutionSectionsModel
|
||||
from app.api.v1.module_yifan.yifan_section_details.model import YifanSectionDetailsModel
|
||||
from app.api.v1.module_yifan.yifan_naming_favorites.model import YifanNamingFavoritesModel
|
||||
|
||||
# 1. 获取方案基本信息
|
||||
solution = await self.get(id=solution_id)
|
||||
if not solution or solution.is_deleted == 1:
|
||||
return None
|
||||
|
||||
# 2. 查询是否已收藏
|
||||
is_favorited = False
|
||||
if user_id:
|
||||
fav_stmt = select(YifanNamingFavoritesModel.id).where(
|
||||
YifanNamingFavoritesModel.user_id == user_id,
|
||||
YifanNamingFavoritesModel.solution_id == solution_id,
|
||||
YifanNamingFavoritesModel.is_deleted == 0
|
||||
)
|
||||
fav_result = await self.auth.db.execute(fav_stmt)
|
||||
is_favorited = fav_result.scalar() is not None
|
||||
|
||||
# 3. 获取所有板块
|
||||
sections_stmt = (
|
||||
select(YifanSolutionSectionsModel)
|
||||
.where(
|
||||
YifanSolutionSectionsModel.solution_id == solution_id,
|
||||
YifanSolutionSectionsModel.is_deleted == 0
|
||||
)
|
||||
.order_by(YifanSolutionSectionsModel.sort_order)
|
||||
)
|
||||
sections_result = await self.auth.db.execute(sections_stmt)
|
||||
sections = sections_result.scalars().all()
|
||||
|
||||
# 4. 获取所有板块的详情
|
||||
section_ids = [s.id for s in sections]
|
||||
details_map = {}
|
||||
if section_ids:
|
||||
details_stmt = (
|
||||
select(YifanSectionDetailsModel)
|
||||
.where(
|
||||
YifanSectionDetailsModel.section_id.in_(section_ids),
|
||||
YifanSectionDetailsModel.is_deleted == 0
|
||||
)
|
||||
.order_by(YifanSectionDetailsModel.sort_order)
|
||||
)
|
||||
details_result = await self.auth.db.execute(details_stmt)
|
||||
details = details_result.scalars().all()
|
||||
|
||||
# 按 section_id 分组
|
||||
for detail in details:
|
||||
if detail.section_id not in details_map:
|
||||
details_map[detail.section_id] = []
|
||||
details_map[detail.section_id].append({
|
||||
'id': detail.id,
|
||||
'detail_type': detail.detail_type,
|
||||
'title': detail.title,
|
||||
'content': detail.content,
|
||||
'sort_order': detail.sort_order
|
||||
})
|
||||
|
||||
# 5. 组装板块数据
|
||||
sections_data = []
|
||||
for section in sections:
|
||||
sections_data.append({
|
||||
'id': section.id,
|
||||
'section_type': section.section_type,
|
||||
'title': section.title,
|
||||
'summary_data': section.summary_data,
|
||||
'sort_order': section.sort_order,
|
||||
'details': details_map.get(section.id, [])
|
||||
})
|
||||
|
||||
real_list = ast.literal_eval(solution.analysis_result)[0]
|
||||
real_list['id'] = solution.id
|
||||
real_list['report_id'] = solution.report_id
|
||||
return json.dumps(real_list, ensure_ascii=False)
|
||||
|
||||
# 6. 组装返回数据
|
||||
# return {
|
||||
# 'id': solution.id,
|
||||
# 'report_id': solution.report_id,
|
||||
# 'name': solution.name,
|
||||
# 'pinyin': solution.pinyin,
|
||||
# 'total_score': solution.total_score,
|
||||
# 'star_rating': solution.star_rating,
|
||||
# 'wuxing': solution.wuxing,
|
||||
# 'shuxiang': solution.shuxiang,
|
||||
# 'tags': solution.tags or [],
|
||||
# 'name_meaning': solution.name_meaning,
|
||||
# 'poetry_source': solution.poetry_source,
|
||||
# 'is_recommended': solution.is_recommended,
|
||||
# 'is_favorited': is_favorited,
|
||||
# 'sections': sections_data
|
||||
# }
|
||||
|
||||
async def get_recommended_solutions_crud(self, page_no: int = 1, page_size: int = 10) -> dict:
|
||||
"""
|
||||
获取推荐方案列表(佳名赏析),按名字去重
|
||||
|
||||
参数:
|
||||
- page_no: 页码
|
||||
- page_size: 每页数量
|
||||
|
||||
返回:
|
||||
- dict: 包含total和items的分页数据
|
||||
"""
|
||||
from sqlalchemy import distinct
|
||||
|
||||
# 查询去重后的总数
|
||||
count_stmt = (
|
||||
select(func.count(distinct(YifanNamingSolutionsModel.name)))
|
||||
.where(
|
||||
YifanNamingSolutionsModel.is_recommended == 1,
|
||||
YifanNamingSolutionsModel.is_deleted == 0
|
||||
)
|
||||
)
|
||||
count_result = await self.auth.db.execute(count_stmt)
|
||||
total = count_result.scalar() or 0
|
||||
|
||||
# 使用子查询获取每个名字的最新记录ID
|
||||
subquery = (
|
||||
select(
|
||||
YifanNamingSolutionsModel.name,
|
||||
func.max(YifanNamingSolutionsModel.id).label('max_id')
|
||||
)
|
||||
.where(
|
||||
YifanNamingSolutionsModel.is_recommended == 1,
|
||||
YifanNamingSolutionsModel.is_deleted == 0
|
||||
)
|
||||
.group_by(YifanNamingSolutionsModel.name)
|
||||
.subquery()
|
||||
)
|
||||
|
||||
# 查询列表(按名字去重,取最新的记录)
|
||||
offset = (page_no - 1) * page_size
|
||||
stmt = (
|
||||
select(YifanNamingSolutionsModel)
|
||||
.join(subquery, YifanNamingSolutionsModel.id == subquery.c.max_id)
|
||||
.order_by(YifanNamingSolutionsModel.id.desc())
|
||||
.offset(offset)
|
||||
.limit(page_size)
|
||||
)
|
||||
result = await self.auth.db.execute(stmt)
|
||||
solutions = result.scalars().all()
|
||||
|
||||
# 组装返回数据
|
||||
items = []
|
||||
for sol in solutions:
|
||||
items.append({
|
||||
'id': sol.id,
|
||||
'name': sol.name or '',
|
||||
'pinyin': sol.pinyin or '',
|
||||
'name_meaning': sol.name_meaning or '',
|
||||
'poetry_source': sol.poetry_source or '',
|
||||
'total_score': sol.total_score,
|
||||
'star_rating': sol.star_rating,
|
||||
'wuxing': sol.wuxing or '',
|
||||
'shuxiang': sol.shuxiang or '',
|
||||
'tags': sol.tags if isinstance(sol.tags, list) else []
|
||||
})
|
||||
|
||||
return {
|
||||
'total': total,
|
||||
'page_no': page_no,
|
||||
'page_size': page_size,
|
||||
'items': items
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from sqlalchemy import String, Integer, SmallInteger, Text, JSON
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.base_model import ModelMixin, UserMixin
|
||||
|
||||
|
||||
class YifanNamingSolutionsModel(ModelMixin, UserMixin):
|
||||
"""
|
||||
改名方案表
|
||||
"""
|
||||
__tablename__: str = 'yifan_naming_solutions'
|
||||
__table_args__: dict[str, str] = {'comment': '改名方案表'}
|
||||
__loader_options__: list[str] = ["created_by", "updated_by"]
|
||||
|
||||
is_deleted: Mapped[int | None] = mapped_column(SmallInteger, nullable=True, comment='是否删除(0:否 1:是)')
|
||||
report_id: Mapped[int] = mapped_column(Integer, nullable=False, comment='关联报告ID')
|
||||
name: Mapped[str] = mapped_column(String(100), nullable=False, comment='姓名/公司名')
|
||||
pinyin: Mapped[str | None] = mapped_column(String(100), nullable=True, comment='拼音')
|
||||
total_score: Mapped[int | None] = mapped_column(Integer, nullable=True, comment='总分')
|
||||
star_rating: Mapped[int | None] = mapped_column(Integer, nullable=True, comment='星级(1-5)')
|
||||
wuxing: Mapped[str | None] = mapped_column(String(50), nullable=True, comment='五行')
|
||||
shuxiang: Mapped[str | None] = mapped_column(String(20), nullable=True, comment='属相')
|
||||
tags: Mapped[dict | None] = mapped_column(JSON, nullable=True, comment='标签')
|
||||
name_meaning: Mapped[str | None] = mapped_column(Text, nullable=True, comment='名字寓意')
|
||||
poetry_source: Mapped[str | None] = mapped_column(Text, nullable=True, comment='诗词出处')
|
||||
is_recommended: Mapped[int | None] = mapped_column(SmallInteger, nullable=True, default=0, comment='是否推荐(0:否 1:是)')
|
||||
sort_order: Mapped[int | None] = mapped_column(Integer, nullable=True, default=0, comment='排序')
|
||||
analysis_result: Mapped[str | None] = mapped_column(Text, nullable=True, comment='AI分析结果')
|
||||
@@ -0,0 +1,143 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from fastapi import Query
|
||||
|
||||
from app.core.validator import DateTimeStr
|
||||
from app.core.base_schema import BaseSchema, UserBySchema
|
||||
|
||||
class YifanNamingSolutionsCreateSchema(BaseModel):
|
||||
"""
|
||||
改名方案新增模型
|
||||
"""
|
||||
is_deleted: int = Field(default=0, description='是否删除(0否 1是)')
|
||||
status: int = Field(default=1, description='状态(0禁用 1启用)')
|
||||
report_id: int = Field(default=..., description='关联报告ID')
|
||||
name: str = Field(default=..., description='姓名')
|
||||
pinyin: str | None = Field(default=None, description='拼音')
|
||||
total_score: int | None = Field(default=None, description='总分')
|
||||
star_rating: int | None = Field(default=None, description='星级(1-5)')
|
||||
wuxing: str | None = Field(default=None, description='五行(如:火土)')
|
||||
shuxiang: str | None = Field(default=None, description='属相(如:龙)')
|
||||
tags: list | None = Field(default=None, description='标签')
|
||||
name_meaning: str | None = Field(default=None, description='名字寓意')
|
||||
poetry_source: str | None = Field(default=None, description='诗词出处')
|
||||
is_recommended: int = Field(default=0, description='是否推荐(0否 1是)')
|
||||
sort_order: int = Field(default=0, description='排序')
|
||||
analysis_result: str | None = Field(default=None, description='AI分析结果')
|
||||
|
||||
|
||||
class YifanNamingSolutionsUpdateSchema(YifanNamingSolutionsCreateSchema):
|
||||
"""
|
||||
改名方案更新模型
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
class YifanNamingSolutionsOutSchema(YifanNamingSolutionsCreateSchema, BaseSchema, UserBySchema):
|
||||
"""
|
||||
改名方案响应模型
|
||||
"""
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class YifanNamingSolutionsQueryParam:
|
||||
"""改名方案查询参数"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str | None = Query(None, description="姓名"),
|
||||
pinyin: str | None = Query(None, description="拼音"),
|
||||
wuxing: str | None = Query(None, description="五行(如:火土)"),
|
||||
shuxiang: str | None = Query(None, description="属相(如:龙)"),
|
||||
name_meaning: str | None = Query(None, description="名字寓意"),
|
||||
poetry_source: str | None = Query(None, description="诗词出处"),
|
||||
created_id: int | None = Query(None, description="创建人ID"),
|
||||
updated_id: int | None = Query(None, description="更新人ID"),
|
||||
is_deleted: int | None = Query(None, description="是否删除(0否 1是)"),
|
||||
status: int | None = Query(None, description="状态(0禁用 1启用)"),
|
||||
report_id: int | None = Query(None, description="关联报告ID"),
|
||||
total_score: int | None = Query(None, description="总分"),
|
||||
star_rating: int | None = Query(None, description="星级(1-5)"),
|
||||
tags: str | None = Query(None, description='标签(JSON字符串,如:["改运补禄","平安顺遂"])'),
|
||||
is_recommended: int | None = Query(None, description="是否推荐(0否 1是)"),
|
||||
sort_order: int | None = Query(None, description="排序"),
|
||||
created_time: list[DateTimeStr] | None = Query(None, description="创建时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]),
|
||||
updated_time: list[DateTimeStr] | None = Query(None, description="更新时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]),
|
||||
|
||||
) -> None:
|
||||
|
||||
# 精确查询字段
|
||||
self.created_id = created_id
|
||||
# 精确查询字段
|
||||
self.updated_id = updated_id
|
||||
# 精确查询字段
|
||||
self.is_deleted = is_deleted
|
||||
# 精确查询字段
|
||||
self.status = status
|
||||
# 精确查询字段
|
||||
self.report_id = report_id
|
||||
# 模糊查询字段
|
||||
self.name = ("like", name)
|
||||
# 模糊查询字段
|
||||
self.pinyin = ("like", pinyin)
|
||||
# 精确查询字段
|
||||
self.total_score = total_score
|
||||
# 精确查询字段
|
||||
self.star_rating = star_rating
|
||||
# 模糊查询字段
|
||||
self.wuxing = ("like", wuxing)
|
||||
# 模糊查询字段
|
||||
self.shuxiang = ("like", shuxiang)
|
||||
# 精确查询字段
|
||||
self.tags = tags
|
||||
# 模糊查询字段
|
||||
self.name_meaning = ("like", name_meaning)
|
||||
# 模糊查询字段
|
||||
self.poetry_source = ("like", poetry_source)
|
||||
# 精确查询字段
|
||||
self.is_recommended = is_recommended
|
||||
# 精确查询字段
|
||||
self.sort_order = sort_order
|
||||
# 时间范围查询
|
||||
if created_time and len(created_time) == 2:
|
||||
self.created_time = ("between", (created_time[0], created_time[1]))
|
||||
if updated_time and len(updated_time) == 2:
|
||||
self.updated_time = ("between", (updated_time[0], updated_time[1]))
|
||||
|
||||
|
||||
class SectionDetailSchema(BaseModel):
|
||||
"""板块详情"""
|
||||
id: int = Field(description='详情ID')
|
||||
detail_type: str = Field(description='详情类型')
|
||||
title: str | None = Field(default=None, description='详情标题')
|
||||
content: dict | None = Field(default=None, description='详细内容')
|
||||
sort_order: int | None = Field(default=0, description='排序')
|
||||
|
||||
|
||||
class SolutionSectionSchema(BaseModel):
|
||||
"""方案板块"""
|
||||
id: int = Field(description='板块ID')
|
||||
section_type: str = Field(description='板块类型')
|
||||
title: str | None = Field(default=None, description='板块标题')
|
||||
summary_data: dict | None = Field(default=None, description='摘要数据')
|
||||
sort_order: int | None = Field(default=0, description='排序')
|
||||
details: list[SectionDetailSchema] = Field(default=[], description='板块详情列表')
|
||||
|
||||
|
||||
class SolutionFullDetailSchema(BaseModel):
|
||||
"""方案完整详情"""
|
||||
id: int = Field(description='方案ID')
|
||||
report_id: int = Field(description='关联报告ID')
|
||||
name: str = Field(description='姓名/公司名')
|
||||
pinyin: str | None = Field(default=None, description='拼音')
|
||||
total_score: int | None = Field(default=None, description='总分')
|
||||
star_rating: int | None = Field(default=None, description='星级(1-5)')
|
||||
wuxing: str | None = Field(default=None, description='五行')
|
||||
shuxiang: str | None = Field(default=None, description='属相')
|
||||
tags: list | None = Field(default=None, description='标签')
|
||||
name_meaning: str | None = Field(default=None, description='名字寓意')
|
||||
poetry_source: str | None = Field(default=None, description='诗词出处')
|
||||
is_recommended: int | None = Field(default=0, description='是否推荐')
|
||||
is_favorited: bool = Field(default=False, description='是否已收藏')
|
||||
sections: list[SolutionSectionSchema] = Field(default=[], description='板块列表')
|
||||
@@ -0,0 +1,382 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import io
|
||||
import os
|
||||
from fastapi import UploadFile
|
||||
import pandas as pd
|
||||
|
||||
from app.core.base_schema import BatchSetAvailable
|
||||
from app.core.exceptions import CustomException
|
||||
from app.utils.excel_util import ExcelUtil
|
||||
from app.utils.pdf_util import PDFUtil
|
||||
from app.core.logger import log
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from .schema import YifanNamingSolutionsCreateSchema, YifanNamingSolutionsUpdateSchema, YifanNamingSolutionsOutSchema, YifanNamingSolutionsQueryParam
|
||||
from .crud import YifanNamingSolutionsCRUD
|
||||
|
||||
|
||||
class YifanNamingSolutionsService:
|
||||
"""
|
||||
改名方案服务层
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
async def detail_yifan_naming_solutions_service(cls, auth: AuthSchema, id: int) -> dict:
|
||||
"""详情"""
|
||||
obj = await YifanNamingSolutionsCRUD(auth).get_by_id_yifan_naming_solutions_crud(id=id)
|
||||
if not obj:
|
||||
raise CustomException(msg="该数据不存在")
|
||||
return YifanNamingSolutionsOutSchema.model_validate(obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def list_yifan_naming_solutions_service(cls, auth: AuthSchema, search: YifanNamingSolutionsQueryParam | None = None, order_by: list[dict] | None = None) -> list[dict]:
|
||||
"""列表查询"""
|
||||
search_dict = search.__dict__ if search else None
|
||||
obj_list = await YifanNamingSolutionsCRUD(auth).list_yifan_naming_solutions_crud(search=search_dict, order_by=order_by)
|
||||
return [YifanNamingSolutionsOutSchema.model_validate(obj).model_dump() for obj in obj_list]
|
||||
|
||||
@classmethod
|
||||
async def page_yifan_naming_solutions_service(cls, auth: AuthSchema, page_no: int, page_size: int, search: YifanNamingSolutionsQueryParam | None = None, order_by: list[dict] | None = None) -> dict:
|
||||
"""分页查询(数据库分页)"""
|
||||
search_dict = search.__dict__ if search else {}
|
||||
order_by_list = order_by or [{'id': 'asc'}]
|
||||
offset = (page_no - 1) * page_size
|
||||
result = await YifanNamingSolutionsCRUD(auth).page_yifan_naming_solutions_crud(
|
||||
offset=offset,
|
||||
limit=page_size,
|
||||
order_by=order_by_list,
|
||||
search=search_dict
|
||||
)
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
async def get_recommended_solutions_service(cls, auth: AuthSchema, page_no: int = 1, page_size: int = 10) -> dict:
|
||||
"""
|
||||
获取推荐方案列表(佳名赏析)
|
||||
|
||||
参数:
|
||||
- auth: 认证信息
|
||||
- page_no: 页码
|
||||
- page_size: 每页数量
|
||||
|
||||
返回:
|
||||
- dict: 包含total和items的分页数据
|
||||
"""
|
||||
return await YifanNamingSolutionsCRUD(auth).get_recommended_solutions_crud(
|
||||
page_no=page_no,
|
||||
page_size=page_size
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def toggle_recommend_service(cls, auth: AuthSchema, id: int) -> dict:
|
||||
"""
|
||||
切换推荐状态
|
||||
|
||||
参数:
|
||||
- auth: 认证信息
|
||||
- id: 方案ID
|
||||
|
||||
返回:
|
||||
- dict: 包含新的推荐状态
|
||||
"""
|
||||
obj = await YifanNamingSolutionsCRUD(auth).get_by_id_yifan_naming_solutions_crud(id=id)
|
||||
if not obj:
|
||||
raise CustomException(msg="方案不存在")
|
||||
|
||||
# 切换状态
|
||||
new_status = 0 if obj.is_recommended == 1 else 1
|
||||
await YifanNamingSolutionsCRUD(auth).set(ids=[id], is_recommended=new_status)
|
||||
|
||||
return {
|
||||
'id': id,
|
||||
'is_recommended': new_status
|
||||
}
|
||||
|
||||
@classmethod
|
||||
async def create_yifan_naming_solutions_service(cls, auth: AuthSchema, data: YifanNamingSolutionsCreateSchema) -> dict:
|
||||
"""创建"""
|
||||
# 检查唯一性约束
|
||||
obj = await YifanNamingSolutionsCRUD(auth).create_yifan_naming_solutions_crud(data=data)
|
||||
return YifanNamingSolutionsOutSchema.model_validate(obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def update_yifan_naming_solutions_service(cls, auth: AuthSchema, id: int, data: YifanNamingSolutionsUpdateSchema) -> dict:
|
||||
"""更新"""
|
||||
# 检查数据是否存在
|
||||
obj = await YifanNamingSolutionsCRUD(auth).get_by_id_yifan_naming_solutions_crud(id=id)
|
||||
if not obj:
|
||||
raise CustomException(msg='更新失败,该数据不存在')
|
||||
|
||||
# 检查唯一性约束
|
||||
|
||||
obj = await YifanNamingSolutionsCRUD(auth).update_yifan_naming_solutions_crud(id=id, data=data)
|
||||
return YifanNamingSolutionsOutSchema.model_validate(obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def delete_yifan_naming_solutions_service(cls, auth: AuthSchema, ids: list[int]) -> None:
|
||||
"""删除"""
|
||||
if len(ids) < 1:
|
||||
raise CustomException(msg='删除失败,删除对象不能为空')
|
||||
for id in ids:
|
||||
obj = await YifanNamingSolutionsCRUD(auth).get_by_id_yifan_naming_solutions_crud(id=id)
|
||||
if not obj:
|
||||
raise CustomException(msg=f'删除失败,ID为{id}的数据不存在')
|
||||
await YifanNamingSolutionsCRUD(auth).delete_yifan_naming_solutions_crud(ids=ids)
|
||||
|
||||
@classmethod
|
||||
async def set_available_yifan_naming_solutions_service(cls, auth: AuthSchema, data: BatchSetAvailable) -> None:
|
||||
"""批量设置状态"""
|
||||
await YifanNamingSolutionsCRUD(auth).set_available_yifan_naming_solutions_crud(ids=data.ids, status=data.status)
|
||||
|
||||
@classmethod
|
||||
async def batch_export_yifan_naming_solutions_service(cls, obj_list: list[dict]) -> bytes:
|
||||
"""批量导出"""
|
||||
mapping_dict = {
|
||||
'id': '主键ID',
|
||||
'created_time': '创建时间',
|
||||
'updated_time': '更新时间',
|
||||
'created_id': '创建人ID',
|
||||
'updated_id': '更新人ID',
|
||||
'is_deleted': '是否删除(0否 1是)',
|
||||
'status': '状态(0禁用 1启用)',
|
||||
'report_id': '关联报告ID',
|
||||
'name': '姓名',
|
||||
'pinyin': '拼音',
|
||||
'total_score': '总分',
|
||||
'star_rating': '星级(1-5)',
|
||||
'wuxing': '五行(如:火土)',
|
||||
'shuxiang': '属相(如:龙)',
|
||||
'tags': '标签(如:["改运补禄","平安顺遂"])',
|
||||
'name_meaning': '名字寓意',
|
||||
'poetry_source': '诗词出处',
|
||||
'is_recommended': '是否推荐(0否 1是)',
|
||||
'sort_order': '排序',
|
||||
'updated_id': '更新者ID',
|
||||
}
|
||||
|
||||
data = obj_list.copy()
|
||||
for item in data:
|
||||
# 状态转换
|
||||
if 'status' in item:
|
||||
item['status'] = '启用' if item.get('status') == '0' else '停用'
|
||||
# 创建者转换
|
||||
creator_info = item.get('creator')
|
||||
if isinstance(creator_info, dict):
|
||||
item['creator'] = creator_info.get('name', '未知')
|
||||
elif creator_info is None:
|
||||
item['creator'] = '未知'
|
||||
|
||||
return ExcelUtil.export_list2excel(list_data=data, mapping_dict=mapping_dict)
|
||||
|
||||
@classmethod
|
||||
async def batch_import_yifan_naming_solutions_service(cls, auth: AuthSchema, file: UploadFile, update_support: bool = False) -> str:
|
||||
"""批量导入"""
|
||||
header_dict = {
|
||||
'主键ID': 'id',
|
||||
'创建时间': 'created_time',
|
||||
'更新时间': 'updated_time',
|
||||
'创建人ID': 'created_id',
|
||||
'更新人ID': 'updated_id',
|
||||
'是否删除(0否 1是)': 'is_deleted',
|
||||
'状态(0禁用 1启用)': 'status',
|
||||
'关联报告ID': 'report_id',
|
||||
'姓名': 'name',
|
||||
'拼音': 'pinyin',
|
||||
'总分': 'total_score',
|
||||
'星级(1-5)': 'star_rating',
|
||||
'五行(如:火土)': 'wuxing',
|
||||
'属相(如:龙)': 'shuxiang',
|
||||
'标签(如:["改运补禄","平安顺遂"])': 'tags',
|
||||
'名字寓意': 'name_meaning',
|
||||
'诗词出处': 'poetry_source',
|
||||
'是否推荐(0否 1是)': 'is_recommended',
|
||||
'排序': 'sort_order',
|
||||
}
|
||||
|
||||
try:
|
||||
contents = await file.read()
|
||||
df = pd.read_excel(io.BytesIO(contents))
|
||||
await file.close()
|
||||
|
||||
if df.empty:
|
||||
raise CustomException(msg="导入文件为空")
|
||||
|
||||
missing_headers = [header for header in header_dict.keys() if header not in df.columns]
|
||||
if missing_headers:
|
||||
raise CustomException(msg=f"导入文件缺少必要的列: {', '.join(missing_headers)}")
|
||||
|
||||
df.rename(columns=header_dict, inplace=True)
|
||||
|
||||
# 验证必填字段
|
||||
|
||||
error_msgs = []
|
||||
success_count = 0
|
||||
count = 0
|
||||
|
||||
for index, row in df.iterrows():
|
||||
count += 1
|
||||
try:
|
||||
data = {
|
||||
"id": row['id'],
|
||||
"created_time": row['created_time'],
|
||||
"updated_time": row['updated_time'],
|
||||
"created_id": row['created_id'],
|
||||
"updated_id": row['updated_id'],
|
||||
"is_deleted": row['is_deleted'],
|
||||
"status": row['status'],
|
||||
"report_id": row['report_id'],
|
||||
"name": row['name'],
|
||||
"pinyin": row['pinyin'],
|
||||
"total_score": row['total_score'],
|
||||
"star_rating": row['star_rating'],
|
||||
"wuxing": row['wuxing'],
|
||||
"shuxiang": row['shuxiang'],
|
||||
"tags": row['tags'],
|
||||
"name_meaning": row['name_meaning'],
|
||||
"poetry_source": row['poetry_source'],
|
||||
"is_recommended": row['is_recommended'],
|
||||
"sort_order": row['sort_order'],
|
||||
}
|
||||
# 使用CreateSchema做校验后入库
|
||||
create_schema = YifanNamingSolutionsCreateSchema.model_validate(data)
|
||||
|
||||
# 检查唯一性约束
|
||||
|
||||
await YifanNamingSolutionsCRUD(auth).create_yifan_naming_solutions_crud(data=create_schema)
|
||||
success_count += 1
|
||||
except Exception as e:
|
||||
error_msgs.append(f"第{count}行: {str(e)}")
|
||||
continue
|
||||
|
||||
result = f"成功导入 {success_count} 条数据"
|
||||
if error_msgs:
|
||||
result += "\n错误信息:\n" + "\n".join(error_msgs)
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
log.error(f"批量导入失败: {str(e)}")
|
||||
raise CustomException(msg=f"导入失败: {str(e)}")
|
||||
|
||||
@classmethod
|
||||
async def import_template_download_yifan_naming_solutions_service(cls) -> bytes:
|
||||
"""下载导入模板"""
|
||||
header_list = [
|
||||
'主键ID',
|
||||
'创建时间',
|
||||
'更新时间',
|
||||
'创建人ID',
|
||||
'更新人ID',
|
||||
'是否删除(0否 1是)',
|
||||
'状态(0禁用 1启用)',
|
||||
'关联报告ID',
|
||||
'姓名',
|
||||
'拼音',
|
||||
'总分',
|
||||
'星级(1-5)',
|
||||
'五行(如:火土)',
|
||||
'属相(如:龙)',
|
||||
'标签(如:["改运补禄","平安顺遂"])',
|
||||
'名字寓意',
|
||||
'诗词出处',
|
||||
'是否推荐(0否 1是)',
|
||||
'排序',
|
||||
]
|
||||
selector_header_list = []
|
||||
option_list = []
|
||||
|
||||
# 添加下拉选项
|
||||
|
||||
return ExcelUtil.get_excel_template(
|
||||
header_list=header_list,
|
||||
selector_header_list=selector_header_list,
|
||||
option_list=option_list
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def get_solution_full_detail_service(cls, auth: AuthSchema, solution_id: int) -> dict:
|
||||
"""
|
||||
获取方案完整详情(包含板块和详情)
|
||||
|
||||
参数:
|
||||
- auth: 认证信息
|
||||
- solution_id: 方案ID
|
||||
|
||||
返回:
|
||||
- dict: 方案完整详情
|
||||
"""
|
||||
user_id = auth.user.id if auth.user else None
|
||||
result = await YifanNamingSolutionsCRUD(auth).get_solution_full_detail_crud(
|
||||
solution_id=solution_id,
|
||||
user_id=user_id
|
||||
)
|
||||
if not result:
|
||||
raise CustomException(msg="方案不存在")
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
async def generate_solution_pdf_service(cls, auth: AuthSchema, solution_id: int) -> bytes:
|
||||
"""
|
||||
生成方案PDF报告
|
||||
|
||||
参数:
|
||||
- auth: 认证信息
|
||||
- solution_id: 方案ID
|
||||
|
||||
返回:
|
||||
- bytes: PDF文件字节
|
||||
"""
|
||||
# 获取方案详情
|
||||
user_id = auth.user.id if auth.user else None
|
||||
solution = await YifanNamingSolutionsCRUD(auth).get_solution_full_detail_crud(
|
||||
solution_id=solution_id,
|
||||
user_id=user_id
|
||||
)
|
||||
if not solution:
|
||||
raise CustomException(msg="方案不存在")
|
||||
|
||||
# PDF模板路径
|
||||
template_path = os.path.join(
|
||||
os.path.dirname(__file__),
|
||||
'..', '..', '..', '..', '..',
|
||||
'static', 'pdf', 'example.pdf'
|
||||
)
|
||||
|
||||
if not os.path.exists(template_path):
|
||||
raise CustomException(msg="PDF模板文件不存在")
|
||||
|
||||
# 准备填充数据
|
||||
data = {
|
||||
'name': solution.get('name', ''),
|
||||
'pinyin': solution.get('pinyin', ''),
|
||||
'total_score': str(solution.get('total_score', '')),
|
||||
'star_rating': '★' * solution.get('star_rating', 0),
|
||||
'wuxing': solution.get('wuxing', ''),
|
||||
'shuxiang': solution.get('shuxiang', ''),
|
||||
'tags': '、'.join(solution.get('tags', [])) if solution.get('tags') else '',
|
||||
'name_meaning': solution.get('name_meaning', ''),
|
||||
'poetry_source': solution.get('poetry_source', ''),
|
||||
}
|
||||
|
||||
# 字段位置配置 (x, y, 字体大小, 颜色)
|
||||
# 注意: 这些位置需要根据实际PDF模板调整
|
||||
field_positions = {
|
||||
'name': (50, 250, 24, '#333333'),
|
||||
'pinyin': (50, 240, 12, '#666666'),
|
||||
'total_score': (150, 220, 18, '#ff6600'),
|
||||
'star_rating': (50, 200, 14, '#ffcc00'),
|
||||
'wuxing': (50, 180, 12, '#333333'),
|
||||
'shuxiang': (120, 180, 12, '#333333'),
|
||||
'tags': (50, 160, 10, '#999999'),
|
||||
'name_meaning': (50, 130, 11, '#333333'),
|
||||
'poetry_source': (50, 100, 10, '#666666'),
|
||||
}
|
||||
|
||||
# 生成PDF
|
||||
pdf_bytes = PDFUtil.fill_pdf_template(
|
||||
template_path=template_path,
|
||||
data=data,
|
||||
field_positions=field_positions
|
||||
)
|
||||
|
||||
log.info(f"生成方案PDF成功, 方案ID: {solution_id}")
|
||||
return pdf_bytes
|
||||
@@ -0,0 +1 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
@@ -0,0 +1,123 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from fastapi import APIRouter, Depends, Body, Path
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
from app.common.response import SuccessResponse
|
||||
from app.core.dependencies import AuthPermission
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from app.core.base_params import PaginationQueryParam
|
||||
from app.core.logger import log
|
||||
from app.core.base_schema import BatchSetAvailable
|
||||
|
||||
from .service import YifanPartnerApplyService
|
||||
from .schema import (
|
||||
YifanPartnerApplyCreateSchema,
|
||||
YifanPartnerApplyUpdateSchema,
|
||||
YifanPartnerApplyQueryParam,
|
||||
YifanPartnerApplyAuditSchema,
|
||||
)
|
||||
|
||||
|
||||
YifanPartnerApplyRouter = APIRouter(prefix='/yifan_partner_apply', tags=["合伙人申请模块"])
|
||||
|
||||
|
||||
@YifanPartnerApplyRouter.post("/apply", summary="提交合伙人申请", description="小程序-提交合伙人申请")
|
||||
async def create_yifan_partner_apply_for_mini(
|
||||
data: YifanPartnerApplyCreateSchema,
|
||||
auth: AuthSchema = Depends(AuthPermission([]))
|
||||
) -> JSONResponse:
|
||||
result_dict = await YifanPartnerApplyService.create_yifan_partner_apply_service(auth=auth, data=data)
|
||||
log.info("提交合伙人申请成功")
|
||||
return SuccessResponse(data=result_dict, msg="提交成功")
|
||||
|
||||
|
||||
@YifanPartnerApplyRouter.get("/mini/my", summary="我的合伙人申请", description="小程序-获取当前用户的申请记录")
|
||||
async def get_my_partner_apply_list_for_mini(
|
||||
auth: AuthSchema = Depends(AuthPermission([]))
|
||||
) -> JSONResponse:
|
||||
result_list = await YifanPartnerApplyService.get_my_apply_list_service(auth=auth)
|
||||
return SuccessResponse(data=result_list, msg="获取成功")
|
||||
|
||||
|
||||
@YifanPartnerApplyRouter.get("/detail/{id}", summary="获取合伙人申请详情", description="获取合伙人申请详情")
|
||||
async def get_yifan_partner_apply_detail_controller(
|
||||
id: int = Path(..., description="ID"),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_partner_apply:query"]))
|
||||
) -> JSONResponse:
|
||||
result_dict = await YifanPartnerApplyService.detail_yifan_partner_apply_service(auth=auth, id=id)
|
||||
log.info(f"获取合伙人申请详情成功 {id}")
|
||||
return SuccessResponse(data=result_dict, msg="获取合伙人申请详情成功")
|
||||
|
||||
|
||||
@YifanPartnerApplyRouter.get("/list", summary="查询合伙人申请列表", description="查询合伙人申请列表")
|
||||
async def get_yifan_partner_apply_list_controller(
|
||||
page: PaginationQueryParam = Depends(),
|
||||
search: YifanPartnerApplyQueryParam = Depends(),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_partner_apply:query"]))
|
||||
) -> JSONResponse:
|
||||
result_dict = await YifanPartnerApplyService.page_yifan_partner_apply_service(
|
||||
auth=auth,
|
||||
page_no=page.page_no if page.page_no is not None else 1,
|
||||
page_size=page.page_size if page.page_size is not None else 10,
|
||||
search=search,
|
||||
order_by=page.order_by
|
||||
)
|
||||
log.info("查询合伙人申请列表成功")
|
||||
return SuccessResponse(data=result_dict, msg="查询成功")
|
||||
|
||||
|
||||
@YifanPartnerApplyRouter.post("/create", summary="创建合伙人申请", description="后台-创建合伙人申请")
|
||||
async def create_yifan_partner_apply_controller(
|
||||
data: YifanPartnerApplyCreateSchema,
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_partner_apply:create"]))
|
||||
) -> JSONResponse:
|
||||
result_dict = await YifanPartnerApplyService.create_yifan_partner_apply_service(auth=auth, data=data)
|
||||
log.info("创建合伙人申请成功")
|
||||
return SuccessResponse(data=result_dict, msg="创建成功")
|
||||
|
||||
|
||||
@YifanPartnerApplyRouter.put("/update/{id}", summary="修改合伙人申请", description="修改合伙人申请")
|
||||
async def update_yifan_partner_apply_controller(
|
||||
data: YifanPartnerApplyUpdateSchema,
|
||||
id: int = Path(..., description="ID"),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_partner_apply:update"]))
|
||||
) -> JSONResponse:
|
||||
result_dict = await YifanPartnerApplyService.update_yifan_partner_apply_service(auth=auth, id=id, data=data)
|
||||
log.info("修改合伙人申请成功")
|
||||
return SuccessResponse(data=result_dict, msg="修改成功")
|
||||
|
||||
|
||||
@YifanPartnerApplyRouter.delete("/delete", summary="删除合伙人申请", description="删除合伙人申请")
|
||||
async def delete_yifan_partner_apply_controller(
|
||||
ids: list[int] = Body(..., description="ID列表"),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_partner_apply:delete"]))
|
||||
) -> JSONResponse:
|
||||
await YifanPartnerApplyService.delete_yifan_partner_apply_service(auth=auth, ids=ids)
|
||||
log.info(f"删除合伙人申请成功: {ids}")
|
||||
return SuccessResponse(msg="删除成功")
|
||||
|
||||
|
||||
@YifanPartnerApplyRouter.patch("/available/setting", summary="批量修改合伙人申请状态", description="批量修改合伙人申请状态")
|
||||
async def batch_set_available_yifan_partner_apply_controller(
|
||||
data: BatchSetAvailable,
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_partner_apply:patch"]))
|
||||
) -> JSONResponse:
|
||||
await YifanPartnerApplyService.set_available_yifan_partner_apply_service(auth=auth, data=data)
|
||||
log.info(f"批量修改合伙人申请状态成功: {data.ids}")
|
||||
return SuccessResponse(msg="操作成功")
|
||||
|
||||
|
||||
@YifanPartnerApplyRouter.patch("/audit/{id}", summary="审核合伙人申请", description="审核合伙人申请")
|
||||
async def audit_yifan_partner_apply_controller(
|
||||
data: YifanPartnerApplyAuditSchema,
|
||||
id: int = Path(..., description="ID"),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_partner_apply:audit"]))
|
||||
) -> JSONResponse:
|
||||
result_dict = await YifanPartnerApplyService.audit_yifan_partner_apply_service(
|
||||
auth=auth,
|
||||
id=id,
|
||||
audit_status=data.audit_status,
|
||||
partner_role=data.partner_role,
|
||||
)
|
||||
return SuccessResponse(data=result_dict, msg="审核成功")
|
||||
@@ -0,0 +1,76 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from typing import Sequence
|
||||
|
||||
from sqlalchemy import select, or_
|
||||
|
||||
from app.core.base_crud import CRUDBase
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
|
||||
from .model import YifanPartnerApplyModel
|
||||
from .schema import (
|
||||
YifanPartnerApplyCreateSchema,
|
||||
YifanPartnerApplyUpdateSchema,
|
||||
YifanPartnerApplyOutSchema,
|
||||
)
|
||||
|
||||
|
||||
class YifanPartnerApplyCRUD(CRUDBase[YifanPartnerApplyModel, YifanPartnerApplyCreateSchema, YifanPartnerApplyUpdateSchema]):
|
||||
"""合伙人申请数据层"""
|
||||
|
||||
def __init__(self, auth: AuthSchema) -> None:
|
||||
super().__init__(model=YifanPartnerApplyModel, auth=auth)
|
||||
|
||||
async def get_by_id_yifan_partner_apply_crud(self, id: int, preload: list | None = None) -> YifanPartnerApplyModel | None:
|
||||
return await self.get(id=id, preload=preload)
|
||||
|
||||
async def list_yifan_partner_apply_crud(self, search: dict | None = None, order_by: list[dict] | None = None, preload: list | None = None) -> Sequence[YifanPartnerApplyModel]:
|
||||
return await self.list(search=search, order_by=order_by, preload=preload)
|
||||
|
||||
async def page_yifan_partner_apply_crud(self, offset: int, limit: int, order_by: list[dict] | None = None, search: dict | None = None, preload: list | None = None) -> dict:
|
||||
order_by_list = order_by or [{'id': 'asc'}]
|
||||
search_dict = search or {}
|
||||
return await self.page(
|
||||
offset=offset,
|
||||
limit=limit,
|
||||
order_by=order_by_list,
|
||||
search=search_dict,
|
||||
out_schema=YifanPartnerApplyOutSchema,
|
||||
preload=preload
|
||||
)
|
||||
|
||||
async def create_yifan_partner_apply_crud(self, data: YifanPartnerApplyCreateSchema) -> YifanPartnerApplyModel | None:
|
||||
return await self.create(data=data)
|
||||
|
||||
async def update_yifan_partner_apply_crud(self, id: int, data: YifanPartnerApplyUpdateSchema) -> YifanPartnerApplyModel | None:
|
||||
return await self.update(id=id, data=data)
|
||||
|
||||
async def delete_yifan_partner_apply_crud(self, ids: list[int]) -> None:
|
||||
return await self.delete(ids=ids)
|
||||
|
||||
async def set_available_yifan_partner_apply_crud(self, ids: list[int], status: str) -> None:
|
||||
return await self.set(ids=ids, status=status)
|
||||
|
||||
async def set_audit_status_yifan_partner_apply_crud(self, id: int, audit_status: int) -> YifanPartnerApplyModel | None:
|
||||
return await self.update(id=id, data={"audit_status": audit_status})
|
||||
|
||||
async def exists_duplicate_apply_crud(self, user_id: int | None, phone: str | None) -> bool:
|
||||
conditions = []
|
||||
if user_id is not None:
|
||||
conditions.append(YifanPartnerApplyModel.user_id == user_id)
|
||||
if phone:
|
||||
conditions.append(YifanPartnerApplyModel.phone == phone)
|
||||
|
||||
if not conditions:
|
||||
return False
|
||||
|
||||
stmt = (
|
||||
select(YifanPartnerApplyModel.id)
|
||||
.where(
|
||||
YifanPartnerApplyModel.is_deleted == 0,
|
||||
or_(*conditions),
|
||||
)
|
||||
.limit(1)
|
||||
)
|
||||
result = await self.auth.db.execute(stmt)
|
||||
return result.scalar() is not None
|
||||
@@ -0,0 +1,23 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from sqlalchemy import Integer, SmallInteger, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.base_model import ModelMixin, UserMixin
|
||||
|
||||
|
||||
class YifanPartnerApplyModel(ModelMixin, UserMixin):
|
||||
"""合伙人申请表"""
|
||||
|
||||
__tablename__: str = 'yifan_partner_apply'
|
||||
__table_args__: dict[str, str] = {'comment': '合伙人申请表'}
|
||||
__loader_options__: list[str] = ["created_by", "updated_by"]
|
||||
|
||||
is_deleted: Mapped[int | None] = mapped_column(SmallInteger, nullable=True, default=0, comment='是否删除(0:否 1:是)')
|
||||
|
||||
user_id: Mapped[int | None] = mapped_column(Integer, nullable=True, index=True, comment='用户ID(关联微信用户)')
|
||||
real_name: Mapped[str] = mapped_column(String(50), nullable=False, comment='真实姓名')
|
||||
phone: Mapped[str] = mapped_column(String(20), nullable=False, index=True, comment='手机号码')
|
||||
wechat_id: Mapped[str | None] = mapped_column(String(50), nullable=True, comment='微信号(选填)')
|
||||
|
||||
audit_status: Mapped[int | None] = mapped_column(SmallInteger, nullable=True, default=0, index=True, comment='审核状态(0:待审核 1:审核通过 2:审核拒绝)')
|
||||
@@ -0,0 +1,74 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from fastapi import Query
|
||||
|
||||
from app.core.validator import DateTimeStr
|
||||
from app.core.base_schema import BaseSchema, UserBySchema
|
||||
|
||||
|
||||
class YifanPartnerApplyCreateSchema(BaseModel):
|
||||
"""合伙人申请新增模型"""
|
||||
|
||||
is_deleted: int = Field(default=0, description='是否删除(0:否 1:是)')
|
||||
status: int = Field(default=0, description='状态(0:启用 1:禁用)')
|
||||
|
||||
user_id: int | None = Field(default=None, description='用户ID(关联微信用户)')
|
||||
real_name: str = Field(default=..., description='真实姓名')
|
||||
phone: str = Field(default=..., description='手机号码')
|
||||
wechat_id: str | None = Field(default=None, description='微信号(选填)')
|
||||
|
||||
audit_status: int = Field(default=0, description='审核状态(0:待审核 1:审核通过 2:审核拒绝)')
|
||||
|
||||
|
||||
class YifanPartnerApplyUpdateSchema(YifanPartnerApplyCreateSchema):
|
||||
"""合伙人申请更新模型"""
|
||||
|
||||
...
|
||||
|
||||
|
||||
class YifanPartnerApplyOutSchema(YifanPartnerApplyCreateSchema, BaseSchema, UserBySchema):
|
||||
"""合伙人申请响应模型"""
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class YifanPartnerApplyQueryParam:
|
||||
"""合伙人申请查询参数"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
user_id: int | None = Query(None, description="用户ID"),
|
||||
real_name: str | None = Query(None, description="真实姓名"),
|
||||
phone: str | None = Query(None, description="手机号码"),
|
||||
wechat_id: str | None = Query(None, description="微信号"),
|
||||
audit_status: int | None = Query(None, description="审核状态(0:待审核 1:审核通过 2:审核拒绝)"),
|
||||
created_id: int | None = Query(None, description="创建人ID"),
|
||||
updated_id: int | None = Query(None, description="更新人ID"),
|
||||
is_deleted: int | None = Query(None, description="是否删除(0:否 1:是)"),
|
||||
status: int | None = Query(None, description="状态(0:禁用 1:启用)"),
|
||||
created_time: list[DateTimeStr] | None = Query(None, description="创建时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]),
|
||||
updated_time: list[DateTimeStr] | None = Query(None, description="更新时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]),
|
||||
) -> None:
|
||||
self.created_id = created_id
|
||||
self.updated_id = updated_id
|
||||
self.is_deleted = is_deleted
|
||||
self.status = status
|
||||
|
||||
self.user_id = user_id
|
||||
self.real_name = ("like", real_name)
|
||||
self.phone = ("like", phone)
|
||||
self.wechat_id = ("like", wechat_id)
|
||||
self.audit_status = audit_status
|
||||
|
||||
if created_time and len(created_time) == 2:
|
||||
self.created_time = ("between", (created_time[0], created_time[1]))
|
||||
if updated_time and len(updated_time) == 2:
|
||||
self.updated_time = ("between", (updated_time[0], updated_time[1]))
|
||||
|
||||
|
||||
class YifanPartnerApplyAuditSchema(BaseModel):
|
||||
"""合伙人申请审核请求模型"""
|
||||
|
||||
audit_status: int = Field(default=..., description='审核状态(1:审核通过 2:审核拒绝)')
|
||||
partner_role: str | None = Field(default=None, description='身份(推广大使/合伙人 或 ambassador/partner)')
|
||||
@@ -0,0 +1,142 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from app.core.base_schema import BatchSetAvailable
|
||||
from app.core.exceptions import CustomException
|
||||
from app.core.logger import log
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from app.api.v1.module_system.user.crud import UserCRUD
|
||||
|
||||
from .crud import YifanPartnerApplyCRUD
|
||||
from .schema import (
|
||||
YifanPartnerApplyCreateSchema,
|
||||
YifanPartnerApplyUpdateSchema,
|
||||
YifanPartnerApplyOutSchema,
|
||||
YifanPartnerApplyQueryParam,
|
||||
)
|
||||
|
||||
|
||||
class YifanPartnerApplyService:
|
||||
"""合伙人申请服务层"""
|
||||
|
||||
@classmethod
|
||||
async def detail_yifan_partner_apply_service(cls, auth: AuthSchema, id: int) -> dict:
|
||||
obj = await YifanPartnerApplyCRUD(auth).get_by_id_yifan_partner_apply_crud(id=id)
|
||||
if not obj:
|
||||
raise CustomException(msg="该数据不存在")
|
||||
return YifanPartnerApplyOutSchema.model_validate(obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def list_yifan_partner_apply_service(cls, auth: AuthSchema, search: YifanPartnerApplyQueryParam | None = None, order_by: list[dict] | None = None) -> list[dict]:
|
||||
search_dict = search.__dict__ if search else None
|
||||
obj_list = await YifanPartnerApplyCRUD(auth).list_yifan_partner_apply_crud(search=search_dict, order_by=order_by)
|
||||
return [YifanPartnerApplyOutSchema.model_validate(obj).model_dump() for obj in obj_list]
|
||||
|
||||
@classmethod
|
||||
async def page_yifan_partner_apply_service(cls, auth: AuthSchema, page_no: int, page_size: int, search: YifanPartnerApplyQueryParam | None = None, order_by: list[dict] | None = None) -> dict:
|
||||
search_dict = search.__dict__ if search else {}
|
||||
order_by_list = order_by or [{'id': 'asc'}]
|
||||
offset = (page_no - 1) * page_size
|
||||
return await YifanPartnerApplyCRUD(auth).page_yifan_partner_apply_crud(
|
||||
offset=offset,
|
||||
limit=page_size,
|
||||
order_by=order_by_list,
|
||||
search=search_dict,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def create_yifan_partner_apply_service(cls, auth: AuthSchema, data: YifanPartnerApplyCreateSchema) -> dict:
|
||||
create_data = data.model_dump()
|
||||
if auth.user and not create_data.get("user_id"):
|
||||
create_data["user_id"] = auth.user.id
|
||||
create_data["audit_status"] = 0
|
||||
|
||||
# 已申请过(包含已审核/待审核)则禁止重复申请
|
||||
auth_no_scope = AuthSchema(db=auth.db, user=auth.user, check_data_scope=False)
|
||||
exists = await YifanPartnerApplyCRUD(auth_no_scope).exists_duplicate_apply_crud(
|
||||
user_id=create_data.get("user_id"),
|
||||
phone=create_data.get("phone"),
|
||||
)
|
||||
if exists:
|
||||
raise CustomException(msg="已提交过合伙人申请,不能重复申请")
|
||||
|
||||
obj = await YifanPartnerApplyCRUD(auth).create_yifan_partner_apply_crud(
|
||||
data=YifanPartnerApplyCreateSchema.model_validate(create_data)
|
||||
)
|
||||
return YifanPartnerApplyOutSchema.model_validate(obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def update_yifan_partner_apply_service(cls, auth: AuthSchema, id: int, data: YifanPartnerApplyUpdateSchema) -> dict:
|
||||
obj = await YifanPartnerApplyCRUD(auth).get_by_id_yifan_partner_apply_crud(id=id)
|
||||
if not obj:
|
||||
raise CustomException(msg='更新失败,该数据不存在')
|
||||
|
||||
obj = await YifanPartnerApplyCRUD(auth).update_yifan_partner_apply_crud(id=id, data=data)
|
||||
return YifanPartnerApplyOutSchema.model_validate(obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def delete_yifan_partner_apply_service(cls, auth: AuthSchema, ids: list[int]) -> None:
|
||||
if len(ids) < 1:
|
||||
raise CustomException(msg='删除失败,删除对象不能为空')
|
||||
for id in ids:
|
||||
obj = await YifanPartnerApplyCRUD(auth).get_by_id_yifan_partner_apply_crud(id=id)
|
||||
if not obj:
|
||||
raise CustomException(msg=f'删除失败,ID为{id}的数据不存在')
|
||||
await YifanPartnerApplyCRUD(auth).delete_yifan_partner_apply_crud(ids=ids)
|
||||
|
||||
@classmethod
|
||||
async def set_available_yifan_partner_apply_service(cls, auth: AuthSchema, data: BatchSetAvailable) -> None:
|
||||
await YifanPartnerApplyCRUD(auth).set_available_yifan_partner_apply_crud(ids=data.ids, status=data.status)
|
||||
|
||||
@classmethod
|
||||
async def audit_yifan_partner_apply_service(
|
||||
cls,
|
||||
auth: AuthSchema,
|
||||
id: int,
|
||||
audit_status: int,
|
||||
partner_role: str | None = None,
|
||||
) -> dict:
|
||||
if audit_status not in (1, 2):
|
||||
raise CustomException(msg='审核状态不合法')
|
||||
|
||||
obj = await YifanPartnerApplyCRUD(auth).get_by_id_yifan_partner_apply_crud(id=id)
|
||||
if not obj:
|
||||
raise CustomException(msg='审核失败,该数据不存在')
|
||||
|
||||
if obj.audit_status != 0:
|
||||
raise CustomException(msg='审核失败,该申请已审核')
|
||||
|
||||
if audit_status == 1:
|
||||
if not partner_role:
|
||||
raise CustomException(msg='审核失败,缺少身份信息')
|
||||
|
||||
# 兼容:允许前端传中文或英文,最终统一存英文
|
||||
role_map = {
|
||||
"推广大使": "ambassador",
|
||||
"合伙人": "partner",
|
||||
"ambassador": "ambassador",
|
||||
"partner": "partner",
|
||||
}
|
||||
mapped_role = role_map.get(partner_role)
|
||||
if not mapped_role:
|
||||
raise CustomException(msg='审核失败,身份信息不合法')
|
||||
|
||||
if not obj.user_id:
|
||||
raise CustomException(msg='审核失败,缺少用户信息')
|
||||
|
||||
auth_no_scope = AuthSchema(db=auth.db, user=auth.user, check_data_scope=False)
|
||||
await UserCRUD(auth_no_scope).update(id=obj.user_id, data={"partner_role": mapped_role})
|
||||
|
||||
obj = await YifanPartnerApplyCRUD(auth).set_audit_status_yifan_partner_apply_crud(id=id, audit_status=audit_status)
|
||||
log.info(f"合伙人申请审核完成 id={id} audit_status={audit_status}")
|
||||
return YifanPartnerApplyOutSchema.model_validate(obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def get_my_apply_list_service(cls, auth: AuthSchema) -> list[dict]:
|
||||
if not auth.user:
|
||||
raise CustomException(msg='未登录')
|
||||
|
||||
obj_list = await YifanPartnerApplyCRUD(auth).list_yifan_partner_apply_crud(
|
||||
search={"user_id": auth.user.id},
|
||||
order_by=[{"id": "desc"}]
|
||||
)
|
||||
return [YifanPartnerApplyOutSchema.model_validate(obj).model_dump() for obj in obj_list]
|
||||
@@ -0,0 +1 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
@@ -0,0 +1,141 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from fastapi import APIRouter, Depends, UploadFile, Body, Path, Query
|
||||
from fastapi.responses import StreamingResponse, JSONResponse
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.common.response import SuccessResponse, StreamResponse
|
||||
from app.core.dependencies import AuthPermission, db_getter
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from app.core.base_params import PaginationQueryParam
|
||||
from app.utils.common_util import bytes2file_response
|
||||
from app.core.logger import log
|
||||
from app.core.base_schema import BatchSetAvailable
|
||||
|
||||
from .service import YifanPrivacyPolicyService
|
||||
from .schema import YifanPrivacyPolicyCreateSchema, YifanPrivacyPolicyUpdateSchema, YifanPrivacyPolicyQueryParam
|
||||
|
||||
YifanPrivacyPolicyRouter = APIRouter(prefix='/yifan_privacy_policy', tags=["隐私政策模块"])
|
||||
|
||||
|
||||
@YifanPrivacyPolicyRouter.get("/mini/current", summary="小程序-获取当前隐私政策", description="根据类型获取当前生效的隐私政策或用户协议")
|
||||
async def get_current_policy_for_mini(
|
||||
policy_type: str = Query(default="privacy", description="协议类型: privacy-隐私政策, terms-用户协议"),
|
||||
db: AsyncSession = Depends(db_getter)
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
小程序获取当前隐私政策/用户协议接口
|
||||
|
||||
无需登录,返回当前生效版本的协议内容
|
||||
"""
|
||||
result = await YifanPrivacyPolicyService.get_current_policy_service(db=db, policy_type=policy_type)
|
||||
return SuccessResponse(data=result, msg="获取成功")
|
||||
|
||||
@YifanPrivacyPolicyRouter.get("/detail/{id}", summary="获取隐私政策详情", description="获取隐私政策详情")
|
||||
async def get_yifan_privacy_policy_detail_controller(
|
||||
id: int = Path(..., description="ID"),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_privacy_policy:query"]))
|
||||
) -> JSONResponse:
|
||||
"""获取隐私政策详情接口"""
|
||||
result_dict = await YifanPrivacyPolicyService.detail_yifan_privacy_policy_service(auth=auth, id=id)
|
||||
log.info(f"获取隐私政策详情成功 {id}")
|
||||
return SuccessResponse(data=result_dict, msg="获取隐私政策详情成功")
|
||||
|
||||
@YifanPrivacyPolicyRouter.get("/list", summary="查询隐私政策列表", description="查询隐私政策列表")
|
||||
async def get_yifan_privacy_policy_list_controller(
|
||||
page: PaginationQueryParam = Depends(),
|
||||
search: YifanPrivacyPolicyQueryParam = Depends(),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_privacy_policy:query"]))
|
||||
) -> JSONResponse:
|
||||
"""查询隐私政策列表接口(数据库分页)"""
|
||||
result_dict = await YifanPrivacyPolicyService.page_yifan_privacy_policy_service(
|
||||
auth=auth,
|
||||
page_no=page.page_no if page.page_no is not None else 1,
|
||||
page_size=page.page_size if page.page_size is not None else 10,
|
||||
search=search,
|
||||
order_by=page.order_by
|
||||
)
|
||||
log.info("查询隐私政策列表成功")
|
||||
return SuccessResponse(data=result_dict, msg="查询隐私政策列表成功")
|
||||
|
||||
@YifanPrivacyPolicyRouter.post("/create", summary="创建隐私政策", description="创建隐私政策")
|
||||
async def create_yifan_privacy_policy_controller(
|
||||
data: YifanPrivacyPolicyCreateSchema,
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_privacy_policy:create"]))
|
||||
) -> JSONResponse:
|
||||
"""创建隐私政策接口"""
|
||||
result_dict = await YifanPrivacyPolicyService.create_yifan_privacy_policy_service(auth=auth, data=data)
|
||||
log.info("创建隐私政策成功")
|
||||
return SuccessResponse(data=result_dict, msg="创建隐私政策成功")
|
||||
|
||||
@YifanPrivacyPolicyRouter.put("/update/{id}", summary="修改隐私政策", description="修改隐私政策")
|
||||
async def update_yifan_privacy_policy_controller(
|
||||
data: YifanPrivacyPolicyUpdateSchema,
|
||||
id: int = Path(..., description="ID"),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_privacy_policy:update"]))
|
||||
) -> JSONResponse:
|
||||
"""修改隐私政策接口"""
|
||||
result_dict = await YifanPrivacyPolicyService.update_yifan_privacy_policy_service(auth=auth, id=id, data=data)
|
||||
log.info("修改隐私政策成功")
|
||||
return SuccessResponse(data=result_dict, msg="修改隐私政策成功")
|
||||
|
||||
@YifanPrivacyPolicyRouter.delete("/delete", summary="删除隐私政策", description="删除隐私政策")
|
||||
async def delete_yifan_privacy_policy_controller(
|
||||
ids: list[int] = Body(..., description="ID列表"),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_privacy_policy:delete"]))
|
||||
) -> JSONResponse:
|
||||
"""删除隐私政策接口"""
|
||||
await YifanPrivacyPolicyService.delete_yifan_privacy_policy_service(auth=auth, ids=ids)
|
||||
log.info(f"删除隐私政策成功: {ids}")
|
||||
return SuccessResponse(msg="删除隐私政策成功")
|
||||
|
||||
@YifanPrivacyPolicyRouter.patch("/available/setting", summary="批量修改隐私政策状态", description="批量修改隐私政策状态")
|
||||
async def batch_set_available_yifan_privacy_policy_controller(
|
||||
data: BatchSetAvailable,
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_privacy_policy:patch"]))
|
||||
) -> JSONResponse:
|
||||
"""批量修改隐私政策状态接口"""
|
||||
await YifanPrivacyPolicyService.set_available_yifan_privacy_policy_service(auth=auth, data=data)
|
||||
log.info(f"批量修改隐私政策状态成功: {data.ids}")
|
||||
return SuccessResponse(msg="批量修改隐私政策状态成功")
|
||||
|
||||
@YifanPrivacyPolicyRouter.post('/export', summary="导出隐私政策", description="导出隐私政策")
|
||||
async def export_yifan_privacy_policy_list_controller(
|
||||
search: YifanPrivacyPolicyQueryParam = Depends(),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_privacy_policy:export"]))
|
||||
) -> StreamingResponse:
|
||||
"""导出隐私政策接口"""
|
||||
result_dict_list = await YifanPrivacyPolicyService.list_yifan_privacy_policy_service(search=search, auth=auth)
|
||||
export_result = await YifanPrivacyPolicyService.batch_export_yifan_privacy_policy_service(obj_list=result_dict_list)
|
||||
log.info('导出隐私政策成功')
|
||||
|
||||
return StreamResponse(
|
||||
data=bytes2file_response(export_result),
|
||||
media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
headers={
|
||||
'Content-Disposition': 'attachment; filename=yifan_privacy_policy.xlsx'
|
||||
}
|
||||
)
|
||||
|
||||
@YifanPrivacyPolicyRouter.post('/import', summary="导入隐私政策", description="导入隐私政策")
|
||||
async def import_yifan_privacy_policy_list_controller(
|
||||
file: UploadFile,
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_privacy_policy:import"]))
|
||||
) -> JSONResponse:
|
||||
"""导入隐私政策接口"""
|
||||
batch_import_result = await YifanPrivacyPolicyService.batch_import_yifan_privacy_policy_service(file=file, auth=auth, update_support=True)
|
||||
log.info("导入隐私政策成功")
|
||||
|
||||
return SuccessResponse(data=batch_import_result, msg="导入隐私政策成功")
|
||||
|
||||
@YifanPrivacyPolicyRouter.post('/download/template', summary="获取隐私政策导入模板", description="获取隐私政策导入模板", dependencies=[Depends(AuthPermission(["module_yifan:yifan_privacy_policy:download"]))])
|
||||
async def export_yifan_privacy_policy_template_controller() -> StreamingResponse:
|
||||
"""获取隐私政策导入模板接口"""
|
||||
import_template_result = await YifanPrivacyPolicyService.import_template_download_yifan_privacy_policy_service()
|
||||
log.info('获取隐私政策导入模板成功')
|
||||
|
||||
return StreamResponse(
|
||||
data=bytes2file_response(import_template_result),
|
||||
media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
headers={'Content-Disposition': 'attachment; filename=yifan_privacy_policy_template.xlsx'}
|
||||
)
|
||||
@@ -0,0 +1,123 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from typing import Sequence
|
||||
|
||||
from app.core.base_crud import CRUDBase
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from .model import YifanPrivacyPolicyModel
|
||||
from .schema import YifanPrivacyPolicyCreateSchema, YifanPrivacyPolicyUpdateSchema, YifanPrivacyPolicyOutSchema
|
||||
|
||||
|
||||
class YifanPrivacyPolicyCRUD(CRUDBase[YifanPrivacyPolicyModel, YifanPrivacyPolicyCreateSchema, YifanPrivacyPolicyUpdateSchema]):
|
||||
"""隐私政策数据层"""
|
||||
|
||||
def __init__(self, auth: AuthSchema) -> None:
|
||||
"""
|
||||
初始化CRUD数据层
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
"""
|
||||
super().__init__(model=YifanPrivacyPolicyModel, auth=auth)
|
||||
|
||||
async def get_by_id_yifan_privacy_policy_crud(self, id: int, preload: list | None = None) -> YifanPrivacyPolicyModel | None:
|
||||
"""
|
||||
详情
|
||||
|
||||
参数:
|
||||
- id (int): 对象ID
|
||||
- preload (list | None): 预加载关系,未提供时使用模型默认项
|
||||
|
||||
返回:
|
||||
- YifanPrivacyPolicyModel | None: 模型实例或None
|
||||
"""
|
||||
return await self.get(id=id, preload=preload)
|
||||
|
||||
async def list_yifan_privacy_policy_crud(self, search: dict | None = None, order_by: list[dict] | None = None, preload: list | None = None) -> Sequence[YifanPrivacyPolicyModel]:
|
||||
"""
|
||||
列表查询
|
||||
|
||||
参数:
|
||||
- search (dict | None): 查询参数
|
||||
- order_by (list[dict] | None): 排序参数,未提供时使用模型默认项
|
||||
- preload (list | None): 预加载关系,未提供时使用模型默认项
|
||||
|
||||
返回:
|
||||
- Sequence[YifanPrivacyPolicyModel]: 模型实例序列
|
||||
"""
|
||||
return await self.list(search=search, order_by=order_by, preload=preload)
|
||||
|
||||
async def create_yifan_privacy_policy_crud(self, data: YifanPrivacyPolicyCreateSchema) -> YifanPrivacyPolicyModel | None:
|
||||
"""
|
||||
创建
|
||||
|
||||
参数:
|
||||
- data (YifanPrivacyPolicyCreateSchema): 创建模型
|
||||
|
||||
返回:
|
||||
- YifanPrivacyPolicyModel | None: 模型实例或None
|
||||
"""
|
||||
return await self.create(data=data)
|
||||
|
||||
async def update_yifan_privacy_policy_crud(self, id: int, data: YifanPrivacyPolicyUpdateSchema) -> YifanPrivacyPolicyModel | None:
|
||||
"""
|
||||
更新
|
||||
|
||||
参数:
|
||||
- id (int): 对象ID
|
||||
- data (YifanPrivacyPolicyUpdateSchema): 更新模型
|
||||
|
||||
返回:
|
||||
- YifanPrivacyPolicyModel | None: 模型实例或None
|
||||
"""
|
||||
return await self.update(id=id, data=data)
|
||||
|
||||
async def delete_yifan_privacy_policy_crud(self, ids: list[int]) -> None:
|
||||
"""
|
||||
批量删除
|
||||
|
||||
参数:
|
||||
- ids (list[int]): 对象ID列表
|
||||
|
||||
返回:
|
||||
- None
|
||||
"""
|
||||
return await self.delete(ids=ids)
|
||||
|
||||
async def set_available_yifan_privacy_policy_crud(self, ids: list[int], status: str) -> None:
|
||||
"""
|
||||
批量设置可用状态
|
||||
|
||||
参数:
|
||||
- ids (list[int]): 对象ID列表
|
||||
- status (str): 可用状态
|
||||
|
||||
返回:
|
||||
- None
|
||||
"""
|
||||
return await self.set(ids=ids, status=status)
|
||||
|
||||
async def page_yifan_privacy_policy_crud(self, offset: int, limit: int, order_by: list[dict] | None = None, search: dict | None = None, preload: list | None = None) -> dict:
|
||||
"""
|
||||
分页查询
|
||||
|
||||
参数:
|
||||
- offset (int): 偏移量
|
||||
- limit (int): 每页数量
|
||||
- order_by (list[dict] | None): 排序参数,未提供时使用模型默认项
|
||||
- search (dict | None): 查询参数,未提供时查询所有
|
||||
- preload (list | None): 预加载关系,未提供时使用模型默认项
|
||||
|
||||
返回:
|
||||
- Dict: 分页数据
|
||||
"""
|
||||
order_by_list = order_by or [{'id': 'asc'}]
|
||||
search_dict = search or {}
|
||||
return await self.page(
|
||||
offset=offset,
|
||||
limit=limit,
|
||||
order_by=order_by_list,
|
||||
search=search_dict,
|
||||
out_schema=YifanPrivacyPolicyOutSchema,
|
||||
preload=preload
|
||||
)
|
||||
@@ -0,0 +1,26 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import datetime
|
||||
from sqlalchemy import Integer, DateTime, SmallInteger, Text, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.base_model import ModelMixin, UserMixin
|
||||
|
||||
|
||||
class YifanPrivacyPolicyModel(ModelMixin, UserMixin):
|
||||
"""
|
||||
隐私政策表
|
||||
"""
|
||||
__tablename__: str = 'yifan_privacy_policy'
|
||||
__table_args__: dict[str, str] = {'comment': '隐私政策'}
|
||||
__loader_options__: list[str] = ["created_by", "updated_by"]
|
||||
|
||||
is_deleted: Mapped[int | None] = mapped_column(SmallInteger, nullable=True, comment='是否删除(0否 1是)')
|
||||
title: Mapped[str | None] = mapped_column(String(200), nullable=True, comment='标题')
|
||||
content: Mapped[str | None] = mapped_column(Text, nullable=True, comment='内容(支持富文本)')
|
||||
version: Mapped[str | None] = mapped_column(String(32), nullable=True, comment='版本号')
|
||||
policy_type: Mapped[str | None] = mapped_column(String(32), nullable=True, comment='协议类型(privacy:隐私政策 terms:用户协议 service:服务条款)')
|
||||
effective_time: Mapped[datetime.datetime | None] = mapped_column(DateTime, nullable=True, comment='生效时间')
|
||||
is_current: Mapped[int | None] = mapped_column(SmallInteger, nullable=True, comment='是否当前版本(0否 1是)')
|
||||
remark: Mapped[str | None] = mapped_column(String(255), nullable=True, comment='备注')
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import datetime
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from fastapi import Query
|
||||
|
||||
from app.core.validator import DateTimeStr
|
||||
from app.core.base_schema import BaseSchema, UserBySchema
|
||||
|
||||
class YifanPrivacyPolicyCreateSchema(BaseModel):
|
||||
"""
|
||||
隐私政策新增模型
|
||||
"""
|
||||
is_deleted: int = Field(default=0, description='是否删除(0否 1是)')
|
||||
status: int = Field(default=1, description='状态(0禁用 1启用)')
|
||||
title: str = Field(default=..., description='标题')
|
||||
content: str = Field(default=..., description='内容(支持富文本)')
|
||||
version: str = Field(default=..., description='版本号')
|
||||
policy_type: str = Field(default='privacy', description='协议类型(privacy:隐私政策 terms:用户协议 service:服务条款)')
|
||||
effective_time: datetime.datetime | None = Field(default=None, description='生效时间')
|
||||
is_current: int = Field(default=0, description='是否当前版本(0否 1是)')
|
||||
remark: str | None = Field(default=None, description='备注')
|
||||
|
||||
|
||||
class YifanPrivacyPolicyUpdateSchema(YifanPrivacyPolicyCreateSchema):
|
||||
"""
|
||||
隐私政策更新模型
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
class YifanPrivacyPolicyOutSchema(YifanPrivacyPolicyCreateSchema, BaseSchema, UserBySchema):
|
||||
"""
|
||||
隐私政策响应模型
|
||||
"""
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
# 重写 effective_time 字段,确保返回字符串格式
|
||||
effective_time: DateTimeStr | None = Field(default=None, description='生效时间')
|
||||
|
||||
|
||||
class YifanPrivacyPolicyQueryParam:
|
||||
"""隐私政策查询参数"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
title: str | None = Query(None, description="标题"),
|
||||
version: str | None = Query(None, description="版本号"),
|
||||
policy_type: str | None = Query(None, description="协议类型(privacy:隐私政策 terms:用户协议 service:服务条款)"),
|
||||
remark: str | None = Query(None, description="备注"),
|
||||
created_id: int | None = Query(None, description="创建人ID"),
|
||||
updated_id: int | None = Query(None, description="更新人ID"),
|
||||
is_deleted: int | None = Query(None, description="是否删除(0否 1是)"),
|
||||
status: int | None = Query(None, description="状态(0禁用 1启用)"),
|
||||
content: str | None = Query(None, description="内容(支持富文本)"),
|
||||
effective_time: datetime.datetime | None = Query(None, description="生效时间"),
|
||||
is_current: int | None = Query(None, description="是否当前版本(0否 1是)"),
|
||||
created_time: list[DateTimeStr] | None = Query(None, description="创建时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]),
|
||||
updated_time: list[DateTimeStr] | None = Query(None, description="更新时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]),
|
||||
|
||||
) -> None:
|
||||
|
||||
# 精确查询字段
|
||||
self.created_id = created_id
|
||||
# 精确查询字段
|
||||
self.updated_id = updated_id
|
||||
# 精确查询字段
|
||||
self.is_deleted = is_deleted
|
||||
# 精确查询字段
|
||||
self.status = status
|
||||
# 模糊查询字段
|
||||
self.title = ("like", title)
|
||||
# 精确查询字段
|
||||
self.content = content
|
||||
# 模糊查询字段
|
||||
self.version = ("like", version)
|
||||
# 模糊查询字段
|
||||
self.policy_type = ("like", policy_type)
|
||||
# 精确查询字段
|
||||
self.effective_time = effective_time
|
||||
# 精确查询字段
|
||||
self.is_current = is_current
|
||||
# 模糊查询字段
|
||||
self.remark = ("like", remark)
|
||||
# 时间范围查询
|
||||
if created_time and len(created_time) == 2:
|
||||
self.created_time = ("between", (created_time[0], created_time[1]))
|
||||
if updated_time and len(updated_time) == 2:
|
||||
self.updated_time = ("between", (updated_time[0], updated_time[1]))
|
||||
@@ -0,0 +1,261 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import io
|
||||
from fastapi import UploadFile
|
||||
import pandas as pd
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.base_schema import BatchSetAvailable
|
||||
from app.core.exceptions import CustomException
|
||||
from app.utils.excel_util import ExcelUtil
|
||||
from app.core.logger import log
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from .schema import YifanPrivacyPolicyCreateSchema, YifanPrivacyPolicyUpdateSchema, YifanPrivacyPolicyOutSchema, YifanPrivacyPolicyQueryParam
|
||||
from .crud import YifanPrivacyPolicyCRUD
|
||||
from .model import YifanPrivacyPolicyModel
|
||||
|
||||
|
||||
class YifanPrivacyPolicyService:
|
||||
"""
|
||||
隐私政策服务层
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
async def get_current_policy_service(cls, db: AsyncSession, policy_type: str) -> dict | None:
|
||||
"""
|
||||
获取当前生效的隐私政策/用户协议(小程序专用)
|
||||
"""
|
||||
stmt = (
|
||||
select(YifanPrivacyPolicyModel)
|
||||
.where(
|
||||
YifanPrivacyPolicyModel.policy_type == policy_type,
|
||||
YifanPrivacyPolicyModel.is_current == 1,
|
||||
YifanPrivacyPolicyModel.status == '1'
|
||||
)
|
||||
.order_by(YifanPrivacyPolicyModel.effective_time.desc())
|
||||
.limit(1)
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
policy = result.scalar_one_or_none()
|
||||
|
||||
if not policy:
|
||||
return None
|
||||
|
||||
return {
|
||||
'id': policy.id,
|
||||
'title': policy.title,
|
||||
'content': policy.content,
|
||||
'version': policy.version,
|
||||
'policy_type': policy.policy_type,
|
||||
'effective_time': str(policy.effective_time) if policy.effective_time else None,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
async def detail_yifan_privacy_policy_service(cls, auth: AuthSchema, id: int) -> dict:
|
||||
"""详情"""
|
||||
obj = await YifanPrivacyPolicyCRUD(auth).get_by_id_yifan_privacy_policy_crud(id=id)
|
||||
if not obj:
|
||||
raise CustomException(msg="该数据不存在")
|
||||
return YifanPrivacyPolicyOutSchema.model_validate(obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def list_yifan_privacy_policy_service(cls, auth: AuthSchema, search: YifanPrivacyPolicyQueryParam | None = None, order_by: list[dict] | None = None) -> list[dict]:
|
||||
"""列表查询"""
|
||||
search_dict = search.__dict__ if search else None
|
||||
obj_list = await YifanPrivacyPolicyCRUD(auth).list_yifan_privacy_policy_crud(search=search_dict, order_by=order_by)
|
||||
return [YifanPrivacyPolicyOutSchema.model_validate(obj).model_dump() for obj in obj_list]
|
||||
|
||||
@classmethod
|
||||
async def page_yifan_privacy_policy_service(cls, auth: AuthSchema, page_no: int, page_size: int, search: YifanPrivacyPolicyQueryParam | None = None, order_by: list[dict] | None = None) -> dict:
|
||||
"""分页查询(数据库分页)"""
|
||||
search_dict = search.__dict__ if search else {}
|
||||
order_by_list = order_by or [{'id': 'asc'}]
|
||||
offset = (page_no - 1) * page_size
|
||||
result = await YifanPrivacyPolicyCRUD(auth).page_yifan_privacy_policy_crud(
|
||||
offset=offset,
|
||||
limit=page_size,
|
||||
order_by=order_by_list,
|
||||
search=search_dict
|
||||
)
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
async def create_yifan_privacy_policy_service(cls, auth: AuthSchema, data: YifanPrivacyPolicyCreateSchema) -> dict:
|
||||
"""创建"""
|
||||
# 检查唯一性约束
|
||||
obj = await YifanPrivacyPolicyCRUD(auth).create_yifan_privacy_policy_crud(data=data)
|
||||
return YifanPrivacyPolicyOutSchema.model_validate(obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def update_yifan_privacy_policy_service(cls, auth: AuthSchema, id: int, data: YifanPrivacyPolicyUpdateSchema) -> dict:
|
||||
"""更新"""
|
||||
# 检查数据是否存在
|
||||
obj = await YifanPrivacyPolicyCRUD(auth).get_by_id_yifan_privacy_policy_crud(id=id)
|
||||
if not obj:
|
||||
raise CustomException(msg='更新失败,该数据不存在')
|
||||
|
||||
# 检查唯一性约束
|
||||
|
||||
obj = await YifanPrivacyPolicyCRUD(auth).update_yifan_privacy_policy_crud(id=id, data=data)
|
||||
return YifanPrivacyPolicyOutSchema.model_validate(obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def delete_yifan_privacy_policy_service(cls, auth: AuthSchema, ids: list[int]) -> None:
|
||||
"""删除"""
|
||||
if len(ids) < 1:
|
||||
raise CustomException(msg='删除失败,删除对象不能为空')
|
||||
for id in ids:
|
||||
obj = await YifanPrivacyPolicyCRUD(auth).get_by_id_yifan_privacy_policy_crud(id=id)
|
||||
if not obj:
|
||||
raise CustomException(msg=f'删除失败,ID为{id}的数据不存在')
|
||||
await YifanPrivacyPolicyCRUD(auth).delete_yifan_privacy_policy_crud(ids=ids)
|
||||
|
||||
@classmethod
|
||||
async def set_available_yifan_privacy_policy_service(cls, auth: AuthSchema, data: BatchSetAvailable) -> None:
|
||||
"""批量设置状态"""
|
||||
await YifanPrivacyPolicyCRUD(auth).set_available_yifan_privacy_policy_crud(ids=data.ids, status=data.status)
|
||||
|
||||
@classmethod
|
||||
async def batch_export_yifan_privacy_policy_service(cls, obj_list: list[dict]) -> bytes:
|
||||
"""批量导出"""
|
||||
mapping_dict = {
|
||||
'id': '主键ID',
|
||||
'created_time': '创建时间',
|
||||
'updated_time': '更新时间',
|
||||
'created_id': '创建人ID',
|
||||
'updated_id': '更新人ID',
|
||||
'is_deleted': '是否删除(0否 1是)',
|
||||
'status': '状态(0禁用 1启用)',
|
||||
'title': '标题',
|
||||
'content': '内容(支持富文本)',
|
||||
'version': '版本号',
|
||||
'policy_type': '协议类型(privacy:隐私政策 terms:用户协议 service:服务条款)',
|
||||
'effective_time': '生效时间',
|
||||
'is_current': '是否当前版本(0否 1是)',
|
||||
'remark': '备注',
|
||||
'updated_id': '更新者ID',
|
||||
}
|
||||
|
||||
data = obj_list.copy()
|
||||
for item in data:
|
||||
# 状态转换
|
||||
if 'status' in item:
|
||||
item['status'] = '启用' if item.get('status') == '0' else '停用'
|
||||
# 创建者转换
|
||||
creator_info = item.get('creator')
|
||||
if isinstance(creator_info, dict):
|
||||
item['creator'] = creator_info.get('name', '未知')
|
||||
elif creator_info is None:
|
||||
item['creator'] = '未知'
|
||||
|
||||
return ExcelUtil.export_list2excel(list_data=data, mapping_dict=mapping_dict)
|
||||
|
||||
@classmethod
|
||||
async def batch_import_yifan_privacy_policy_service(cls, auth: AuthSchema, file: UploadFile, update_support: bool = False) -> str:
|
||||
"""批量导入"""
|
||||
header_dict = {
|
||||
'主键ID': 'id',
|
||||
'创建时间': 'created_time',
|
||||
'更新时间': 'updated_time',
|
||||
'创建人ID': 'created_id',
|
||||
'更新人ID': 'updated_id',
|
||||
'是否删除(0否 1是)': 'is_deleted',
|
||||
'状态(0禁用 1启用)': 'status',
|
||||
'标题': 'title',
|
||||
'内容(支持富文本)': 'content',
|
||||
'版本号': 'version',
|
||||
'协议类型(privacy:隐私政策 terms:用户协议 service:服务条款)': 'policy_type',
|
||||
'生效时间': 'effective_time',
|
||||
'是否当前版本(0否 1是)': 'is_current',
|
||||
'备注': 'remark',
|
||||
}
|
||||
|
||||
try:
|
||||
contents = await file.read()
|
||||
df = pd.read_excel(io.BytesIO(contents))
|
||||
await file.close()
|
||||
|
||||
if df.empty:
|
||||
raise CustomException(msg="导入文件为空")
|
||||
|
||||
missing_headers = [header for header in header_dict.keys() if header not in df.columns]
|
||||
if missing_headers:
|
||||
raise CustomException(msg=f"导入文件缺少必要的列: {', '.join(missing_headers)}")
|
||||
|
||||
df.rename(columns=header_dict, inplace=True)
|
||||
|
||||
# 验证必填字段
|
||||
|
||||
error_msgs = []
|
||||
success_count = 0
|
||||
count = 0
|
||||
|
||||
for index, row in df.iterrows():
|
||||
count += 1
|
||||
try:
|
||||
data = {
|
||||
"id": row['id'],
|
||||
"created_time": row['created_time'],
|
||||
"updated_time": row['updated_time'],
|
||||
"created_id": row['created_id'],
|
||||
"updated_id": row['updated_id'],
|
||||
"is_deleted": row['is_deleted'],
|
||||
"status": row['status'],
|
||||
"title": row['title'],
|
||||
"content": row['content'],
|
||||
"version": row['version'],
|
||||
"policy_type": row['policy_type'],
|
||||
"effective_time": row['effective_time'],
|
||||
"is_current": row['is_current'],
|
||||
"remark": row['remark'],
|
||||
}
|
||||
# 使用CreateSchema做校验后入库
|
||||
create_schema = YifanPrivacyPolicyCreateSchema.model_validate(data)
|
||||
|
||||
# 检查唯一性约束
|
||||
|
||||
await YifanPrivacyPolicyCRUD(auth).create_yifan_privacy_policy_crud(data=create_schema)
|
||||
success_count += 1
|
||||
except Exception as e:
|
||||
error_msgs.append(f"第{count}行: {str(e)}")
|
||||
continue
|
||||
|
||||
result = f"成功导入 {success_count} 条数据"
|
||||
if error_msgs:
|
||||
result += "\n错误信息:\n" + "\n".join(error_msgs)
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
log.error(f"批量导入失败: {str(e)}")
|
||||
raise CustomException(msg=f"导入失败: {str(e)}")
|
||||
|
||||
@classmethod
|
||||
async def import_template_download_yifan_privacy_policy_service(cls) -> bytes:
|
||||
"""下载导入模板"""
|
||||
header_list = [
|
||||
'主键ID',
|
||||
'创建时间',
|
||||
'更新时间',
|
||||
'创建人ID',
|
||||
'更新人ID',
|
||||
'是否删除(0否 1是)',
|
||||
'状态(0禁用 1启用)',
|
||||
'标题',
|
||||
'内容(支持富文本)',
|
||||
'版本号',
|
||||
'协议类型(privacy:隐私政策 terms:用户协议 service:服务条款)',
|
||||
'生效时间',
|
||||
'是否当前版本(0否 1是)',
|
||||
'备注',
|
||||
]
|
||||
selector_header_list = []
|
||||
option_list = []
|
||||
|
||||
# 添加下拉选项
|
||||
|
||||
return ExcelUtil.get_excel_template(
|
||||
header_list=header_list,
|
||||
selector_header_list=selector_header_list,
|
||||
option_list=option_list
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
@@ -0,0 +1,126 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from fastapi import APIRouter, Depends, UploadFile, Body, Path, Query
|
||||
from fastapi.responses import StreamingResponse, JSONResponse
|
||||
|
||||
from app.common.response import SuccessResponse, StreamResponse
|
||||
from app.core.dependencies import AuthPermission
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from app.core.base_params import PaginationQueryParam
|
||||
from app.utils.common_util import bytes2file_response
|
||||
from app.core.logger import log
|
||||
from app.core.base_schema import BatchSetAvailable
|
||||
|
||||
from .service import YifanSectionDetailsService
|
||||
from .schema import YifanSectionDetailsCreateSchema, YifanSectionDetailsUpdateSchema, YifanSectionDetailsQueryParam
|
||||
|
||||
YifanSectionDetailsRouter = APIRouter(prefix='/yifan_section_details', tags=["板块详情模块"])
|
||||
|
||||
@YifanSectionDetailsRouter.get("/detail/{id}", summary="获取板块详情详情", description="获取板块详情详情")
|
||||
async def get_yifan_section_details_detail_controller(
|
||||
id: int = Path(..., description="ID"),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_section_details:query"]))
|
||||
) -> JSONResponse:
|
||||
"""获取板块详情详情接口"""
|
||||
result_dict = await YifanSectionDetailsService.detail_yifan_section_details_service(auth=auth, id=id)
|
||||
log.info(f"获取板块详情详情成功 {id}")
|
||||
return SuccessResponse(data=result_dict, msg="获取板块详情详情成功")
|
||||
|
||||
@YifanSectionDetailsRouter.get("/list", summary="查询板块详情列表", description="查询板块详情列表")
|
||||
async def get_yifan_section_details_list_controller(
|
||||
page: PaginationQueryParam = Depends(),
|
||||
search: YifanSectionDetailsQueryParam = Depends(),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_section_details:query"]))
|
||||
) -> JSONResponse:
|
||||
"""查询板块详情列表接口(数据库分页)"""
|
||||
result_dict = await YifanSectionDetailsService.page_yifan_section_details_service(
|
||||
auth=auth,
|
||||
page_no=page.page_no if page.page_no is not None else 1,
|
||||
page_size=page.page_size if page.page_size is not None else 10,
|
||||
search=search,
|
||||
order_by=page.order_by
|
||||
)
|
||||
log.info("查询板块详情列表成功")
|
||||
return SuccessResponse(data=result_dict, msg="查询板块详情列表成功")
|
||||
|
||||
@YifanSectionDetailsRouter.post("/create", summary="创建板块详情", description="创建板块详情")
|
||||
async def create_yifan_section_details_controller(
|
||||
data: YifanSectionDetailsCreateSchema,
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_section_details:create"]))
|
||||
) -> JSONResponse:
|
||||
"""创建板块详情接口"""
|
||||
result_dict = await YifanSectionDetailsService.create_yifan_section_details_service(auth=auth, data=data)
|
||||
log.info("创建板块详情成功")
|
||||
return SuccessResponse(data=result_dict, msg="创建板块详情成功")
|
||||
|
||||
@YifanSectionDetailsRouter.put("/update/{id}", summary="修改板块详情", description="修改板块详情")
|
||||
async def update_yifan_section_details_controller(
|
||||
data: YifanSectionDetailsUpdateSchema,
|
||||
id: int = Path(..., description="ID"),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_section_details:update"]))
|
||||
) -> JSONResponse:
|
||||
"""修改板块详情接口"""
|
||||
result_dict = await YifanSectionDetailsService.update_yifan_section_details_service(auth=auth, id=id, data=data)
|
||||
log.info("修改板块详情成功")
|
||||
return SuccessResponse(data=result_dict, msg="修改板块详情成功")
|
||||
|
||||
@YifanSectionDetailsRouter.delete("/delete", summary="删除板块详情", description="删除板块详情")
|
||||
async def delete_yifan_section_details_controller(
|
||||
ids: list[int] = Body(..., description="ID列表"),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_section_details:delete"]))
|
||||
) -> JSONResponse:
|
||||
"""删除板块详情接口"""
|
||||
await YifanSectionDetailsService.delete_yifan_section_details_service(auth=auth, ids=ids)
|
||||
log.info(f"删除板块详情成功: {ids}")
|
||||
return SuccessResponse(msg="删除板块详情成功")
|
||||
|
||||
@YifanSectionDetailsRouter.patch("/available/setting", summary="批量修改板块详情状态", description="批量修改板块详情状态")
|
||||
async def batch_set_available_yifan_section_details_controller(
|
||||
data: BatchSetAvailable,
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_section_details:patch"]))
|
||||
) -> JSONResponse:
|
||||
"""批量修改板块详情状态接口"""
|
||||
await YifanSectionDetailsService.set_available_yifan_section_details_service(auth=auth, data=data)
|
||||
log.info(f"批量修改板块详情状态成功: {data.ids}")
|
||||
return SuccessResponse(msg="批量修改板块详情状态成功")
|
||||
|
||||
@YifanSectionDetailsRouter.post('/export', summary="导出板块详情", description="导出板块详情")
|
||||
async def export_yifan_section_details_list_controller(
|
||||
search: YifanSectionDetailsQueryParam = Depends(),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_section_details:export"]))
|
||||
) -> StreamingResponse:
|
||||
"""导出板块详情接口"""
|
||||
result_dict_list = await YifanSectionDetailsService.list_yifan_section_details_service(search=search, auth=auth)
|
||||
export_result = await YifanSectionDetailsService.batch_export_yifan_section_details_service(obj_list=result_dict_list)
|
||||
log.info('导出板块详情成功')
|
||||
|
||||
return StreamResponse(
|
||||
data=bytes2file_response(export_result),
|
||||
media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
headers={
|
||||
'Content-Disposition': 'attachment; filename=yifan_section_details.xlsx'
|
||||
}
|
||||
)
|
||||
|
||||
@YifanSectionDetailsRouter.post('/import', summary="导入板块详情", description="导入板块详情")
|
||||
async def import_yifan_section_details_list_controller(
|
||||
file: UploadFile,
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_section_details:import"]))
|
||||
) -> JSONResponse:
|
||||
"""导入板块详情接口"""
|
||||
batch_import_result = await YifanSectionDetailsService.batch_import_yifan_section_details_service(file=file, auth=auth, update_support=True)
|
||||
log.info("导入板块详情成功")
|
||||
|
||||
return SuccessResponse(data=batch_import_result, msg="导入板块详情成功")
|
||||
|
||||
@YifanSectionDetailsRouter.post('/download/template', summary="获取板块详情导入模板", description="获取板块详情导入模板", dependencies=[Depends(AuthPermission(["module_yifan:yifan_section_details:download"]))])
|
||||
async def export_yifan_section_details_template_controller() -> StreamingResponse:
|
||||
"""获取板块详情导入模板接口"""
|
||||
import_template_result = await YifanSectionDetailsService.import_template_download_yifan_section_details_service()
|
||||
log.info('获取板块详情导入模板成功')
|
||||
|
||||
return StreamResponse(
|
||||
data=bytes2file_response(import_template_result),
|
||||
media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
headers={'Content-Disposition': 'attachment; filename=yifan_section_details_template.xlsx'}
|
||||
)
|
||||
@@ -0,0 +1,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from app.core.base_crud import CRUDBase
|
||||
from .model import YifanSectionDetailsModel
|
||||
|
||||
|
||||
class YifanSectionDetailsCRUD(CRUDBase[YifanSectionDetailsModel, None, None]):
|
||||
"""板块详情 CRUD"""
|
||||
|
||||
def __init__(self, auth):
|
||||
super().__init__(model=YifanSectionDetailsModel, auth=auth)
|
||||
@@ -0,0 +1,22 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from sqlalchemy import String, Integer, SmallInteger, JSON
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.base_model import ModelMixin, UserMixin
|
||||
|
||||
|
||||
class YifanSectionDetailsModel(ModelMixin, UserMixin):
|
||||
"""
|
||||
板块详情表
|
||||
"""
|
||||
__tablename__: str = 'yifan_section_details'
|
||||
__table_args__: dict[str, str] = {'comment': '板块详情表'}
|
||||
__loader_options__: list[str] = ["created_by", "updated_by"]
|
||||
|
||||
is_deleted: Mapped[int | None] = mapped_column(SmallInteger, nullable=True, comment='是否删除(0:否 1:是)')
|
||||
section_id: Mapped[int] = mapped_column(Integer, nullable=False, comment='关联板块ID')
|
||||
detail_type: Mapped[str] = mapped_column(String(50), nullable=False, comment='详情类型')
|
||||
title: Mapped[str | None] = mapped_column(String(100), nullable=True, comment='详情标题')
|
||||
content: Mapped[dict | None] = mapped_column(JSON, nullable=True, comment='详细内容')
|
||||
sort_order: Mapped[int | None] = mapped_column(Integer, nullable=True, default=0, comment='排序')
|
||||
@@ -0,0 +1,77 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from fastapi import Query
|
||||
|
||||
from app.core.validator import DateTimeStr
|
||||
from app.core.base_schema import BaseSchema, UserBySchema
|
||||
|
||||
class YifanSectionDetailsCreateSchema(BaseModel):
|
||||
"""
|
||||
板块详情新增模型
|
||||
"""
|
||||
is_deleted: int = Field(default=0, description='是否删除(0否 1是)')
|
||||
status: int = Field(default=1, description='状态(0禁用 1启用)')
|
||||
section_id: int = Field(default=..., description='关联板块ID')
|
||||
detail_type: str = Field(default=..., description='详情类型(life_guide:生活开运指南 poetry_base:诗词底蕴 hexagram_meaning:卦象释义 depth_analysis:深度字义解读 zodiac_analysis:生肖喜忌分析 master_comment:大师总评)')
|
||||
title: str = Field(default=..., description='详情标题')
|
||||
content: dict = Field(default=..., description='详细内容')
|
||||
sort_order: int = Field(default=..., description='排序')
|
||||
|
||||
|
||||
class YifanSectionDetailsUpdateSchema(YifanSectionDetailsCreateSchema):
|
||||
"""
|
||||
板块详情更新模型
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
class YifanSectionDetailsOutSchema(YifanSectionDetailsCreateSchema, BaseSchema, UserBySchema):
|
||||
"""
|
||||
板块详情响应模型
|
||||
"""
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class YifanSectionDetailsQueryParam:
|
||||
"""板块详情查询参数"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
detail_type: str | None = Query(None, description="详情类型(life_guide:生活开运指南 poetry_base:诗词底蕴 hexagram_meaning:卦象释义 depth_analysis:深度字义解读 zodiac_analysis:生肖喜忌分析 master_comment:大师总评)"),
|
||||
title: str | None = Query(None, description="详情标题"),
|
||||
created_id: int | None = Query(None, description="创建人ID"),
|
||||
updated_id: int | None = Query(None, description="更新人ID"),
|
||||
is_deleted: int | None = Query(None, description="是否删除(0否 1是)"),
|
||||
status: int | None = Query(None, description="状态(0禁用 1启用)"),
|
||||
section_id: int | None = Query(None, description="关联板块ID"),
|
||||
content: str | None = Query(None, description="详细内容(JSON字符串)"),
|
||||
sort_order: int | None = Query(None, description="排序"),
|
||||
created_time: list[DateTimeStr] | None = Query(None, description="创建时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]),
|
||||
updated_time: list[DateTimeStr] | None = Query(None, description="更新时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]),
|
||||
|
||||
) -> None:
|
||||
|
||||
# 精确查询字段
|
||||
self.created_id = created_id
|
||||
# 精确查询字段
|
||||
self.updated_id = updated_id
|
||||
# 精确查询字段
|
||||
self.is_deleted = is_deleted
|
||||
# 精确查询字段
|
||||
self.status = status
|
||||
# 精确查询字段
|
||||
self.section_id = section_id
|
||||
# 模糊查询字段
|
||||
self.detail_type = ("like", detail_type)
|
||||
# 模糊查询字段
|
||||
self.title = ("like", title)
|
||||
# 精确查询字段
|
||||
self.content = content
|
||||
# 精确查询字段
|
||||
self.sort_order = sort_order
|
||||
# 时间范围查询
|
||||
if created_time and len(created_time) == 2:
|
||||
self.created_time = ("between", (created_time[0], created_time[1]))
|
||||
if updated_time and len(updated_time) == 2:
|
||||
self.updated_time = ("between", (updated_time[0], updated_time[1]))
|
||||
@@ -0,0 +1,220 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import io
|
||||
from fastapi import UploadFile
|
||||
import pandas as pd
|
||||
|
||||
from app.core.base_schema import BatchSetAvailable
|
||||
from app.core.exceptions import CustomException
|
||||
from app.utils.excel_util import ExcelUtil
|
||||
from app.core.logger import log
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from .schema import YifanSectionDetailsCreateSchema, YifanSectionDetailsUpdateSchema, YifanSectionDetailsOutSchema, YifanSectionDetailsQueryParam
|
||||
from .crud import YifanSectionDetailsCRUD
|
||||
|
||||
|
||||
class YifanSectionDetailsService:
|
||||
"""
|
||||
板块详情服务层
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
async def detail_yifan_section_details_service(cls, auth: AuthSchema, id: int) -> dict:
|
||||
"""详情"""
|
||||
obj = await YifanSectionDetailsCRUD(auth).get_by_id_yifan_section_details_crud(id=id)
|
||||
if not obj:
|
||||
raise CustomException(msg="该数据不存在")
|
||||
return YifanSectionDetailsOutSchema.model_validate(obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def list_yifan_section_details_service(cls, auth: AuthSchema, search: YifanSectionDetailsQueryParam | None = None, order_by: list[dict] | None = None) -> list[dict]:
|
||||
"""列表查询"""
|
||||
search_dict = search.__dict__ if search else None
|
||||
obj_list = await YifanSectionDetailsCRUD(auth).list_yifan_section_details_crud(search=search_dict, order_by=order_by)
|
||||
return [YifanSectionDetailsOutSchema.model_validate(obj).model_dump() for obj in obj_list]
|
||||
|
||||
@classmethod
|
||||
async def page_yifan_section_details_service(cls, auth: AuthSchema, page_no: int, page_size: int, search: YifanSectionDetailsQueryParam | None = None, order_by: list[dict] | None = None) -> dict:
|
||||
"""分页查询(数据库分页)"""
|
||||
search_dict = search.__dict__ if search else {}
|
||||
order_by_list = order_by or [{'id': 'asc'}]
|
||||
offset = (page_no - 1) * page_size
|
||||
result = await YifanSectionDetailsCRUD(auth).page_yifan_section_details_crud(
|
||||
offset=offset,
|
||||
limit=page_size,
|
||||
order_by=order_by_list,
|
||||
search=search_dict
|
||||
)
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
async def create_yifan_section_details_service(cls, auth: AuthSchema, data: YifanSectionDetailsCreateSchema) -> dict:
|
||||
"""创建"""
|
||||
# 检查唯一性约束
|
||||
obj = await YifanSectionDetailsCRUD(auth).create_yifan_section_details_crud(data=data)
|
||||
return YifanSectionDetailsOutSchema.model_validate(obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def update_yifan_section_details_service(cls, auth: AuthSchema, id: int, data: YifanSectionDetailsUpdateSchema) -> dict:
|
||||
"""更新"""
|
||||
# 检查数据是否存在
|
||||
obj = await YifanSectionDetailsCRUD(auth).get_by_id_yifan_section_details_crud(id=id)
|
||||
if not obj:
|
||||
raise CustomException(msg='更新失败,该数据不存在')
|
||||
|
||||
# 检查唯一性约束
|
||||
|
||||
obj = await YifanSectionDetailsCRUD(auth).update_yifan_section_details_crud(id=id, data=data)
|
||||
return YifanSectionDetailsOutSchema.model_validate(obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def delete_yifan_section_details_service(cls, auth: AuthSchema, ids: list[int]) -> None:
|
||||
"""删除"""
|
||||
if len(ids) < 1:
|
||||
raise CustomException(msg='删除失败,删除对象不能为空')
|
||||
for id in ids:
|
||||
obj = await YifanSectionDetailsCRUD(auth).get_by_id_yifan_section_details_crud(id=id)
|
||||
if not obj:
|
||||
raise CustomException(msg=f'删除失败,ID为{id}的数据不存在')
|
||||
await YifanSectionDetailsCRUD(auth).delete_yifan_section_details_crud(ids=ids)
|
||||
|
||||
@classmethod
|
||||
async def set_available_yifan_section_details_service(cls, auth: AuthSchema, data: BatchSetAvailable) -> None:
|
||||
"""批量设置状态"""
|
||||
await YifanSectionDetailsCRUD(auth).set_available_yifan_section_details_crud(ids=data.ids, status=data.status)
|
||||
|
||||
@classmethod
|
||||
async def batch_export_yifan_section_details_service(cls, obj_list: list[dict]) -> bytes:
|
||||
"""批量导出"""
|
||||
mapping_dict = {
|
||||
'id': '主键ID',
|
||||
'created_time': '创建时间',
|
||||
'updated_time': '更新时间',
|
||||
'created_id': '创建人ID',
|
||||
'updated_id': '更新人ID',
|
||||
'is_deleted': '是否删除(0否 1是)',
|
||||
'status': '状态(0禁用 1启用)',
|
||||
'section_id': '关联板块ID',
|
||||
'detail_type': '详情类型(life_guide:生活开运指南 poetry_base:诗词底蕴 hexagram_meaning:卦象释义 depth_analysis:深度字义解读 zodiac_analysis:生肖喜忌分析 master_comment:大师总评)',
|
||||
'title': '详情标题',
|
||||
'content': '详细内容',
|
||||
'sort_order': '排序',
|
||||
'updated_id': '更新者ID',
|
||||
}
|
||||
|
||||
data = obj_list.copy()
|
||||
for item in data:
|
||||
# 状态转换
|
||||
if 'status' in item:
|
||||
item['status'] = '启用' if item.get('status') == '0' else '停用'
|
||||
# 创建者转换
|
||||
creator_info = item.get('creator')
|
||||
if isinstance(creator_info, dict):
|
||||
item['creator'] = creator_info.get('name', '未知')
|
||||
elif creator_info is None:
|
||||
item['creator'] = '未知'
|
||||
|
||||
return ExcelUtil.export_list2excel(list_data=data, mapping_dict=mapping_dict)
|
||||
|
||||
@classmethod
|
||||
async def batch_import_yifan_section_details_service(cls, auth: AuthSchema, file: UploadFile, update_support: bool = False) -> str:
|
||||
"""批量导入"""
|
||||
header_dict = {
|
||||
'主键ID': 'id',
|
||||
'创建时间': 'created_time',
|
||||
'更新时间': 'updated_time',
|
||||
'创建人ID': 'created_id',
|
||||
'更新人ID': 'updated_id',
|
||||
'是否删除(0否 1是)': 'is_deleted',
|
||||
'状态(0禁用 1启用)': 'status',
|
||||
'关联板块ID': 'section_id',
|
||||
'详情类型(life_guide:生活开运指南 poetry_base:诗词底蕴 hexagram_meaning:卦象释义 depth_analysis:深度字义解读 zodiac_analysis:生肖喜忌分析 master_comment:大师总评)': 'detail_type',
|
||||
'详情标题': 'title',
|
||||
'详细内容': 'content',
|
||||
'排序': 'sort_order',
|
||||
}
|
||||
|
||||
try:
|
||||
contents = await file.read()
|
||||
df = pd.read_excel(io.BytesIO(contents))
|
||||
await file.close()
|
||||
|
||||
if df.empty:
|
||||
raise CustomException(msg="导入文件为空")
|
||||
|
||||
missing_headers = [header for header in header_dict.keys() if header not in df.columns]
|
||||
if missing_headers:
|
||||
raise CustomException(msg=f"导入文件缺少必要的列: {', '.join(missing_headers)}")
|
||||
|
||||
df.rename(columns=header_dict, inplace=True)
|
||||
|
||||
# 验证必填字段
|
||||
|
||||
error_msgs = []
|
||||
success_count = 0
|
||||
count = 0
|
||||
|
||||
for index, row in df.iterrows():
|
||||
count += 1
|
||||
try:
|
||||
data = {
|
||||
"id": row['id'],
|
||||
"created_time": row['created_time'],
|
||||
"updated_time": row['updated_time'],
|
||||
"created_id": row['created_id'],
|
||||
"updated_id": row['updated_id'],
|
||||
"is_deleted": row['is_deleted'],
|
||||
"status": row['status'],
|
||||
"section_id": row['section_id'],
|
||||
"detail_type": row['detail_type'],
|
||||
"title": row['title'],
|
||||
"content": row['content'],
|
||||
"sort_order": row['sort_order'],
|
||||
}
|
||||
# 使用CreateSchema做校验后入库
|
||||
create_schema = YifanSectionDetailsCreateSchema.model_validate(data)
|
||||
|
||||
# 检查唯一性约束
|
||||
|
||||
await YifanSectionDetailsCRUD(auth).create_yifan_section_details_crud(data=create_schema)
|
||||
success_count += 1
|
||||
except Exception as e:
|
||||
error_msgs.append(f"第{count}行: {str(e)}")
|
||||
continue
|
||||
|
||||
result = f"成功导入 {success_count} 条数据"
|
||||
if error_msgs:
|
||||
result += "\n错误信息:\n" + "\n".join(error_msgs)
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
log.error(f"批量导入失败: {str(e)}")
|
||||
raise CustomException(msg=f"导入失败: {str(e)}")
|
||||
|
||||
@classmethod
|
||||
async def import_template_download_yifan_section_details_service(cls) -> bytes:
|
||||
"""下载导入模板"""
|
||||
header_list = [
|
||||
'主键ID',
|
||||
'创建时间',
|
||||
'更新时间',
|
||||
'创建人ID',
|
||||
'更新人ID',
|
||||
'是否删除(0否 1是)',
|
||||
'状态(0禁用 1启用)',
|
||||
'关联板块ID',
|
||||
'详情类型(life_guide:生活开运指南 poetry_base:诗词底蕴 hexagram_meaning:卦象释义 depth_analysis:深度字义解读 zodiac_analysis:生肖喜忌分析 master_comment:大师总评)',
|
||||
'详情标题',
|
||||
'详细内容',
|
||||
'排序',
|
||||
]
|
||||
selector_header_list = []
|
||||
option_list = []
|
||||
|
||||
# 添加下拉选项
|
||||
|
||||
return ExcelUtil.get_excel_template(
|
||||
header_list=header_list,
|
||||
selector_header_list=selector_header_list,
|
||||
option_list=option_list
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
@@ -0,0 +1,126 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from fastapi import APIRouter, Depends, UploadFile, Body, Path, Query
|
||||
from fastapi.responses import StreamingResponse, JSONResponse
|
||||
|
||||
from app.common.response import SuccessResponse, StreamResponse
|
||||
from app.core.dependencies import AuthPermission
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from app.core.base_params import PaginationQueryParam
|
||||
from app.utils.common_util import bytes2file_response
|
||||
from app.core.logger import log
|
||||
from app.core.base_schema import BatchSetAvailable
|
||||
|
||||
from .service import YifanSolutionSectionsService
|
||||
from .schema import YifanSolutionSectionsCreateSchema, YifanSolutionSectionsUpdateSchema, YifanSolutionSectionsQueryParam
|
||||
|
||||
YifanSolutionSectionsRouter = APIRouter(prefix='/yifan_solution_sections', tags=["方案板块模块"])
|
||||
|
||||
@YifanSolutionSectionsRouter.get("/detail/{id}", summary="获取方案板块详情", description="获取方案板块详情")
|
||||
async def get_yifan_solution_sections_detail_controller(
|
||||
id: int = Path(..., description="ID"),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_solution_sections:query"]))
|
||||
) -> JSONResponse:
|
||||
"""获取方案板块详情接口"""
|
||||
result_dict = await YifanSolutionSectionsService.detail_yifan_solution_sections_service(auth=auth, id=id)
|
||||
log.info(f"获取方案板块详情成功 {id}")
|
||||
return SuccessResponse(data=result_dict, msg="获取方案板块详情成功")
|
||||
|
||||
@YifanSolutionSectionsRouter.get("/list", summary="查询方案板块列表", description="查询方案板块列表")
|
||||
async def get_yifan_solution_sections_list_controller(
|
||||
page: PaginationQueryParam = Depends(),
|
||||
search: YifanSolutionSectionsQueryParam = Depends(),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_solution_sections:query"]))
|
||||
) -> JSONResponse:
|
||||
"""查询方案板块列表接口(数据库分页)"""
|
||||
result_dict = await YifanSolutionSectionsService.page_yifan_solution_sections_service(
|
||||
auth=auth,
|
||||
page_no=page.page_no if page.page_no is not None else 1,
|
||||
page_size=page.page_size if page.page_size is not None else 10,
|
||||
search=search,
|
||||
order_by=page.order_by
|
||||
)
|
||||
log.info("查询方案板块列表成功")
|
||||
return SuccessResponse(data=result_dict, msg="查询方案板块列表成功")
|
||||
|
||||
@YifanSolutionSectionsRouter.post("/create", summary="创建方案板块", description="创建方案板块")
|
||||
async def create_yifan_solution_sections_controller(
|
||||
data: YifanSolutionSectionsCreateSchema,
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_solution_sections:create"]))
|
||||
) -> JSONResponse:
|
||||
"""创建方案板块接口"""
|
||||
result_dict = await YifanSolutionSectionsService.create_yifan_solution_sections_service(auth=auth, data=data)
|
||||
log.info("创建方案板块成功")
|
||||
return SuccessResponse(data=result_dict, msg="创建方案板块成功")
|
||||
|
||||
@YifanSolutionSectionsRouter.put("/update/{id}", summary="修改方案板块", description="修改方案板块")
|
||||
async def update_yifan_solution_sections_controller(
|
||||
data: YifanSolutionSectionsUpdateSchema,
|
||||
id: int = Path(..., description="ID"),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_solution_sections:update"]))
|
||||
) -> JSONResponse:
|
||||
"""修改方案板块接口"""
|
||||
result_dict = await YifanSolutionSectionsService.update_yifan_solution_sections_service(auth=auth, id=id, data=data)
|
||||
log.info("修改方案板块成功")
|
||||
return SuccessResponse(data=result_dict, msg="修改方案板块成功")
|
||||
|
||||
@YifanSolutionSectionsRouter.delete("/delete", summary="删除方案板块", description="删除方案板块")
|
||||
async def delete_yifan_solution_sections_controller(
|
||||
ids: list[int] = Body(..., description="ID列表"),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_solution_sections:delete"]))
|
||||
) -> JSONResponse:
|
||||
"""删除方案板块接口"""
|
||||
await YifanSolutionSectionsService.delete_yifan_solution_sections_service(auth=auth, ids=ids)
|
||||
log.info(f"删除方案板块成功: {ids}")
|
||||
return SuccessResponse(msg="删除方案板块成功")
|
||||
|
||||
@YifanSolutionSectionsRouter.patch("/available/setting", summary="批量修改方案板块状态", description="批量修改方案板块状态")
|
||||
async def batch_set_available_yifan_solution_sections_controller(
|
||||
data: BatchSetAvailable,
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_solution_sections:patch"]))
|
||||
) -> JSONResponse:
|
||||
"""批量修改方案板块状态接口"""
|
||||
await YifanSolutionSectionsService.set_available_yifan_solution_sections_service(auth=auth, data=data)
|
||||
log.info(f"批量修改方案板块状态成功: {data.ids}")
|
||||
return SuccessResponse(msg="批量修改方案板块状态成功")
|
||||
|
||||
@YifanSolutionSectionsRouter.post('/export', summary="导出方案板块", description="导出方案板块")
|
||||
async def export_yifan_solution_sections_list_controller(
|
||||
search: YifanSolutionSectionsQueryParam = Depends(),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_solution_sections:export"]))
|
||||
) -> StreamingResponse:
|
||||
"""导出方案板块接口"""
|
||||
result_dict_list = await YifanSolutionSectionsService.list_yifan_solution_sections_service(search=search, auth=auth)
|
||||
export_result = await YifanSolutionSectionsService.batch_export_yifan_solution_sections_service(obj_list=result_dict_list)
|
||||
log.info('导出方案板块成功')
|
||||
|
||||
return StreamResponse(
|
||||
data=bytes2file_response(export_result),
|
||||
media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
headers={
|
||||
'Content-Disposition': 'attachment; filename=yifan_solution_sections.xlsx'
|
||||
}
|
||||
)
|
||||
|
||||
@YifanSolutionSectionsRouter.post('/import', summary="导入方案板块", description="导入方案板块")
|
||||
async def import_yifan_solution_sections_list_controller(
|
||||
file: UploadFile,
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_solution_sections:import"]))
|
||||
) -> JSONResponse:
|
||||
"""导入方案板块接口"""
|
||||
batch_import_result = await YifanSolutionSectionsService.batch_import_yifan_solution_sections_service(file=file, auth=auth, update_support=True)
|
||||
log.info("导入方案板块成功")
|
||||
|
||||
return SuccessResponse(data=batch_import_result, msg="导入方案板块成功")
|
||||
|
||||
@YifanSolutionSectionsRouter.post('/download/template', summary="获取方案板块导入模板", description="获取方案板块导入模板", dependencies=[Depends(AuthPermission(["module_yifan:yifan_solution_sections:download"]))])
|
||||
async def export_yifan_solution_sections_template_controller() -> StreamingResponse:
|
||||
"""获取方案板块导入模板接口"""
|
||||
import_template_result = await YifanSolutionSectionsService.import_template_download_yifan_solution_sections_service()
|
||||
log.info('获取方案板块导入模板成功')
|
||||
|
||||
return StreamResponse(
|
||||
data=bytes2file_response(import_template_result),
|
||||
media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
headers={'Content-Disposition': 'attachment; filename=yifan_solution_sections_template.xlsx'}
|
||||
)
|
||||
@@ -0,0 +1,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from app.core.base_crud import CRUDBase
|
||||
from .model import YifanSolutionSectionsModel
|
||||
|
||||
|
||||
class YifanSolutionSectionsCRUD(CRUDBase[YifanSolutionSectionsModel, None, None]):
|
||||
"""方案板块 CRUD"""
|
||||
|
||||
def __init__(self, auth):
|
||||
super().__init__(model=YifanSolutionSectionsModel, auth=auth)
|
||||
@@ -0,0 +1,22 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from sqlalchemy import String, Integer, SmallInteger, JSON
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.base_model import ModelMixin, UserMixin
|
||||
|
||||
|
||||
class YifanSolutionSectionsModel(ModelMixin, UserMixin):
|
||||
"""
|
||||
方案板块表
|
||||
"""
|
||||
__tablename__: str = 'yifan_solution_sections'
|
||||
__table_args__: dict[str, str] = {'comment': '方案板块表'}
|
||||
__loader_options__: list[str] = ["created_by", "updated_by"]
|
||||
|
||||
is_deleted: Mapped[int | None] = mapped_column(SmallInteger, nullable=True, comment='是否删除(0:否 1:是)')
|
||||
solution_id: Mapped[int] = mapped_column(Integer, nullable=False, comment='关联方案ID')
|
||||
section_type: Mapped[str] = mapped_column(String(50), nullable=False, comment='板块类型')
|
||||
title: Mapped[str | None] = mapped_column(String(100), nullable=True, comment='板块标题')
|
||||
summary_data: Mapped[dict | None] = mapped_column(JSON, nullable=True, comment='摘要数据')
|
||||
sort_order: Mapped[int | None] = mapped_column(Integer, nullable=True, default=0, comment='排序')
|
||||
@@ -0,0 +1,77 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from fastapi import Query
|
||||
|
||||
from app.core.validator import DateTimeStr
|
||||
from app.core.base_schema import BaseSchema, UserBySchema
|
||||
|
||||
class YifanSolutionSectionsCreateSchema(BaseModel):
|
||||
"""
|
||||
方案板块新增模型
|
||||
"""
|
||||
is_deleted: int = Field(default=0, description='是否删除(0否 1是)')
|
||||
status: int = Field(default=1, description='状态(0禁用 1启用)')
|
||||
solution_id: int = Field(default=..., description='关联方案ID')
|
||||
section_type: str = Field(default=..., description='板块类型(ditiantai:地天泰 kaiyun_jingnang:开运锦囊 shici:诗词出处 wuxing:五行分布 liuyao:六维格局 jiazu:家族起名 bihua:笔画数理 ziyi_shengxiao:字义生肖 zonghe_pingfen:综合评分)')
|
||||
title: str = Field(default=..., description='板块标题')
|
||||
summary_data: dict = Field(default=..., description='摘要数据(首页卡片展示)')
|
||||
sort_order: int = Field(default=..., description='排序')
|
||||
|
||||
|
||||
class YifanSolutionSectionsUpdateSchema(YifanSolutionSectionsCreateSchema):
|
||||
"""
|
||||
方案板块更新模型
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
class YifanSolutionSectionsOutSchema(YifanSolutionSectionsCreateSchema, BaseSchema, UserBySchema):
|
||||
"""
|
||||
方案板块响应模型
|
||||
"""
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class YifanSolutionSectionsQueryParam:
|
||||
"""方案板块查询参数"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
section_type: str | None = Query(None, description="板块类型(ditiantai:地天泰 kaiyun_jingnang:开运锦囊 shici:诗词出处 wuxing:五行分布 liuyao:六维格局 jiazu:家族起名 bihua:笔画数理 ziyi_shengxiao:字义生肖 zonghe_pingfen:综合评分)"),
|
||||
title: str | None = Query(None, description="板块标题"),
|
||||
created_id: int | None = Query(None, description="创建人ID"),
|
||||
updated_id: int | None = Query(None, description="更新人ID"),
|
||||
is_deleted: int | None = Query(None, description="是否删除(0否 1是)"),
|
||||
status: int | None = Query(None, description="状态(0禁用 1启用)"),
|
||||
solution_id: int | None = Query(None, description="关联方案ID"),
|
||||
summary_data: str | None = Query(None, description="摘要数据(JSON字符串)"),
|
||||
sort_order: int | None = Query(None, description="排序"),
|
||||
created_time: list[DateTimeStr] | None = Query(None, description="创建时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]),
|
||||
updated_time: list[DateTimeStr] | None = Query(None, description="更新时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]),
|
||||
|
||||
) -> None:
|
||||
|
||||
# 精确查询字段
|
||||
self.created_id = created_id
|
||||
# 精确查询字段
|
||||
self.updated_id = updated_id
|
||||
# 精确查询字段
|
||||
self.is_deleted = is_deleted
|
||||
# 精确查询字段
|
||||
self.status = status
|
||||
# 精确查询字段
|
||||
self.solution_id = solution_id
|
||||
# 模糊查询字段
|
||||
self.section_type = ("like", section_type)
|
||||
# 模糊查询字段
|
||||
self.title = ("like", title)
|
||||
# 精确查询字段
|
||||
self.summary_data = summary_data
|
||||
# 精确查询字段
|
||||
self.sort_order = sort_order
|
||||
# 时间范围查询
|
||||
if created_time and len(created_time) == 2:
|
||||
self.created_time = ("between", (created_time[0], created_time[1]))
|
||||
if updated_time and len(updated_time) == 2:
|
||||
self.updated_time = ("between", (updated_time[0], updated_time[1]))
|
||||
@@ -0,0 +1,220 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import io
|
||||
from fastapi import UploadFile
|
||||
import pandas as pd
|
||||
|
||||
from app.core.base_schema import BatchSetAvailable
|
||||
from app.core.exceptions import CustomException
|
||||
from app.utils.excel_util import ExcelUtil
|
||||
from app.core.logger import log
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from .schema import YifanSolutionSectionsCreateSchema, YifanSolutionSectionsUpdateSchema, YifanSolutionSectionsOutSchema, YifanSolutionSectionsQueryParam
|
||||
from .crud import YifanSolutionSectionsCRUD
|
||||
|
||||
|
||||
class YifanSolutionSectionsService:
|
||||
"""
|
||||
方案板块服务层
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
async def detail_yifan_solution_sections_service(cls, auth: AuthSchema, id: int) -> dict:
|
||||
"""详情"""
|
||||
obj = await YifanSolutionSectionsCRUD(auth).get_by_id_yifan_solution_sections_crud(id=id)
|
||||
if not obj:
|
||||
raise CustomException(msg="该数据不存在")
|
||||
return YifanSolutionSectionsOutSchema.model_validate(obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def list_yifan_solution_sections_service(cls, auth: AuthSchema, search: YifanSolutionSectionsQueryParam | None = None, order_by: list[dict] | None = None) -> list[dict]:
|
||||
"""列表查询"""
|
||||
search_dict = search.__dict__ if search else None
|
||||
obj_list = await YifanSolutionSectionsCRUD(auth).list_yifan_solution_sections_crud(search=search_dict, order_by=order_by)
|
||||
return [YifanSolutionSectionsOutSchema.model_validate(obj).model_dump() for obj in obj_list]
|
||||
|
||||
@classmethod
|
||||
async def page_yifan_solution_sections_service(cls, auth: AuthSchema, page_no: int, page_size: int, search: YifanSolutionSectionsQueryParam | None = None, order_by: list[dict] | None = None) -> dict:
|
||||
"""分页查询(数据库分页)"""
|
||||
search_dict = search.__dict__ if search else {}
|
||||
order_by_list = order_by or [{'id': 'asc'}]
|
||||
offset = (page_no - 1) * page_size
|
||||
result = await YifanSolutionSectionsCRUD(auth).page_yifan_solution_sections_crud(
|
||||
offset=offset,
|
||||
limit=page_size,
|
||||
order_by=order_by_list,
|
||||
search=search_dict
|
||||
)
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
async def create_yifan_solution_sections_service(cls, auth: AuthSchema, data: YifanSolutionSectionsCreateSchema) -> dict:
|
||||
"""创建"""
|
||||
# 检查唯一性约束
|
||||
obj = await YifanSolutionSectionsCRUD(auth).create_yifan_solution_sections_crud(data=data)
|
||||
return YifanSolutionSectionsOutSchema.model_validate(obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def update_yifan_solution_sections_service(cls, auth: AuthSchema, id: int, data: YifanSolutionSectionsUpdateSchema) -> dict:
|
||||
"""更新"""
|
||||
# 检查数据是否存在
|
||||
obj = await YifanSolutionSectionsCRUD(auth).get_by_id_yifan_solution_sections_crud(id=id)
|
||||
if not obj:
|
||||
raise CustomException(msg='更新失败,该数据不存在')
|
||||
|
||||
# 检查唯一性约束
|
||||
|
||||
obj = await YifanSolutionSectionsCRUD(auth).update_yifan_solution_sections_crud(id=id, data=data)
|
||||
return YifanSolutionSectionsOutSchema.model_validate(obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def delete_yifan_solution_sections_service(cls, auth: AuthSchema, ids: list[int]) -> None:
|
||||
"""删除"""
|
||||
if len(ids) < 1:
|
||||
raise CustomException(msg='删除失败,删除对象不能为空')
|
||||
for id in ids:
|
||||
obj = await YifanSolutionSectionsCRUD(auth).get_by_id_yifan_solution_sections_crud(id=id)
|
||||
if not obj:
|
||||
raise CustomException(msg=f'删除失败,ID为{id}的数据不存在')
|
||||
await YifanSolutionSectionsCRUD(auth).delete_yifan_solution_sections_crud(ids=ids)
|
||||
|
||||
@classmethod
|
||||
async def set_available_yifan_solution_sections_service(cls, auth: AuthSchema, data: BatchSetAvailable) -> None:
|
||||
"""批量设置状态"""
|
||||
await YifanSolutionSectionsCRUD(auth).set_available_yifan_solution_sections_crud(ids=data.ids, status=data.status)
|
||||
|
||||
@classmethod
|
||||
async def batch_export_yifan_solution_sections_service(cls, obj_list: list[dict]) -> bytes:
|
||||
"""批量导出"""
|
||||
mapping_dict = {
|
||||
'id': '主键ID',
|
||||
'created_time': '创建时间',
|
||||
'updated_time': '更新时间',
|
||||
'created_id': '创建人ID',
|
||||
'updated_id': '更新人ID',
|
||||
'is_deleted': '是否删除(0否 1是)',
|
||||
'status': '状态(0禁用 1启用)',
|
||||
'solution_id': '关联方案ID',
|
||||
'section_type': '板块类型(ditiantai:地天泰 kaiyun_jingnang:开运锦囊 shici:诗词出处 wuxing:五行分布 liuyao:六维格局 jiazu:家族起名 bihua:笔画数理 ziyi_shengxiao:字义生肖 zonghe_pingfen:综合评分)',
|
||||
'title': '板块标题',
|
||||
'summary_data': '摘要数据(首页卡片展示)',
|
||||
'sort_order': '排序',
|
||||
'updated_id': '更新者ID',
|
||||
}
|
||||
|
||||
data = obj_list.copy()
|
||||
for item in data:
|
||||
# 状态转换
|
||||
if 'status' in item:
|
||||
item['status'] = '启用' if item.get('status') == '0' else '停用'
|
||||
# 创建者转换
|
||||
creator_info = item.get('creator')
|
||||
if isinstance(creator_info, dict):
|
||||
item['creator'] = creator_info.get('name', '未知')
|
||||
elif creator_info is None:
|
||||
item['creator'] = '未知'
|
||||
|
||||
return ExcelUtil.export_list2excel(list_data=data, mapping_dict=mapping_dict)
|
||||
|
||||
@classmethod
|
||||
async def batch_import_yifan_solution_sections_service(cls, auth: AuthSchema, file: UploadFile, update_support: bool = False) -> str:
|
||||
"""批量导入"""
|
||||
header_dict = {
|
||||
'主键ID': 'id',
|
||||
'创建时间': 'created_time',
|
||||
'更新时间': 'updated_time',
|
||||
'创建人ID': 'created_id',
|
||||
'更新人ID': 'updated_id',
|
||||
'是否删除(0否 1是)': 'is_deleted',
|
||||
'状态(0禁用 1启用)': 'status',
|
||||
'关联方案ID': 'solution_id',
|
||||
'板块类型(ditiantai:地天泰 kaiyun_jingnang:开运锦囊 shici:诗词出处 wuxing:五行分布 liuyao:六维格局 jiazu:家族起名 bihua:笔画数理 ziyi_shengxiao:字义生肖 zonghe_pingfen:综合评分)': 'section_type',
|
||||
'板块标题': 'title',
|
||||
'摘要数据(首页卡片展示)': 'summary_data',
|
||||
'排序': 'sort_order',
|
||||
}
|
||||
|
||||
try:
|
||||
contents = await file.read()
|
||||
df = pd.read_excel(io.BytesIO(contents))
|
||||
await file.close()
|
||||
|
||||
if df.empty:
|
||||
raise CustomException(msg="导入文件为空")
|
||||
|
||||
missing_headers = [header for header in header_dict.keys() if header not in df.columns]
|
||||
if missing_headers:
|
||||
raise CustomException(msg=f"导入文件缺少必要的列: {', '.join(missing_headers)}")
|
||||
|
||||
df.rename(columns=header_dict, inplace=True)
|
||||
|
||||
# 验证必填字段
|
||||
|
||||
error_msgs = []
|
||||
success_count = 0
|
||||
count = 0
|
||||
|
||||
for index, row in df.iterrows():
|
||||
count += 1
|
||||
try:
|
||||
data = {
|
||||
"id": row['id'],
|
||||
"created_time": row['created_time'],
|
||||
"updated_time": row['updated_time'],
|
||||
"created_id": row['created_id'],
|
||||
"updated_id": row['updated_id'],
|
||||
"is_deleted": row['is_deleted'],
|
||||
"status": row['status'],
|
||||
"solution_id": row['solution_id'],
|
||||
"section_type": row['section_type'],
|
||||
"title": row['title'],
|
||||
"summary_data": row['summary_data'],
|
||||
"sort_order": row['sort_order'],
|
||||
}
|
||||
# 使用CreateSchema做校验后入库
|
||||
create_schema = YifanSolutionSectionsCreateSchema.model_validate(data)
|
||||
|
||||
# 检查唯一性约束
|
||||
|
||||
await YifanSolutionSectionsCRUD(auth).create_yifan_solution_sections_crud(data=create_schema)
|
||||
success_count += 1
|
||||
except Exception as e:
|
||||
error_msgs.append(f"第{count}行: {str(e)}")
|
||||
continue
|
||||
|
||||
result = f"成功导入 {success_count} 条数据"
|
||||
if error_msgs:
|
||||
result += "\n错误信息:\n" + "\n".join(error_msgs)
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
log.error(f"批量导入失败: {str(e)}")
|
||||
raise CustomException(msg=f"导入失败: {str(e)}")
|
||||
|
||||
@classmethod
|
||||
async def import_template_download_yifan_solution_sections_service(cls) -> bytes:
|
||||
"""下载导入模板"""
|
||||
header_list = [
|
||||
'主键ID',
|
||||
'创建时间',
|
||||
'更新时间',
|
||||
'创建人ID',
|
||||
'更新人ID',
|
||||
'是否删除(0否 1是)',
|
||||
'状态(0禁用 1启用)',
|
||||
'关联方案ID',
|
||||
'板块类型(ditiantai:地天泰 kaiyun_jingnang:开运锦囊 shici:诗词出处 wuxing:五行分布 liuyao:六维格局 jiazu:家族起名 bihua:笔画数理 ziyi_shengxiao:字义生肖 zonghe_pingfen:综合评分)',
|
||||
'板块标题',
|
||||
'摘要数据(首页卡片展示)',
|
||||
'排序',
|
||||
]
|
||||
selector_header_list = []
|
||||
option_list = []
|
||||
|
||||
# 添加下拉选项
|
||||
|
||||
return ExcelUtil.get_excel_template(
|
||||
header_list=header_list,
|
||||
selector_header_list=selector_header_list,
|
||||
option_list=option_list
|
||||
)
|
||||
@@ -0,0 +1,178 @@
|
||||
# 手机号认证功能说明
|
||||
|
||||
## 概述
|
||||
为微信小程序添加了手机号和密码的登录注册功能,支持短信验证码验证。
|
||||
|
||||
## 新增接口
|
||||
|
||||
### 1. 发送短信验证码
|
||||
- **接口路径**: `POST /yifan_wx_auth/send-sms`
|
||||
- **功能**: 发送手机短信验证码
|
||||
- **请求参数**:
|
||||
```json
|
||||
{
|
||||
"mobile": "13800138000",
|
||||
"code_type": "register" // register, resetpwd, changepwd, changemobile, mobilelogin
|
||||
}
|
||||
```
|
||||
- **响应**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"msg": "验证码发送成功"
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 手机号密码注册
|
||||
- **接口路径**: `POST /yifan_wx_auth/mobile-register`
|
||||
- **功能**: 使用手机号和密码注册新用户
|
||||
- **请求参数**:
|
||||
```json
|
||||
{
|
||||
"mobile": "13800138000",
|
||||
"password": "123456",
|
||||
"repassword": "123456",
|
||||
"verification_code": "123456",
|
||||
"inviter_id": 1 // 可选,邀请人ID
|
||||
}
|
||||
```
|
||||
- **响应**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"msg": "注册成功",
|
||||
"data": {
|
||||
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
|
||||
"refresh_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
|
||||
"token_type": "Bearer",
|
||||
"expires_in": 7200,
|
||||
"user_info": {
|
||||
"id": 1,
|
||||
"username": "mobile_38000_abc1",
|
||||
"name": "mobile_38000_abc1",
|
||||
"avatar": null,
|
||||
"mobile": "13800138000"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 手机号密码登录
|
||||
- **接口路径**: `POST /yifan_wx_auth/mobile-login`
|
||||
- **功能**: 使用手机号和密码登录
|
||||
- **请求参数**:
|
||||
```json
|
||||
{
|
||||
"mobile": "13800138000",
|
||||
"password": "123456",
|
||||
"inviter_id": 1 // 可选,邀请人ID
|
||||
}
|
||||
```
|
||||
- **响应**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"msg": "登录成功",
|
||||
"data": {
|
||||
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
|
||||
"refresh_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
|
||||
"token_type": "Bearer",
|
||||
"expires_in": 7200,
|
||||
"user_info": {
|
||||
"id": 1,
|
||||
"username": "mobile_38000_abc1",
|
||||
"name": "用户昵称",
|
||||
"avatar": null,
|
||||
"mobile": "13800138000"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 忘记密码
|
||||
- **接口路径**: `POST /yifan_wx_auth/forgot-password`
|
||||
- **功能**: 通过手机号和短信验证码重置密码
|
||||
- **请求参数**:
|
||||
```json
|
||||
{
|
||||
"mobile": "13800138000",
|
||||
"password": "newpassword123",
|
||||
"repassword": "newpassword123",
|
||||
"verification_code": "123456"
|
||||
}
|
||||
```
|
||||
- **响应**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"msg": "密码重置成功"
|
||||
}
|
||||
```
|
||||
|
||||
## 阿里云短信配置
|
||||
|
||||
### 配置信息
|
||||
- **AccessKey ID**: `LTAI5t7ox6LSot4bTXQiU39R`
|
||||
- **AccessKey Secret**: `X4Z5K3ZSrZcXzcc5HgWZNmMUmTvK8N`
|
||||
- **地域ID**: `cn-hangzhou`
|
||||
- **国内短信签名**: `山东实城派网络科技`
|
||||
- **国际短信签名**: `Shando Tech`
|
||||
- **支持国际短信**: `true`
|
||||
|
||||
### 短信模板配置
|
||||
- **注册验证码**: `SMS_486390015`
|
||||
- **重置密码验证码**: `SMS_486445022`
|
||||
- **修改密码验证码**: `SMS_486450016`
|
||||
- **修改手机号验证码**: `SMS_487250049`
|
||||
- **手机登录验证码**: `SMS_487410035`
|
||||
|
||||
### 国际短信模板
|
||||
- **国际注册验证码**: `SMS_INTL_486390015`
|
||||
- **国际重置密码验证码**: `SMS_INTL_486445022`
|
||||
- **国际修改密码验证码**: `SMS_INTL_486450016`
|
||||
- **国际修改手机号验证码**: `SMS_INTL_487250049`
|
||||
- **国际手机登录验证码**: `SMS_INTL_487410035`
|
||||
|
||||
## 功能特性
|
||||
|
||||
### 1. 短信验证码
|
||||
- **有效期**: 5分钟
|
||||
- **发送频率限制**: 1分钟内只能发送一次
|
||||
- **验证码长度**: 6位数字
|
||||
- **支持国际短信**: 自动识别国际手机号
|
||||
|
||||
### 2. 安全特性
|
||||
- 密码使用bcrypt加密存储
|
||||
- 验证码使用后自动删除
|
||||
- 支持发送频率限制
|
||||
- 注册时检查手机号是否已存在
|
||||
|
||||
### 3. 用户管理
|
||||
- 自动生成用户名格式: `mobile_{手机号后8位}_{随机4位}`
|
||||
- 支持邀请人关系绑定
|
||||
- 自动更新最后登录时间
|
||||
- 创建注册通知
|
||||
|
||||
## 依赖包
|
||||
|
||||
需要安装以下Python包:
|
||||
```
|
||||
alibabacloud-dysmsapi20170525==3.0.0 # 阿里云短信服务SDK
|
||||
alibabacloud-tea-openapi==0.3.9 # 阿里云SDK基础依赖
|
||||
alibabacloud-tea-util==0.3.13 # 阿里云SDK工具依赖
|
||||
```
|
||||
|
||||
## 使用说明
|
||||
|
||||
1. **安装依赖**: 运行 `pip install -r requirements.txt` 安装新增的依赖包
|
||||
2. **配置检查**: 确认阿里云短信服务配置信息正确
|
||||
3. **测试接口**: 使用API文档测试新增的三个接口
|
||||
4. **前端集成**: 前端可以调用这些接口实现手机号注册登录功能
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 短信验证码有5分钟有效期,请及时使用
|
||||
2. 发送验证码有频率限制,1分钟内只能发送一次
|
||||
3. 手机号格式必须是11位数字(中国大陆)
|
||||
4. 密码长度要求6-20位字符
|
||||
5. 国际手机号会自动使用国际短信模板和签名
|
||||
@@ -0,0 +1 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
@@ -0,0 +1,402 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from fastapi import APIRouter, Body, Depends, Request, UploadFile, Query
|
||||
from fastapi.responses import JSONResponse
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from redis.asyncio.client import Redis
|
||||
|
||||
from app.common.response import SuccessResponse
|
||||
from app.core.router_class import OperationLogRoute
|
||||
from app.core.logger import log
|
||||
from app.core.dependencies import db_getter, redis_getter, get_current_user
|
||||
from app.core.exceptions import CustomException
|
||||
from app.utils.upload_util import UploadUtil
|
||||
from app.utils.oss_util import OSSUtil
|
||||
from app.config.setting import settings
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from app.api.v1.module_system.notice.crud import NoticeCRUD
|
||||
from app.api.v1.module_system.notice.schema import NoticeOutSchema
|
||||
|
||||
from .service import YifanWxAuthService
|
||||
from .qrcode_service import WxMiniQRCodeService
|
||||
|
||||
from .schema import (
|
||||
WxMiniLoginSchema,
|
||||
WxMiniLoginOutSchema,
|
||||
WxMiniRegisterSchema,
|
||||
SendSmsSchema,
|
||||
MobileRegisterSchema,
|
||||
MobileLoginSchema,
|
||||
MobileLoginOutSchema,
|
||||
ForgotPasswordSchema,
|
||||
)
|
||||
from .qrcode_schema import WxMiniQRCodeSchema, WxMiniQRCodeOutSchema
|
||||
|
||||
|
||||
YifanWxAuthRouter = APIRouter(route_class=OperationLogRoute, prefix="/yifan_wx_auth", tags=["易凡小程序认证"])
|
||||
YifanNoticeRouter = APIRouter(route_class=OperationLogRoute, prefix="/notice", tags=["小程序消息"])
|
||||
|
||||
|
||||
@YifanNoticeRouter.get("/mini/list", summary="小程序消息列表", description="小程序获取消息列表(无需后台权限)")
|
||||
async def get_yifan_notice_mini_list_controller(
|
||||
page_no: int = Query(1, ge=1, description="页码"),
|
||||
page_size: int = Query(10, ge=1, le=50, description="每页数量"),
|
||||
db: AsyncSession = Depends(db_getter),
|
||||
) -> JSONResponse:
|
||||
auth = AuthSchema(db=db, check_data_scope=False)
|
||||
offset = (page_no - 1) * page_size
|
||||
result = await NoticeCRUD(auth).page(
|
||||
offset=offset,
|
||||
limit=page_size,
|
||||
order_by=[{"created_time": "desc"}],
|
||||
search={"status": "0"},
|
||||
out_schema=NoticeOutSchema,
|
||||
)
|
||||
log.info("小程序查询消息列表成功")
|
||||
return SuccessResponse(data=result, msg="获取消息列表成功")
|
||||
|
||||
|
||||
@YifanWxAuthRouter.post("/login", summary="微信小程序登录", description="微信小程序登录", response_model=WxMiniLoginOutSchema)
|
||||
async def wx_mini_login_controller(
|
||||
request: Request,
|
||||
login_data: WxMiniLoginSchema,
|
||||
redis: Redis = Depends(redis_getter),
|
||||
db: AsyncSession = Depends(db_getter),
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
微信小程序登录
|
||||
|
||||
参数:
|
||||
- request (Request): FastAPI请求对象
|
||||
- login_data (WxMiniLoginSchema): 小程序登录数据(code)
|
||||
- redis (Redis): Redis客户端对象
|
||||
- db (AsyncSession): 数据库会话对象
|
||||
|
||||
返回:
|
||||
- WxMiniLoginOutSchema: 登录响应,包含是否已注册、token等信息
|
||||
"""
|
||||
result = await YifanWxAuthService.wx_mini_login_service(
|
||||
request=request,
|
||||
redis=redis,
|
||||
db=db,
|
||||
login_data=login_data
|
||||
)
|
||||
|
||||
if result.is_registered:
|
||||
log.info(f"微信小程序用户登录成功")
|
||||
return SuccessResponse(data=result.model_dump(), msg="登录成功")
|
||||
else:
|
||||
log.info(f"微信小程序用户未注册,openid: {result.openid}")
|
||||
# 用户未注册,返回 success=False,前端可通过 success 或 is_registered 判断
|
||||
return SuccessResponse(data=result.model_dump(), msg="用户未注册", success=False)
|
||||
|
||||
# path: /yifan_wx_auth/register
|
||||
@YifanWxAuthRouter.post("/register", summary="微信小程序注册", description="微信小程序注册新用户", response_model=WxMiniLoginOutSchema)
|
||||
async def wx_mini_register_controller(
|
||||
request: Request,
|
||||
register_data: WxMiniRegisterSchema,
|
||||
redis: Redis = Depends(redis_getter),
|
||||
db: AsyncSession = Depends(db_getter),
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
微信小程序注册新用户
|
||||
|
||||
参数:
|
||||
- request (Request): FastAPI请求对象
|
||||
- register_data (WxMiniRegisterSchema): 注册数据(openid, name, avatar, mobile)
|
||||
- redis (Redis): Redis客户端对象
|
||||
- db (AsyncSession): 数据库会话对象
|
||||
|
||||
返回:
|
||||
- WxMiniLoginOutSchema: 登录响应,包含token等信息
|
||||
"""
|
||||
result = await YifanWxAuthService.wx_mini_register_service(
|
||||
request=request,
|
||||
redis=redis,
|
||||
db=db,
|
||||
register_data=register_data
|
||||
)
|
||||
|
||||
log.info(f"微信小程序用户注册成功")
|
||||
return SuccessResponse(data=result.model_dump(), msg="注册成功")
|
||||
|
||||
|
||||
@YifanWxAuthRouter.post("/logout", summary="微信小程序退出登录", description="微信小程序退出登录", dependencies=[Depends(get_current_user)])
|
||||
async def wx_mini_logout_controller(
|
||||
request: Request,
|
||||
redis: Redis = Depends(redis_getter),
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
微信小程序退出登录
|
||||
|
||||
参数:
|
||||
- request (Request): FastAPI请求对象
|
||||
- redis (Redis): Redis客户端对象
|
||||
|
||||
返回:
|
||||
- JSONResponse: 退出登录响应
|
||||
"""
|
||||
await YifanWxAuthService.wx_mini_logout_service(
|
||||
request=request,
|
||||
redis=redis
|
||||
)
|
||||
|
||||
log.info(f"微信小程序用户退出登录成功")
|
||||
return SuccessResponse(msg="退出登录成功")
|
||||
|
||||
|
||||
@YifanWxAuthRouter.post("/upload-image", summary="小程序上传图片", description="小程序上传图片到阿里云OSS")
|
||||
async def wx_mini_upload_image_controller(
|
||||
file: UploadFile,
|
||||
request: Request,
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
上传图片到阿里云OSS
|
||||
|
||||
参数:
|
||||
- file (UploadFile): 上传的图片文件
|
||||
- request (Request): FastAPI请求对象
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含文件信息的响应
|
||||
"""
|
||||
if not file or not file.filename:
|
||||
raise CustomException(msg="请选择要上传的图片")
|
||||
|
||||
filename_lower = file.filename.lower()
|
||||
allowed_image_exts = {".jpg", ".jpeg", ".png", ".gif", ".webp"}
|
||||
ext = "." + filename_lower.rsplit(".", 1)[-1] if "." in filename_lower else ""
|
||||
if ext not in allowed_image_exts:
|
||||
raise CustomException(msg="仅支持上传图片: jpg/jpeg/png/gif/webp")
|
||||
if not (file.content_type or "").startswith("image/"):
|
||||
raise CustomException(msg="仅支持上传图片")
|
||||
|
||||
# 使用OSS上传
|
||||
oss_util = OSSUtil()
|
||||
filename, oss_key, file_url = await oss_util.upload_file(file=file)
|
||||
|
||||
log.info(f"图片上传OSS成功: {filename}")
|
||||
return SuccessResponse(
|
||||
data={
|
||||
"file_path": oss_key, # OSS对象键
|
||||
"file_name": filename, # 生成的文件名
|
||||
"origin_name": file.filename, # 原始文件名
|
||||
"file_url": file_url, # OSS访问URL
|
||||
"storage_type": "oss" # 存储类型标识
|
||||
},
|
||||
msg="上传成功",
|
||||
)
|
||||
|
||||
|
||||
@YifanWxAuthRouter.delete("/delete-image", summary="删除OSS图片", description="通过文件URL删除阿里云OSS中的图片")
|
||||
async def delete_oss_image_controller(
|
||||
file_url: str = Body(..., embed=True, description="要删除的文件URL"),
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
删除OSS图片
|
||||
|
||||
参数:
|
||||
- file_url (str): 要删除的文件完整URL
|
||||
|
||||
返回:
|
||||
- JSONResponse: 删除结果响应
|
||||
"""
|
||||
if not file_url:
|
||||
raise CustomException(msg="文件URL不能为空")
|
||||
|
||||
# 验证URL格式并提取OSS Key
|
||||
try:
|
||||
# 从URL中提取OSS对象键
|
||||
# 例如: https://your-domain.com/upload/2024/02/08/filename.jpg -> upload/2024/02/08/filename.jpg
|
||||
if settings.OSS_DOMAIN not in file_url:
|
||||
raise CustomException(msg="无效的OSS文件URL")
|
||||
|
||||
# 提取OSS Key (去掉域名部分)
|
||||
oss_key = file_url.replace(f"{settings.OSS_DOMAIN}/", "")
|
||||
|
||||
if not oss_key:
|
||||
raise CustomException(msg="无法从URL中提取文件路径")
|
||||
|
||||
# 使用OSS工具删除文件
|
||||
oss_util = OSSUtil()
|
||||
success = oss_util.delete_file(oss_key)
|
||||
|
||||
if success:
|
||||
log.info(f"OSS图片删除成功: {oss_key}")
|
||||
return SuccessResponse(
|
||||
data={"deleted_file": oss_key},
|
||||
msg="图片删除成功"
|
||||
)
|
||||
else:
|
||||
raise CustomException(msg="图片删除失败")
|
||||
|
||||
except CustomException:
|
||||
raise
|
||||
except Exception as e:
|
||||
log.error(f"删除OSS图片异常: {str(e)}")
|
||||
raise CustomException(msg=f"删除图片失败: {str(e)}")
|
||||
|
||||
|
||||
@YifanWxAuthRouter.post("/generate-qrcode", summary="生成分享二维码", description="生成包含用户ID的小程序分享二维码", response_model=WxMiniQRCodeOutSchema)
|
||||
async def generate_share_qr_code_controller(
|
||||
request: Request,
|
||||
page: str = Query(default="pages/index/index", description="跳转页面路径,如: pages/index/index"),
|
||||
scene: str = Query(default=None, description="额外场景值"),
|
||||
width: int = Query(default=430, ge=280, le=1280, description="二维码宽度"),
|
||||
auto_color: bool = Query(default=False, description="自动配置线条颜色"),
|
||||
is_hyaline: bool = Query(default=False, description="是否需要透明底色"),
|
||||
current_user = Depends(get_current_user),
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
生成微信小程序分享二维码
|
||||
|
||||
参数:
|
||||
- request (Request): FastAPI请求对象
|
||||
- page (str): 跳转页面路径,默认 pages/index/index
|
||||
- scene (str): 额外场景值,可选
|
||||
- width (int): 二维码宽度,默认430px
|
||||
- auto_color (bool): 自动配置线条颜色,默认False
|
||||
- is_hyaline (bool): 是否需要透明底色,默认False
|
||||
- current_user: 当前登录用户
|
||||
|
||||
返回:
|
||||
- WxMiniQRCodeOutSchema: 二维码信息,包含URL和场景值
|
||||
"""
|
||||
# 记录请求参数用于调试
|
||||
log.info(f"生成二维码请求参数 - 用户ID: {current_user.user.id}, 页面路径: {page}, 场景值: {scene}")
|
||||
|
||||
qr_data = WxMiniQRCodeSchema(
|
||||
page=page,
|
||||
scene=scene,
|
||||
width=width,
|
||||
auto_color=auto_color,
|
||||
line_color={"r": 0, "g": 0, "b": 0},
|
||||
is_hyaline=is_hyaline
|
||||
)
|
||||
|
||||
try:
|
||||
result = await WxMiniQRCodeService.generate_qr_code(
|
||||
user_id=current_user.user.id,
|
||||
qr_data=qr_data,
|
||||
base_url=str(request.base_url)
|
||||
)
|
||||
except CustomException as e:
|
||||
log.error(f"生成二维码失败 - 用户ID: {current_user.user.id}, 页面路径: {page}, 错误: {str(e)}")
|
||||
raise
|
||||
|
||||
log.info(f"用户 {current_user.user.id} 生成分享二维码成功")
|
||||
return SuccessResponse(data=result.model_dump(), msg="二维码生成成功")
|
||||
|
||||
|
||||
@YifanWxAuthRouter.post("/send-sms", summary="发送短信验证码", description="发送手机短信验证码")
|
||||
async def send_sms_verification_code_controller(
|
||||
sms_data: SendSmsSchema,
|
||||
redis: Redis = Depends(redis_getter),
|
||||
db: AsyncSession = Depends(db_getter),
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
发送短信验证码
|
||||
|
||||
参数:
|
||||
- sms_data (SendSmsSchema): 短信发送数据(手机号、验证码类型)
|
||||
- redis (Redis): Redis客户端对象
|
||||
- db (AsyncSession): 数据库会话对象
|
||||
|
||||
返回:
|
||||
- JSONResponse: 发送结果响应
|
||||
"""
|
||||
await YifanWxAuthService.send_sms_verification_code_service(
|
||||
redis=redis,
|
||||
db=db,
|
||||
sms_data=sms_data
|
||||
)
|
||||
|
||||
log.info(f"短信验证码发送成功: {sms_data.mobile}")
|
||||
return SuccessResponse(msg="验证码发送成功")
|
||||
|
||||
|
||||
@YifanWxAuthRouter.post("/mobile-register", summary="手机号密码注册", description="使用手机号和密码注册新用户", response_model=MobileLoginOutSchema)
|
||||
async def mobile_register_controller(
|
||||
request: Request,
|
||||
register_data: MobileRegisterSchema,
|
||||
redis: Redis = Depends(redis_getter),
|
||||
db: AsyncSession = Depends(db_getter),
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
手机号密码注册新用户
|
||||
|
||||
参数:
|
||||
- request (Request): FastAPI请求对象
|
||||
- register_data (MobileRegisterSchema): 注册数据(手机号、密码、确认密码、验证码)
|
||||
- redis (Redis): Redis客户端对象
|
||||
- db (AsyncSession): 数据库会话对象
|
||||
|
||||
返回:
|
||||
- MobileLoginOutSchema: 登录响应,包含token等信息
|
||||
"""
|
||||
result = await YifanWxAuthService.mobile_register_service(
|
||||
request=request,
|
||||
redis=redis,
|
||||
db=db,
|
||||
register_data=register_data
|
||||
)
|
||||
|
||||
log.info(f"手机号用户注册成功: {register_data.mobile}")
|
||||
return SuccessResponse(data=result.model_dump(), msg="注册成功")
|
||||
|
||||
|
||||
@YifanWxAuthRouter.post("/mobile-login", summary="手机号密码登录", description="使用手机号和密码登录", response_model=MobileLoginOutSchema)
|
||||
async def mobile_login_controller(
|
||||
request: Request,
|
||||
login_data: MobileLoginSchema,
|
||||
redis: Redis = Depends(redis_getter),
|
||||
db: AsyncSession = Depends(db_getter),
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
手机号密码登录
|
||||
|
||||
参数:
|
||||
- request (Request): FastAPI请求对象
|
||||
- login_data (MobileLoginSchema): 登录数据(手机号、密码)
|
||||
- redis (Redis): Redis客户端对象
|
||||
- db (AsyncSession): 数据库会话对象
|
||||
|
||||
返回:
|
||||
- MobileLoginOutSchema: 登录响应,包含token等信息
|
||||
"""
|
||||
result = await YifanWxAuthService.mobile_login_service(
|
||||
request=request,
|
||||
redis=redis,
|
||||
db=db,
|
||||
login_data=login_data
|
||||
)
|
||||
|
||||
log.info(f"手机号用户登录成功: {login_data.mobile}")
|
||||
return SuccessResponse(data=result.model_dump(), msg="登录成功")
|
||||
|
||||
|
||||
@YifanWxAuthRouter.post("/forgot-password", summary="忘记密码", description="通过手机号和短信验证码重置密码")
|
||||
async def forgot_password_controller(
|
||||
forgot_data: ForgotPasswordSchema,
|
||||
redis: Redis = Depends(redis_getter),
|
||||
db: AsyncSession = Depends(db_getter),
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
忘记密码
|
||||
|
||||
参数:
|
||||
- forgot_data (ForgotPasswordSchema): 忘记密码数据(手机号、新密码、确认密码、验证码)
|
||||
- redis (Redis): Redis客户端对象
|
||||
- db (AsyncSession): 数据库会话对象
|
||||
|
||||
返回:
|
||||
- JSONResponse: 重置结果响应
|
||||
"""
|
||||
await YifanWxAuthService.forgot_password_service(
|
||||
redis=redis,
|
||||
db=db,
|
||||
forgot_data=forgot_data
|
||||
)
|
||||
|
||||
log.info(f"用户忘记密码重置成功: {forgot_data.mobile}")
|
||||
return SuccessResponse(msg="密码重置成功")
|
||||
@@ -0,0 +1,23 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class WxMiniQRCodeSchema(BaseModel):
|
||||
"""微信小程序二维码生成请求模型"""
|
||||
page: str = Field(default="pages/index/index", description="跳转页面路径")
|
||||
scene: Optional[str] = Field(default=None, description="场景值,最大32个可见字符")
|
||||
width: int = Field(default=430, ge=280, le=1280, description="二维码宽度,默认430px")
|
||||
auto_color: bool = Field(default=False, description="自动配置线条颜色")
|
||||
line_color: dict = Field(default={"r": 0, "g": 0, "b": 0}, description="线条颜色RGB")
|
||||
is_hyaline: bool = Field(default=False, description="是否需要透明底色")
|
||||
|
||||
|
||||
class WxMiniQRCodeOutSchema(BaseModel):
|
||||
"""微信小程序二维码生成响应模型"""
|
||||
qr_code_url: str = Field(..., description="二维码图片URL")
|
||||
qr_code_path: str = Field(..., description="二维码图片本地路径")
|
||||
scene: str = Field(..., description="场景值")
|
||||
page: str = Field(..., description="跳转页面")
|
||||
expires_at: Optional[str] = Field(None, description="过期时间")
|
||||
@@ -0,0 +1,187 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import httpx
|
||||
import aiofiles
|
||||
import re
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from urllib.parse import urljoin
|
||||
|
||||
from app.config.setting import settings
|
||||
from app.core.exceptions import CustomException
|
||||
from app.core.logger import log
|
||||
from .qrcode_schema import WxMiniQRCodeSchema, WxMiniQRCodeOutSchema
|
||||
|
||||
|
||||
class WxMiniQRCodeService:
|
||||
"""微信小程序二维码服务"""
|
||||
|
||||
@staticmethod
|
||||
async def get_access_token() -> str:
|
||||
"""获取微信小程序access_token"""
|
||||
url = "https://api.weixin.qq.com/cgi-bin/token"
|
||||
params = {
|
||||
"grant_type": "client_credential",
|
||||
"appid": settings.WX_MINI_APP_ID,
|
||||
"secret": settings.WX_MINI_APP_SECRET
|
||||
}
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(url, params=params)
|
||||
data = response.json()
|
||||
|
||||
if "errcode" in data and data["errcode"] != 0:
|
||||
log.error(f"获取access_token失败: {data}")
|
||||
raise CustomException(msg=f"获取access_token失败: {data.get('errmsg', '未知错误')}")
|
||||
|
||||
return data["access_token"]
|
||||
|
||||
@staticmethod
|
||||
def _validate_page_path(page: str) -> bool:
|
||||
"""验证小程序页面路径格式"""
|
||||
if not page:
|
||||
return False
|
||||
|
||||
# 页面路径应该符合小程序页面路径格式,如: pages/index/index, pages/user/profile
|
||||
# 不能包含特殊字符,不能是UUID格式
|
||||
page_pattern = r'^[a-zA-Z0-9_/]+$'
|
||||
|
||||
# 检查是否为UUID格式(这种格式是错误的)
|
||||
uuid_pattern = r'^[0-9a-f]{8}-[0-9a-f]{8}-[0-9a-f]{8}$'
|
||||
|
||||
if re.match(uuid_pattern, page):
|
||||
log.error(f"页面路径不能是UUID格式: {page}")
|
||||
return False
|
||||
|
||||
if not re.match(page_pattern, page):
|
||||
log.error(f"页面路径格式不正确: {page}")
|
||||
return False
|
||||
|
||||
# 页面路径通常以pages/开头
|
||||
if not page.startswith('pages/'):
|
||||
log.warning(f"页面路径建议以'pages/'开头: {page}")
|
||||
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
async def generate_qr_code(
|
||||
cls,
|
||||
user_id: int,
|
||||
qr_data: WxMiniQRCodeSchema,
|
||||
base_url: str
|
||||
) -> WxMiniQRCodeOutSchema:
|
||||
"""生成微信小程序二维码"""
|
||||
|
||||
# 验证页面路径格式
|
||||
if not cls._validate_page_path(qr_data.page):
|
||||
raise CustomException(msg=f"页面路径格式错误: {qr_data.page},应为类似 'pages/index/index' 的格式")
|
||||
|
||||
# 构建场景值,包含用户ID
|
||||
scene = f"uid={user_id}"
|
||||
if qr_data.scene:
|
||||
scene += f"&{qr_data.scene}"
|
||||
|
||||
# 检查场景值长度限制
|
||||
if len(scene) > 32:
|
||||
raise CustomException(msg="场景值超过32个字符限制")
|
||||
|
||||
# 获取access_token
|
||||
access_token = await cls.get_access_token()
|
||||
|
||||
# 调用微信API生成二维码
|
||||
url = f"https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token={access_token}"
|
||||
|
||||
payload = {
|
||||
"scene": scene,
|
||||
"page": qr_data.page,
|
||||
"width": qr_data.width,
|
||||
"auto_color": qr_data.auto_color,
|
||||
"line_color": qr_data.line_color,
|
||||
"is_hyaline": qr_data.is_hyaline
|
||||
}
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.post(url, json=payload)
|
||||
|
||||
# 检查响应是否为图片
|
||||
if response.headers.get("content-type", "").startswith("image/"):
|
||||
# 保存二维码图片
|
||||
qr_filename, qr_filepath, qr_url = await cls._save_qr_code_image(
|
||||
response.content, user_id, base_url
|
||||
)
|
||||
|
||||
return WxMiniQRCodeOutSchema(
|
||||
qr_code_url=qr_url,
|
||||
qr_code_path=str(qr_filepath),
|
||||
scene=scene,
|
||||
page=qr_data.page,
|
||||
expires_at=None # 小程序码永久有效
|
||||
)
|
||||
else:
|
||||
# 响应为错误信息
|
||||
try:
|
||||
error_data = response.json()
|
||||
error_code = error_data.get('errcode', 'unknown')
|
||||
error_msg = error_data.get('errmsg', '未知错误')
|
||||
|
||||
# 针对常见错误提供更友好的提示
|
||||
if error_code == 41030:
|
||||
error_msg = f"页面路径不存在或格式错误: {qr_data.page},请检查小程序是否已发布该页面"
|
||||
elif error_code == 45009:
|
||||
error_msg = f"接口调用超过限制,请稍后再试"
|
||||
elif error_code == 40013:
|
||||
error_msg = f"AppID无效,请检查小程序配置"
|
||||
|
||||
log.error(f"生成小程序码失败 - 错误码: {error_code}, 错误信息: {error_msg}, 页面路径: {qr_data.page}")
|
||||
raise CustomException(msg=f"生成小程序码失败: {error_msg}")
|
||||
except Exception as e:
|
||||
log.error(f"解析微信API错误响应失败: {e}, 原始响应: {response.text}")
|
||||
raise CustomException(msg=f"生成小程序码失败: 微信API返回异常响应")
|
||||
|
||||
@staticmethod
|
||||
async def _save_qr_code_image(
|
||||
image_content: bytes,
|
||||
user_id: int,
|
||||
base_url: str
|
||||
) -> tuple[str, Path, str]:
|
||||
"""保存二维码图片到本地"""
|
||||
|
||||
# 创建二维码专用目录
|
||||
qr_dir = settings.UPLOAD_FILE_PATH / "qrcode" / datetime.now().strftime("%Y/%m/%d")
|
||||
qr_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 生成文件名
|
||||
timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
|
||||
filename = f"qr_uid_{user_id}_{timestamp}.jpg"
|
||||
filepath = qr_dir / filename
|
||||
|
||||
# 保存图片
|
||||
async with aiofiles.open(filepath, 'wb') as f:
|
||||
await f.write(image_content)
|
||||
|
||||
# 生成访问URL
|
||||
file_url = urljoin(base_url, filepath.as_posix())
|
||||
|
||||
log.info(f"二维码保存成功: {filepath}")
|
||||
return filename, filepath, file_url
|
||||
|
||||
@classmethod
|
||||
async def generate_share_qr_code(
|
||||
cls,
|
||||
user_id: int,
|
||||
base_url: str,
|
||||
page: str = "pages/index/index",
|
||||
scene: str = None
|
||||
) -> WxMiniQRCodeOutSchema:
|
||||
"""生成分享二维码的便捷方法"""
|
||||
|
||||
qr_data = WxMiniQRCodeSchema(
|
||||
page=page,
|
||||
scene=scene,
|
||||
width=430,
|
||||
auto_color=False,
|
||||
line_color={"r": 0, "g": 0, "b": 0},
|
||||
is_hyaline=False
|
||||
)
|
||||
|
||||
return await cls.generate_qr_code(user_id, qr_data, base_url)
|
||||
@@ -0,0 +1,86 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from pydantic import ConfigDict, Field, BaseModel
|
||||
|
||||
|
||||
class WxMiniLoginSchema(BaseModel):
|
||||
"""微信小程序登录请求模型"""
|
||||
code: str = Field(..., min_length=1, description='微信小程序登录code')
|
||||
inviter_id: int | None = Field(default=None, description='邀请人ID(可选)')
|
||||
|
||||
|
||||
class WxMiniUserInfoSchema(BaseModel):
|
||||
"""微信小程序用户信息模型"""
|
||||
openid: str = Field(..., description='用户唯一标识')
|
||||
unionid: str | None = Field(default=None, description='用户在开放平台的唯一标识符')
|
||||
session_key: str = Field(..., description='会话密钥')
|
||||
|
||||
|
||||
class WxMiniLoginOutSchema(BaseModel):
|
||||
"""微信小程序登录响应模型"""
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
is_registered: bool = Field(..., description='是否已注册')
|
||||
access_token: str | None = Field(default=None, description='访问token')
|
||||
refresh_token: str | None = Field(default=None, description='刷新token')
|
||||
token_type: str | None = Field(default=None, description='token类型')
|
||||
expires_in: int | None = Field(default=None, description='过期时间(秒)')
|
||||
openid: str = Field(..., description='微信OpenID')
|
||||
user_info: dict | None = Field(default=None, description='用户信息')
|
||||
|
||||
|
||||
class WxMiniRegisterSchema(BaseModel):
|
||||
"""微信小程序注册请求模型"""
|
||||
openid: str = Field(..., min_length=1, description='微信OpenID')
|
||||
name: str = Field(..., min_length=1, max_length=32, description='昵称')
|
||||
avatar: str | None = Field(default=None, description='头像URL')
|
||||
mobile: str | None = Field(default=None, description='手机号')
|
||||
inviter_id: int | None = Field(default=None, description='邀请人ID(可选)')
|
||||
|
||||
|
||||
class WxMiniBindSchema(BaseModel):
|
||||
"""微信小程序绑定已有账号请求模型"""
|
||||
openid: str = Field(..., min_length=1, description='微信OpenID')
|
||||
username: str = Field(..., min_length=1, description='用户名')
|
||||
password: str = Field(..., min_length=1, description='密码')
|
||||
|
||||
|
||||
class SendSmsSchema(BaseModel):
|
||||
"""发送短信验证码请求模型"""
|
||||
mobile: str = Field(..., min_length=11, max_length=11, description='手机号')
|
||||
code_type: str = Field(default="register", description='验证码类型: register, resetpwd, changepwd, changemobile, mobilelogin')
|
||||
|
||||
|
||||
class MobileRegisterSchema(BaseModel):
|
||||
"""手机号密码注册请求模型"""
|
||||
mobile: str = Field(..., min_length=11, max_length=11, description='手机号')
|
||||
password: str = Field(..., min_length=6, max_length=20, description='密码')
|
||||
repassword: str = Field(..., min_length=6, max_length=20, description='确认密码')
|
||||
verification_code: str = Field(..., min_length=6, max_length=6, description='短信验证码')
|
||||
inviter_id: int | None = Field(default=None, description='邀请人ID(可选)')
|
||||
|
||||
|
||||
class MobileLoginSchema(BaseModel):
|
||||
"""手机号密码登录请求模型"""
|
||||
mobile: str = Field(..., min_length=11, max_length=11, description='手机号')
|
||||
password: str = Field(..., min_length=1, description='密码')
|
||||
inviter_id: int | None = Field(default=None, description='邀请人ID(可选)')
|
||||
|
||||
|
||||
class MobileLoginOutSchema(BaseModel):
|
||||
"""手机号登录响应模型"""
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
access_token: str = Field(..., description='访问token')
|
||||
refresh_token: str = Field(..., description='刷新token')
|
||||
token_type: str = Field(..., description='token类型')
|
||||
expires_in: int = Field(..., description='过期时间(秒)')
|
||||
user_info: dict = Field(..., description='用户信息')
|
||||
|
||||
|
||||
class ForgotPasswordSchema(BaseModel):
|
||||
"""忘记密码请求模型"""
|
||||
mobile: str = Field(..., min_length=11, max_length=11, description='手机号')
|
||||
password: str = Field(..., min_length=6, max_length=20, description='新密码')
|
||||
repassword: str = Field(..., min_length=6, max_length=20, description='确认新密码')
|
||||
verification_code: str = Field(..., min_length=6, max_length=6, description='短信验证码')
|
||||
@@ -0,0 +1,628 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import json
|
||||
import uuid
|
||||
import httpx
|
||||
from fastapi import Request
|
||||
from redis.asyncio.client import Redis
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from datetime import datetime, timedelta
|
||||
from user_agents import parse
|
||||
|
||||
from app.common.enums import RedisInitKeyConfig
|
||||
from app.utils.common_util import get_random_character
|
||||
from app.utils.ip_local_util import IpLocalUtil
|
||||
from app.utils.hash_bcrpy_util import PwdUtil
|
||||
from app.core.redis_crud import RedisCURD
|
||||
from app.core.exceptions import CustomException
|
||||
from app.core.logger import log
|
||||
from app.config.setting import settings
|
||||
from app.core.security import create_access_token
|
||||
|
||||
from app.api.v1.module_monitor.online.schema import OnlineOutSchema
|
||||
from app.api.v1.module_system.user.crud import UserCRUD
|
||||
from app.api.v1.module_system.user.model import UserModel
|
||||
from app.api.v1.module_system.notice.crud import NoticeCRUD
|
||||
from app.api.v1.module_system.auth.schema import JWTPayloadSchema, JWTOutSchema, AuthSchema
|
||||
|
||||
from .schema import (
|
||||
WxMiniLoginSchema,
|
||||
WxMiniUserInfoSchema,
|
||||
WxMiniLoginOutSchema,
|
||||
WxMiniRegisterSchema,
|
||||
WxMiniBindSchema,
|
||||
SendSmsSchema,
|
||||
MobileRegisterSchema,
|
||||
MobileLoginSchema,
|
||||
MobileLoginOutSchema,
|
||||
ForgotPasswordSchema
|
||||
)
|
||||
|
||||
|
||||
class YifanWxAuthService:
|
||||
"""易凡小程序微信登录服务"""
|
||||
|
||||
@classmethod
|
||||
async def create_token_service(cls, request: Request, redis: Redis, user: UserModel, login_type: str) -> JWTOutSchema:
|
||||
"""创建访问令牌和刷新令牌"""
|
||||
session_id = str(uuid.uuid4())
|
||||
request.scope["session_id"] = session_id
|
||||
|
||||
user_agent = parse(request.headers.get("user-agent"))
|
||||
x_forwarded_for = request.headers.get('X-Forwarded-For')
|
||||
if x_forwarded_for:
|
||||
request_ip = x_forwarded_for.split(',')[0].strip()
|
||||
else:
|
||||
request_ip = request.client.host if request.client else "127.0.0.1"
|
||||
|
||||
login_location = await IpLocalUtil.get_ip_location(request_ip)
|
||||
request.scope["login_location"] = login_location
|
||||
request.scope["user_username"] = user.username
|
||||
|
||||
access_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
refresh_expires = timedelta(minutes=settings.REFRESH_TOKEN_EXPIRE_MINUTES)
|
||||
now = datetime.now()
|
||||
|
||||
session_info = OnlineOutSchema(
|
||||
session_id=session_id,
|
||||
user_id=user.id,
|
||||
name=user.name,
|
||||
user_name=user.username,
|
||||
ipaddr=request_ip,
|
||||
login_location=login_location,
|
||||
os=user_agent.os.family,
|
||||
browser=user_agent.browser.family,
|
||||
login_time=user.last_login,
|
||||
login_type=login_type
|
||||
).model_dump_json()
|
||||
|
||||
access_token = create_access_token(payload=JWTPayloadSchema(
|
||||
sub=session_info,
|
||||
is_refresh=False,
|
||||
exp=now + access_expires,
|
||||
))
|
||||
refresh_token = create_access_token(payload=JWTPayloadSchema(
|
||||
sub=session_info,
|
||||
is_refresh=True,
|
||||
exp=now + refresh_expires,
|
||||
))
|
||||
|
||||
await RedisCURD(redis).set(
|
||||
key=f'{RedisInitKeyConfig.ACCESS_TOKEN.key}:{session_id}',
|
||||
value=access_token,
|
||||
expire=int(access_expires.total_seconds())
|
||||
)
|
||||
await RedisCURD(redis).set(
|
||||
key=f'{RedisInitKeyConfig.REFRESH_TOKEN.key}:{session_id}',
|
||||
value=refresh_token,
|
||||
expire=int(refresh_expires.total_seconds())
|
||||
)
|
||||
|
||||
return JWTOutSchema(
|
||||
access_token=access_token,
|
||||
refresh_token=refresh_token,
|
||||
expires_in=int(access_expires.total_seconds()),
|
||||
token_type=settings.TOKEN_TYPE
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def code2session(cls, code: str) -> WxMiniUserInfoSchema:
|
||||
"""通过code获取微信用户信息"""
|
||||
import httpx
|
||||
|
||||
url = "https://api.weixin.qq.com/sns/jscode2session"
|
||||
params = {
|
||||
"appid": settings.WX_MINI_APP_ID,
|
||||
"secret": settings.WX_MINI_APP_SECRET,
|
||||
"js_code": code,
|
||||
"grant_type": "authorization_code"
|
||||
}
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(url, params=params)
|
||||
data = response.json()
|
||||
|
||||
if "errcode" in data and data["errcode"] != 0:
|
||||
log.error(f"微信登录失败: {data}")
|
||||
raise CustomException(msg=f"微信登录失败: {data.get('errmsg', '未知错误')}")
|
||||
|
||||
return WxMiniUserInfoSchema(
|
||||
openid=data["openid"],
|
||||
unionid=data.get("unionid"),
|
||||
session_key=data["session_key"]
|
||||
)
|
||||
|
||||
|
||||
@classmethod
|
||||
async def wx_mini_login_service(
|
||||
cls,
|
||||
request: Request,
|
||||
redis: Redis,
|
||||
db: AsyncSession,
|
||||
login_data: WxMiniLoginSchema
|
||||
) -> WxMiniLoginOutSchema:
|
||||
"""微信小程序登录"""
|
||||
wx_info = await cls.code2session(login_data.code)
|
||||
|
||||
auth = AuthSchema(db=db)
|
||||
user = await UserCRUD(auth).get_by_wx_openid_crud(wx_openid=wx_info.openid)
|
||||
|
||||
if not user:
|
||||
return WxMiniLoginOutSchema(
|
||||
is_registered=False,
|
||||
openid=wx_info.openid
|
||||
)
|
||||
|
||||
if not user.status:
|
||||
raise CustomException(msg="用户已被停用")
|
||||
|
||||
if user.inviter_id is None and login_data.inviter_id is not None:
|
||||
await UserCRUD(auth).update(id=user.id, data={"inviter_id": login_data.inviter_id})
|
||||
user = await UserCRUD(auth).get_by_id_crud(id=user.id)
|
||||
|
||||
user = await UserCRUD(auth).update_last_login_crud(id=user.id)
|
||||
|
||||
token = await cls.create_token_service(
|
||||
request=request,
|
||||
redis=redis,
|
||||
user=user,
|
||||
login_type="wx_mini"
|
||||
)
|
||||
|
||||
log.info(f"微信小程序用户 {user.username} 登录成功")
|
||||
|
||||
return WxMiniLoginOutSchema(
|
||||
is_registered=True,
|
||||
access_token=token.access_token,
|
||||
refresh_token=token.refresh_token,
|
||||
token_type=token.token_type,
|
||||
expires_in=token.expires_in,
|
||||
openid=wx_info.openid,
|
||||
user_info={
|
||||
"id": user.id,
|
||||
"username": user.username,
|
||||
"name": user.name,
|
||||
"avatar": user.avatar
|
||||
}
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def wx_mini_register_service(
|
||||
cls,
|
||||
request: Request,
|
||||
redis: Redis,
|
||||
db: AsyncSession,
|
||||
register_data: WxMiniRegisterSchema
|
||||
) -> WxMiniLoginOutSchema:
|
||||
"""微信小程序注册新用户"""
|
||||
auth = AuthSchema(db=db)
|
||||
# 根据微信小程序OpenID获取用户信息
|
||||
existing_user = await UserCRUD(auth).get_by_wx_openid_crud(wx_openid=register_data.openid)
|
||||
if existing_user:
|
||||
raise CustomException(msg="该微信已绑定其他账号")
|
||||
|
||||
if register_data.mobile:
|
||||
mobile_user = await UserCRUD(auth).get_by_mobile_crud(mobile=register_data.mobile)
|
||||
if mobile_user:
|
||||
raise CustomException(msg="该手机号已被注册")
|
||||
|
||||
username = f"wx_{register_data.openid[-8:]}_{get_random_character()[:4]}"
|
||||
random_password = get_random_character()[:16]
|
||||
password_hash = PwdUtil.set_password_hash(random_password)
|
||||
|
||||
# 直接用字典创建用户,避免 UserCreateSchema 中的 role_ids 等字段
|
||||
user_data = {
|
||||
"username": username,
|
||||
"password": password_hash,
|
||||
"name": register_data.name,
|
||||
"avatar": register_data.avatar,
|
||||
"mobile": register_data.mobile,
|
||||
"inviter_id": register_data.inviter_id,
|
||||
"status": "0"
|
||||
}
|
||||
|
||||
new_user = await UserCRUD(auth).create(data=user_data)
|
||||
await UserCRUD(auth).bind_wx_openid_crud(id=new_user.id, wx_openid=register_data.openid)
|
||||
|
||||
try:
|
||||
await NoticeCRUD(auth).create_crud(
|
||||
data={
|
||||
"notice_title": f"{username}注册",
|
||||
"notice_type": "1",
|
||||
"notice_content": f"{username}注册了壹梵小程序",
|
||||
"status": "0",
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
log.error(f"注册成功但写入通知失败: {e}")
|
||||
|
||||
user = await UserCRUD(auth).get_by_id_crud(id=new_user.id)
|
||||
user = await UserCRUD(auth).update_last_login_crud(id=user.id)
|
||||
|
||||
token = await cls.create_token_service(
|
||||
request=request,
|
||||
redis=redis,
|
||||
user=user,
|
||||
login_type="wx_mini"
|
||||
)
|
||||
|
||||
log.info(f"微信小程序用户 {username} 注册成功")
|
||||
|
||||
return WxMiniLoginOutSchema(
|
||||
is_registered=True,
|
||||
access_token=token.access_token,
|
||||
refresh_token=token.refresh_token,
|
||||
token_type=token.token_type,
|
||||
expires_in=token.expires_in,
|
||||
openid=register_data.openid,
|
||||
user_info={
|
||||
"id": user.id,
|
||||
"username": user.username,
|
||||
"name": user.name,
|
||||
"avatar": user.avatar
|
||||
}
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def wx_mini_bind_service(
|
||||
cls,
|
||||
request: Request,
|
||||
redis: Redis,
|
||||
db: AsyncSession,
|
||||
bind_data: WxMiniBindSchema
|
||||
) -> WxMiniLoginOutSchema:
|
||||
"""微信小程序绑定已有账号"""
|
||||
auth = AuthSchema(db=db)
|
||||
|
||||
existing_user = await UserCRUD(auth).get_by_wx_openid_crud(wx_openid=bind_data.openid)
|
||||
if existing_user:
|
||||
raise CustomException(msg="该微信已绑定其他账号")
|
||||
|
||||
user = await UserCRUD(auth).get_by_username_crud(username=bind_data.username)
|
||||
if not user:
|
||||
raise CustomException(msg="用户不存在")
|
||||
|
||||
if not PwdUtil.verify_password(plain_password=bind_data.password, password_hash=user.password):
|
||||
raise CustomException(msg="密码错误")
|
||||
|
||||
if not user.status:
|
||||
raise CustomException(msg="用户已被停用")
|
||||
|
||||
if user.wx_openid:
|
||||
raise CustomException(msg="该账号已绑定其他微信")
|
||||
|
||||
await UserCRUD(auth).bind_wx_openid_crud(id=user.id, wx_openid=bind_data.openid)
|
||||
user = await UserCRUD(auth).update_last_login_crud(id=user.id)
|
||||
|
||||
token = await cls.create_token_service(
|
||||
request=request,
|
||||
redis=redis,
|
||||
user=user,
|
||||
login_type="wx_mini"
|
||||
)
|
||||
|
||||
log.info(f"微信小程序用户 {user.username} 绑定成功")
|
||||
|
||||
return WxMiniLoginOutSchema(
|
||||
is_registered=True,
|
||||
access_token=token.access_token,
|
||||
refresh_token=token.refresh_token,
|
||||
token_type=token.token_type,
|
||||
expires_in=token.expires_in,
|
||||
openid=bind_data.openid,
|
||||
user_info={
|
||||
"id": user.id,
|
||||
"username": user.username,
|
||||
"name": user.name,
|
||||
"avatar": user.avatar
|
||||
}
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def wx_mini_logout_service(
|
||||
cls,
|
||||
request: Request,
|
||||
redis: Redis
|
||||
) -> None:
|
||||
"""微信小程序退出登录"""
|
||||
session_id = request.state.session_id
|
||||
|
||||
# 删除Redis中的token
|
||||
await RedisCURD(redis).delete(f'{RedisInitKeyConfig.ACCESS_TOKEN.key}:{session_id}')
|
||||
await RedisCURD(redis).delete(f'{RedisInitKeyConfig.REFRESH_TOKEN.key}:{session_id}')
|
||||
|
||||
log.info(f"微信小程序用户退出登录,session_id: {session_id}")
|
||||
|
||||
@classmethod
|
||||
async def send_subscribe_message(
|
||||
cls,
|
||||
openid: str,
|
||||
template_id: str,
|
||||
data: dict,
|
||||
page: str = ""
|
||||
) -> bool:
|
||||
"""
|
||||
发送微信小程序订阅消息(公共方法)
|
||||
|
||||
参数:
|
||||
- openid: 用户OpenID
|
||||
- template_id: 模板ID
|
||||
- data: 模板数据,如 {"thing1": {"value": "xxx"}, "time2": {"value": "xxx"}}
|
||||
- page: 点击通知跳转的小程序页面路径
|
||||
"""
|
||||
if not openid:
|
||||
log.warning("[订阅消息] openid为空,跳过发送")
|
||||
return False
|
||||
|
||||
try:
|
||||
from .qrcode_service import WxMiniQRCodeService
|
||||
access_token = await WxMiniQRCodeService.get_access_token()
|
||||
|
||||
url = f"https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token={access_token}"
|
||||
payload = {
|
||||
"touser": openid,
|
||||
"template_id": template_id,
|
||||
"data": data,
|
||||
}
|
||||
if page:
|
||||
payload["page"] = page
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.post(url, json=payload)
|
||||
result = response.json()
|
||||
|
||||
if result.get("errcode", 0) != 0:
|
||||
log.warning(f"[订阅消息] 发送失败: {result}")
|
||||
return False
|
||||
|
||||
log.info(f"[订阅消息] 发送成功: openid={openid}, template={template_id}")
|
||||
return True
|
||||
except Exception as e:
|
||||
log.error(f"[订阅消息] 发送异常: {e}")
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
async def send_sms_verification_code_service(
|
||||
cls,
|
||||
redis: Redis,
|
||||
db: AsyncSession,
|
||||
sms_data: SendSmsSchema
|
||||
) -> bool:
|
||||
"""发送短信验证码服务"""
|
||||
from app.utils.sms_util import SMSUtil
|
||||
|
||||
# 检查手机号格式
|
||||
if not sms_data.mobile.isdigit() or len(sms_data.mobile) != 11:
|
||||
raise CustomException(msg="手机号格式不正确")
|
||||
|
||||
# 检查发送频率限制(1分钟内只能发送一次)
|
||||
redis_key = f"sms_code_limit:{sms_data.mobile}"
|
||||
if await RedisCURD(redis).get(redis_key):
|
||||
raise CustomException(msg="发送过于频繁,请1分钟后再试")
|
||||
|
||||
# 如果是注册类型,检查手机号是否已存在
|
||||
# if sms_data.code_type == "register":
|
||||
# auth = AuthSchema(db=db)
|
||||
# existing_user = await UserCRUD(auth).get_by_mobile_crud(mobile=sms_data.mobile)
|
||||
# if existing_user:
|
||||
# raise CustomException(msg="该手机号已被注册")
|
||||
|
||||
# 发送短信验证码(异常会直接抛出)
|
||||
sms_util = SMSUtil()
|
||||
success, verification_code = await sms_util.send_verification_code(
|
||||
mobile=sms_data.mobile,
|
||||
code_type=sms_data.code_type
|
||||
)
|
||||
|
||||
# 将验证码存储到Redis(5分钟有效期)
|
||||
code_key = f"sms_code:{sms_data.mobile}:{sms_data.code_type}"
|
||||
log.info(f"Redis key is {code_key}")
|
||||
await RedisCURD(redis).set(
|
||||
key=code_key,
|
||||
value=verification_code,
|
||||
expire=300 # 5分钟
|
||||
)
|
||||
|
||||
# 设置发送频率限制(1分钟)
|
||||
await RedisCURD(redis).set(
|
||||
key=redis_key,
|
||||
value="1",
|
||||
expire=60 # 1分钟
|
||||
)
|
||||
|
||||
log.info(f"短信验证码发送成功: {sms_data.mobile}, 类型: {sms_data.code_type}")
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
async def mobile_register_service(
|
||||
cls,
|
||||
request: Request,
|
||||
redis: Redis,
|
||||
db: AsyncSession,
|
||||
register_data: MobileRegisterSchema
|
||||
) -> MobileLoginOutSchema:
|
||||
"""手机号密码注册服务"""
|
||||
auth = AuthSchema(db=db)
|
||||
|
||||
# 验证密码一致性
|
||||
if register_data.password != register_data.repassword:
|
||||
raise CustomException(msg="两次输入的密码不一致")
|
||||
|
||||
# 验证短信验证码
|
||||
code_key = f"sms_code:{register_data.mobile}:register"
|
||||
stored_code = await RedisCURD(redis).get(code_key)
|
||||
if not stored_code or stored_code != register_data.verification_code:
|
||||
raise CustomException(msg="验证码错误或已过期")
|
||||
|
||||
# 检查手机号是否已存在
|
||||
existing_user = await UserCRUD(auth).get_by_mobile_crud(mobile=register_data.mobile)
|
||||
if existing_user:
|
||||
raise CustomException(msg="该手机号已被注册")
|
||||
|
||||
# 生成用户名
|
||||
username = f"mobile_{register_data.mobile[-8:]}_{get_random_character()[:4]}"
|
||||
password_hash = PwdUtil.set_password_hash(register_data.password)
|
||||
|
||||
# 创建用户
|
||||
user_data = {
|
||||
"username": username,
|
||||
"password": password_hash,
|
||||
"name": username, # 使用用户名作为昵称
|
||||
"mobile": register_data.mobile,
|
||||
"inviter_id": register_data.inviter_id,
|
||||
"status": "0"
|
||||
}
|
||||
|
||||
new_user = await UserCRUD(auth).create(data=user_data)
|
||||
|
||||
# 删除已使用的验证码
|
||||
await RedisCURD(redis).delete(code_key)
|
||||
|
||||
# 创建注册通知
|
||||
try:
|
||||
await NoticeCRUD(auth).create_crud(
|
||||
data={
|
||||
"notice_title": f"{username}注册",
|
||||
"notice_type": "1",
|
||||
"notice_content": f"{username}通过手机号注册了壹梵小程序",
|
||||
"status": "0",
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
log.error(f"注册成功但写入通知失败: {e}")
|
||||
|
||||
# 更新最后登录时间
|
||||
user = await UserCRUD(auth).get_by_id_crud(id=new_user.id)
|
||||
user = await UserCRUD(auth).update_last_login_crud(id=user.id)
|
||||
|
||||
# 创建token
|
||||
token = await cls.create_token_service(
|
||||
request=request,
|
||||
redis=redis,
|
||||
user=user,
|
||||
login_type="mobile"
|
||||
)
|
||||
|
||||
log.info(f"手机号用户 {username} 注册成功")
|
||||
|
||||
return MobileLoginOutSchema(
|
||||
access_token=token.access_token,
|
||||
refresh_token=token.refresh_token,
|
||||
token_type=token.token_type,
|
||||
expires_in=token.expires_in,
|
||||
user_info={
|
||||
"id": user.id,
|
||||
"username": user.username,
|
||||
"name": user.name,
|
||||
"avatar": user.avatar,
|
||||
"mobile": user.mobile
|
||||
}
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def mobile_login_service(
|
||||
cls,
|
||||
request: Request,
|
||||
redis: Redis,
|
||||
db: AsyncSession,
|
||||
login_data: MobileLoginSchema
|
||||
) -> MobileLoginOutSchema:
|
||||
"""手机号密码登录服务"""
|
||||
auth = AuthSchema(db=db)
|
||||
|
||||
# 根据手机号查找用户
|
||||
user = await UserCRUD(auth).get_by_mobile_crud(mobile=login_data.mobile)
|
||||
if not user:
|
||||
raise CustomException(msg="手机号未注册")
|
||||
|
||||
# 验证密码
|
||||
if not PwdUtil.verify_password(plain_password=login_data.password, password_hash=user.password):
|
||||
raise CustomException(msg="密码错误")
|
||||
|
||||
# 检查用户状态
|
||||
if not user.status:
|
||||
raise CustomException(msg="用户已被停用")
|
||||
|
||||
# 更新邀请人信息(如果需要)
|
||||
if user.inviter_id is None and login_data.inviter_id is not None:
|
||||
await UserCRUD(auth).update(id=user.id, data={"inviter_id": login_data.inviter_id})
|
||||
user = await UserCRUD(auth).get_by_id_crud(id=user.id)
|
||||
|
||||
# 更新最后登录时间
|
||||
user = await UserCRUD(auth).update_last_login_crud(id=user.id)
|
||||
|
||||
# 创建token
|
||||
token = await cls.create_token_service(
|
||||
request=request,
|
||||
redis=redis,
|
||||
user=user,
|
||||
login_type="mobile"
|
||||
)
|
||||
|
||||
log.info(f"手机号用户 {user.username} 登录成功")
|
||||
|
||||
return MobileLoginOutSchema(
|
||||
access_token=token.access_token,
|
||||
refresh_token=token.refresh_token,
|
||||
token_type=token.token_type,
|
||||
expires_in=token.expires_in,
|
||||
user_info={
|
||||
"id": user.id,
|
||||
"username": user.username,
|
||||
"name": user.name,
|
||||
"avatar": user.avatar,
|
||||
"mobile": user.mobile
|
||||
}
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def forgot_password_service(
|
||||
cls,
|
||||
redis: Redis,
|
||||
db: AsyncSession,
|
||||
forgot_data: ForgotPasswordSchema
|
||||
) -> bool:
|
||||
"""忘记密码服务"""
|
||||
auth = AuthSchema(db=db)
|
||||
|
||||
# 验证密码一致性
|
||||
if forgot_data.password != forgot_data.repassword:
|
||||
raise CustomException(msg="两次输入的密码不一致")
|
||||
|
||||
# 验证短信验证码
|
||||
code_key = f"sms_code:{forgot_data.mobile}:register"
|
||||
stored_code = await RedisCURD(redis).get(code_key)
|
||||
if not stored_code or stored_code != forgot_data.verification_code:
|
||||
raise CustomException(msg="验证码错误或已过期")
|
||||
|
||||
# 检查手机号对应的用户是否存在
|
||||
user = await UserCRUD(auth).get_by_mobile_crud(mobile=forgot_data.mobile)
|
||||
if not user:
|
||||
raise CustomException(msg="该手机号未注册")
|
||||
|
||||
# 检查用户状态
|
||||
if not user.status:
|
||||
raise CustomException(msg="用户已被停用")
|
||||
|
||||
# 更新密码
|
||||
password_hash = PwdUtil.set_password_hash(forgot_data.password)
|
||||
await UserCRUD(auth).update(id=user.id, data={"password": password_hash})
|
||||
|
||||
# 删除已使用的验证码
|
||||
await RedisCURD(redis).delete(code_key)
|
||||
|
||||
# 创建密码重置通知
|
||||
try:
|
||||
await NoticeCRUD(auth).create_crud(
|
||||
data={
|
||||
"notice_title": f"{user.username}重置密码",
|
||||
"notice_type": "1",
|
||||
"notice_content": f"{user.username}通过手机号重置了登录密码",
|
||||
"status": "0",
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
log.error(f"密码重置成功但写入通知失败: {e}")
|
||||
|
||||
log.info(f"用户 {user.username} 重置密码成功")
|
||||
return True
|
||||
@@ -0,0 +1 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
@@ -0,0 +1,157 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from fastapi import APIRouter, Depends, Request, Query
|
||||
from fastapi.responses import JSONResponse
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.dependencies import db_getter, get_current_user, AuthPermission
|
||||
from app.common.response import SuccessResponse, ErrorResponse
|
||||
from app.core.logger import log
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
|
||||
from .schema import CreateOrderSchema, QueryOrderSchema, OrderInfoSchema
|
||||
from .service import WxPayService
|
||||
|
||||
# 创建一个独立的路由分发器,专门管理 /yifan_wx_pay 前缀下的所有微信支付相关接口,
|
||||
# 同时给这些接口统一打上「微信支付」的标签,方便接口文档自动分类。
|
||||
router = APIRouter(prefix="/yifan_wx_pay", tags=["微信支付"])
|
||||
|
||||
|
||||
@router.post("/create-order", summary="创建支付订单", dependencies=[Depends(get_current_user)])
|
||||
async def create_order(
|
||||
request: Request,
|
||||
data: CreateOrderSchema,
|
||||
db: AsyncSession = Depends(db_getter)
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
创建微信支付订单(JSAPI/小程序支付)
|
||||
|
||||
支持两种模式:
|
||||
1. 传入code参数:通过code获取openid并存储到用户表
|
||||
2. 不传code参数:使用用户表已存储的openid
|
||||
|
||||
返回小程序调起支付所需的全部参数
|
||||
"""
|
||||
# 从JWT中获取用户信息
|
||||
user = request.state.user
|
||||
user_id = user.id
|
||||
openid = user.wx_openid
|
||||
log.info(f"用户ID is {user_id}")
|
||||
|
||||
# 如果传入了code参数,通过code获取openid
|
||||
if data.code:
|
||||
openid = await WxPayService.get_openid_by_code(
|
||||
db=db,
|
||||
user_id=user_id,
|
||||
code=data.code
|
||||
)
|
||||
elif not openid:
|
||||
return ErrorResponse(msg="用户未绑定微信,请传入code参数进行授权")
|
||||
|
||||
# 小程序内部浏览器 支付类型为jsapi。外部浏览器 支付类型则为h5。
|
||||
# 根据支付类型调用不同方法
|
||||
if data.pay_type == "h5":
|
||||
result = await WxPayService.create_h5_order(
|
||||
db=db,
|
||||
user_id=user_id,
|
||||
openid=openid,
|
||||
data=data
|
||||
)
|
||||
else:
|
||||
result = await WxPayService.create_jsapi_order(
|
||||
db=db,
|
||||
user_id=user_id,
|
||||
openid=openid,
|
||||
data=data
|
||||
)
|
||||
return SuccessResponse(data=result.model_dump())
|
||||
|
||||
|
||||
@router.get("/list", summary="获取我的支付订单列表", dependencies=[Depends(get_current_user)])
|
||||
async def list_orders(
|
||||
request: Request,
|
||||
page_no: int = Query(1, description="页码"),
|
||||
page_size: int = Query(10, description="每页数量"),
|
||||
db: AsyncSession = Depends(db_getter)
|
||||
) -> JSONResponse:
|
||||
"""分页获取当前用户的微信支付订单列表"""
|
||||
user = request.state.user
|
||||
result = await WxPayService.list_user_orders(db=db, user_id=user.id, page_no=page_no, page_size=page_size)
|
||||
return SuccessResponse(data=result.model_dump())
|
||||
|
||||
|
||||
@router.get("/admin/list", summary="管理员:获取支付订单列表")
|
||||
async def admin_list_orders(
|
||||
page_no: int = Query(1, description="页码"),
|
||||
page_size: int = Query(10, description="每页数量"),
|
||||
order_no: str | None = Query(None, description="订单号"),
|
||||
out_trade_no: str | None = Query(None, description="商户订单号"),
|
||||
transaction_id: str | None = Query(None, description="微信支付订单号"),
|
||||
user_id: int | None = Query(None, description="用户ID"),
|
||||
user_name: str | None = Query(None, description="用户昵称"),
|
||||
user_mobile: str | None = Query(None, description="用户手机号"),
|
||||
trade_state: str | None = Query(None, description="支付状态"),
|
||||
business_type: str | None = Query(None, description="业务类型"),
|
||||
db: AsyncSession = Depends(db_getter),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_yifan:yifan_wx_pay_order:query"])),
|
||||
) -> JSONResponse:
|
||||
"""分页获取全部微信支付订单列表(管理员)"""
|
||||
result = await WxPayService.list_admin_orders(
|
||||
db=db,
|
||||
page_no=page_no,
|
||||
page_size=page_size,
|
||||
order_no=order_no,
|
||||
out_trade_no=out_trade_no,
|
||||
transaction_id=transaction_id,
|
||||
user_id=user_id,
|
||||
user_name=user_name,
|
||||
user_mobile=user_mobile,
|
||||
trade_state=trade_state,
|
||||
business_type=business_type,
|
||||
)
|
||||
return SuccessResponse(data=result.model_dump())
|
||||
|
||||
|
||||
@router.get("/query-order/{out_trade_no}", summary="查询订单状态", dependencies=[Depends(get_current_user)])
|
||||
async def query_order(
|
||||
out_trade_no: str,
|
||||
db: AsyncSession = Depends(db_getter)
|
||||
) -> JSONResponse:
|
||||
"""查询订单支付状态"""
|
||||
result = await WxPayService.query_order(db=db, out_trade_no=out_trade_no)
|
||||
return SuccessResponse(data=result.model_dump())
|
||||
|
||||
|
||||
@router.post("/close-order/{out_trade_no}", summary="关闭订单", dependencies=[Depends(get_current_user)])
|
||||
async def close_order(
|
||||
out_trade_no: str,
|
||||
db: AsyncSession = Depends(db_getter)
|
||||
) -> JSONResponse:
|
||||
"""关闭未支付的订单"""
|
||||
await WxPayService.close_order(db=db, out_trade_no=out_trade_no)
|
||||
return SuccessResponse(msg="订单已关闭")
|
||||
|
||||
|
||||
@router.post("/notify", summary="支付回调通知")
|
||||
async def pay_notify(
|
||||
request: Request,
|
||||
db: AsyncSession = Depends(db_getter)
|
||||
):
|
||||
"""
|
||||
微信支付回调通知接口
|
||||
|
||||
注意:此接口不需要JWT认证,由微信服务器调用
|
||||
"""
|
||||
try:
|
||||
body = await request.json()
|
||||
log.info(f"收到支付回调: {body}")
|
||||
|
||||
success = await WxPayService.handle_pay_notify(db=db, notify_data=body)
|
||||
|
||||
if success:
|
||||
return {"code": "SUCCESS", "message": "成功"}
|
||||
else:
|
||||
return {"code": "FAIL", "message": "处理失败"}
|
||||
except Exception as e:
|
||||
log.error(f"支付回调处理异常: {e}")
|
||||
return {"code": "FAIL", "message": str(e)}
|
||||
@@ -0,0 +1,148 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from typing import Optional
|
||||
from sqlalchemy import select, update, func
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from datetime import datetime
|
||||
|
||||
from .model import WxPayOrderModel
|
||||
from .schema import CreateOrderSchema
|
||||
|
||||
|
||||
class WxPayOrderCRUD:
|
||||
"""微信支付订单CRUD"""
|
||||
|
||||
def __init__(self, db: AsyncSession):
|
||||
self.db = db
|
||||
|
||||
async def create_order(
|
||||
self,
|
||||
out_trade_no: str,
|
||||
user_id: int,
|
||||
openid: str,
|
||||
data: CreateOrderSchema,
|
||||
total_amount_fen: int = None
|
||||
) -> WxPayOrderModel:
|
||||
"""创建订单"""
|
||||
# 如果传入了分单位金额,使用分单位;否则使用元单位
|
||||
amount = total_amount_fen if total_amount_fen is not None else data.total_amount
|
||||
|
||||
order = WxPayOrderModel(
|
||||
order_no=out_trade_no,
|
||||
out_trade_no=out_trade_no,
|
||||
user_id=user_id,
|
||||
openid=openid,
|
||||
description=data.description,
|
||||
total_amount=amount,
|
||||
business_type=data.business_type,
|
||||
business_id=data.business_id,
|
||||
trade_state="NOTPAY"
|
||||
)
|
||||
self.db.add(order)
|
||||
await self.db.flush()
|
||||
await self.db.refresh(order)
|
||||
return order
|
||||
|
||||
async def get_by_out_trade_no(self, out_trade_no: str) -> Optional[WxPayOrderModel]:
|
||||
"""根据商户订单号查询"""
|
||||
result = await self.db.execute(
|
||||
select(WxPayOrderModel).where(WxPayOrderModel.out_trade_no == out_trade_no)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
async def update_order_status(
|
||||
self,
|
||||
out_trade_no: str,
|
||||
transaction_id: str,
|
||||
trade_state: str,
|
||||
trade_state_desc: str = None,
|
||||
success_time: datetime = None
|
||||
) -> Optional[WxPayOrderModel]:
|
||||
"""更新订单状态"""
|
||||
await self.db.execute(
|
||||
update(WxPayOrderModel)
|
||||
.where(WxPayOrderModel.out_trade_no == out_trade_no)
|
||||
.values(
|
||||
transaction_id=transaction_id,
|
||||
trade_state=trade_state,
|
||||
trade_state_desc=trade_state_desc,
|
||||
success_time=success_time
|
||||
)
|
||||
)
|
||||
await self.db.flush()
|
||||
return await self.get_by_out_trade_no(out_trade_no)
|
||||
|
||||
async def get_user_orders(self, user_id: int, limit: int = 20) -> list[WxPayOrderModel]:
|
||||
"""获取用户订单列表"""
|
||||
result = await self.db.execute(
|
||||
select(WxPayOrderModel)
|
||||
.where(WxPayOrderModel.user_id == user_id)
|
||||
.order_by(WxPayOrderModel.created_time.desc())
|
||||
.limit(limit)
|
||||
)
|
||||
return result.scalars().all()
|
||||
|
||||
async def page_user_orders(self, user_id: int, offset: int, limit: int) -> tuple[int, list[WxPayOrderModel]]:
|
||||
"""分页获取用户订单列表(返回 total, items)"""
|
||||
total_stmt = select(func.count()).select_from(WxPayOrderModel).where(WxPayOrderModel.user_id == user_id)
|
||||
total_result = await self.db.execute(total_stmt)
|
||||
total = int(total_result.scalar() or 0)
|
||||
|
||||
items_stmt = (
|
||||
select(WxPayOrderModel)
|
||||
.where(WxPayOrderModel.user_id == user_id)
|
||||
.order_by(WxPayOrderModel.created_time.desc())
|
||||
.offset(offset)
|
||||
.limit(limit)
|
||||
)
|
||||
items_result = await self.db.execute(items_stmt)
|
||||
return total, items_result.scalars().all()
|
||||
|
||||
async def page_orders(
|
||||
self,
|
||||
offset: int,
|
||||
limit: int,
|
||||
order_no: str | None = None,
|
||||
out_trade_no: str | None = None,
|
||||
transaction_id: str | None = None,
|
||||
user_id: int | None = None,
|
||||
user_ids: list[int] | None = None,
|
||||
trade_state: str | None = None,
|
||||
business_type: str | None = None,
|
||||
) -> tuple[int, list[WxPayOrderModel]]:
|
||||
"""管理员:分页获取订单列表(返回 total, items)"""
|
||||
|
||||
where_clauses = []
|
||||
if order_no:
|
||||
where_clauses.append(WxPayOrderModel.order_no == order_no)
|
||||
if out_trade_no:
|
||||
where_clauses.append(WxPayOrderModel.out_trade_no == out_trade_no)
|
||||
if transaction_id:
|
||||
where_clauses.append(WxPayOrderModel.transaction_id == transaction_id)
|
||||
if user_id is not None:
|
||||
where_clauses.append(WxPayOrderModel.user_id == user_id)
|
||||
if user_ids is not None:
|
||||
if len(user_ids) == 0:
|
||||
return 0, []
|
||||
where_clauses.append(WxPayOrderModel.user_id.in_(user_ids))
|
||||
if trade_state:
|
||||
where_clauses.append(WxPayOrderModel.trade_state == trade_state)
|
||||
if business_type:
|
||||
where_clauses.append(WxPayOrderModel.business_type == business_type)
|
||||
|
||||
total_stmt = select(func.count()).select_from(WxPayOrderModel)
|
||||
if where_clauses:
|
||||
total_stmt = total_stmt.where(*where_clauses)
|
||||
total_result = await self.db.execute(total_stmt)
|
||||
total = int(total_result.scalar() or 0)
|
||||
|
||||
items_stmt = select(WxPayOrderModel)
|
||||
if where_clauses:
|
||||
items_stmt = items_stmt.where(*where_clauses)
|
||||
items_stmt = (
|
||||
items_stmt.order_by(WxPayOrderModel.created_time.desc())
|
||||
.offset(offset)
|
||||
.limit(limit)
|
||||
)
|
||||
items_result = await self.db.execute(items_stmt)
|
||||
return total, items_result.scalars().all()
|
||||
@@ -0,0 +1,37 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
from sqlalchemy import String, Integer, DateTime, Numeric, Text, DECIMAL
|
||||
from sqlalchemy.sql import func
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.base_model import ModelMixin
|
||||
|
||||
|
||||
class WxPayOrderModel(ModelMixin):
|
||||
"""微信支付订单表"""
|
||||
__tablename__ = "yifan_wx_pay_order"
|
||||
|
||||
order_no: Mapped[str] = mapped_column(String(64), unique=True, nullable=False, index=True, comment="订单号")
|
||||
out_trade_no: Mapped[str] = mapped_column(String(64), unique=True, nullable=False, index=True, comment="商户订单号")
|
||||
transaction_id: Mapped[str | None] = mapped_column(String(64), nullable=True, comment="微信支付订单号")
|
||||
user_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True, comment="用户ID")
|
||||
openid: Mapped[str] = mapped_column(String(128), nullable=False, comment="用户openid")
|
||||
|
||||
# 订单信息
|
||||
description: Mapped[str] = mapped_column(String(128), nullable=False, comment="商品描述")
|
||||
total_amount: Mapped[Decimal] = mapped_column(DECIMAL(10, 2), nullable=False, comment="订单金额(元)")
|
||||
pay_amount: Mapped[Decimal | None] = mapped_column(DECIMAL(10, 2), nullable=True, comment="实付金额(元)")
|
||||
refund_amount: Mapped[Decimal | None] = mapped_column(DECIMAL(10, 2), nullable=True, comment="退款金额(元)")
|
||||
|
||||
# 业务关联
|
||||
business_type: Mapped[str] = mapped_column(String(32), nullable=False, comment="业务类型: naming_report等")
|
||||
business_id: Mapped[int | None] = mapped_column(Integer, nullable=True, comment="业务ID")
|
||||
|
||||
# 支付状态
|
||||
trade_state: Mapped[str] = mapped_column(String(32), default="NOTPAY", comment="交易状态: NOTPAY/SUCCESS/CLOSED/REFUND")
|
||||
trade_state_desc: Mapped[str | None] = mapped_column(String(256), nullable=True, comment="交易状态描述")
|
||||
|
||||
# 时间
|
||||
success_time: Mapped[datetime | None] = mapped_column(DateTime, nullable=True, comment="支付成功时间")
|
||||
@@ -0,0 +1,82 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from decimal import Decimal
|
||||
from typing import Optional, Dict, Any
|
||||
from pydantic import BaseModel, Field, ConfigDict
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class CreateOrderSchema(BaseModel):
|
||||
"""创建订单请求"""
|
||||
description: str = Field(..., max_length=128, description="商品描述")
|
||||
total_amount: Decimal = Field(..., gt=0, decimal_places=2, description="订单金额(元)")
|
||||
business_type: str = Field(..., max_length=32, description="业务类型: company_naming-企业起名, company_renaming-企业改名, company_name_analysis-企业测名, personal_naming-个人起名, personal_renaming-个人改名, personal_name_analysis-个人测名")
|
||||
business_id: Optional[int] = Field(None, description="业务ID")
|
||||
code: Optional[str] = Field(None, description="微信授权code(可选,用于获取openid)")
|
||||
pay_type: str = Field(default="h5", description="支付类型: h5-H5支付, jsapi-小程序支付")
|
||||
|
||||
|
||||
class CreateOrderOutSchema(BaseModel):
|
||||
"""创建订单响应"""
|
||||
out_trade_no: str = Field(..., description="商户订单号")
|
||||
prepay_id: str = Field(..., description="预支付交易会话标识")
|
||||
pay_type: str = Field(..., description="支付类型")
|
||||
# H5支付参数
|
||||
h5_url: Optional[str] = Field(None, description="H5支付链接")
|
||||
# 小程序调起支付所需参数
|
||||
appId: Optional[str] = Field(None, description="小程序appId")
|
||||
timeStamp: Optional[str] = Field(None, description="时间戳")
|
||||
nonceStr: Optional[str] = Field(None, description="随机字符串")
|
||||
package: Optional[str] = Field(None, description="订单详情扩展字符串")
|
||||
signType: Optional[str] = Field(None, description="签名方式")
|
||||
paySign: Optional[str] = Field(None, description="签名")
|
||||
|
||||
|
||||
class QueryOrderSchema(BaseModel):
|
||||
"""查询订单请求"""
|
||||
out_trade_no: str = Field(..., description="商户订单号")
|
||||
|
||||
|
||||
class OrderInfoSchema(BaseModel):
|
||||
"""订单信息"""
|
||||
id: int
|
||||
uuid: Optional[str] = None
|
||||
status: Optional[str | int] = None
|
||||
created_time: Optional[datetime] = None
|
||||
updated_time: Optional[datetime] = None
|
||||
|
||||
order_no: Optional[str] = None
|
||||
out_trade_no: str
|
||||
transaction_id: Optional[str]
|
||||
user_id: Optional[int] = None
|
||||
user_name: Optional[str] = Field(default=None, description="用户昵称")
|
||||
user_mobile: Optional[str] = Field(default=None, description="用户手机号")
|
||||
inviter_name: Optional[str] = Field(default=None, description="邀请人昵称")
|
||||
inviter_mobile: Optional[str] = Field(default=None, description="邀请人手机号")
|
||||
openid: Optional[str] = None
|
||||
description: str
|
||||
total_amount: Decimal
|
||||
pay_amount: Optional[Decimal] = None
|
||||
refund_amount: Optional[Decimal] = None
|
||||
business_type: str
|
||||
business_id: Optional[int]
|
||||
trade_state: str
|
||||
trade_state_desc: Optional[str]
|
||||
success_time: Optional[datetime]
|
||||
|
||||
model_config = ConfigDict(from_attributes=True, populate_by_name=True)
|
||||
|
||||
|
||||
class OrderListSchema(BaseModel):
|
||||
total: int = Field(..., description="总数")
|
||||
items: list[OrderInfoSchema] = Field(default=[], description="订单列表")
|
||||
|
||||
|
||||
class WxPayNotifySchema(BaseModel):
|
||||
"""微信支付回调通知"""
|
||||
id: str
|
||||
create_time: str
|
||||
resource_type: str
|
||||
event_type: str
|
||||
summary: str
|
||||
resource: Dict[str, Any]
|
||||
@@ -0,0 +1,628 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import json
|
||||
import time
|
||||
import uuid
|
||||
import base64
|
||||
import hashlib
|
||||
from decimal import Decimal, ROUND_HALF_UP
|
||||
from typing import Optional, Dict, Any
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from cryptography.hazmat.primitives import serialization, hashes
|
||||
from cryptography.hazmat.primitives.asymmetric import padding
|
||||
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
||||
import httpx
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.config.setting import settings
|
||||
from app.core.exceptions import CustomException
|
||||
from app.core.logger import log
|
||||
|
||||
from .crud import WxPayOrderCRUD
|
||||
from .schema import CreateOrderSchema, CreateOrderOutSchema, OrderInfoSchema, OrderListSchema
|
||||
from app.api.v1.module_system.user.crud import UserCRUD
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
|
||||
|
||||
class WxPayService:
|
||||
"""微信支付服务"""
|
||||
|
||||
# 微信支付API地址
|
||||
WX_PAY_BASE_URL = "https://api.mch.weixin.qq.com"
|
||||
|
||||
@classmethod
|
||||
def _generate_out_trade_no(cls) -> str:
|
||||
"""生成商户订单号"""
|
||||
timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
|
||||
random_str = uuid.uuid4().hex[:8].upper()
|
||||
return f"{timestamp}{random_str}"
|
||||
|
||||
@classmethod
|
||||
def _generate_nonce_str(cls) -> str:
|
||||
"""生成随机字符串"""
|
||||
return uuid.uuid4().hex
|
||||
|
||||
@classmethod
|
||||
async def get_openid_by_code(cls, db: AsyncSession, user_id: int, code: str) -> str:
|
||||
"""通过code获取openid并存储到用户表"""
|
||||
# 调用微信API获取openid
|
||||
url = "https://api.weixin.qq.com/sns/oauth2/access_token"
|
||||
params = {
|
||||
"appid": settings.WX_MINI_APP_ID,
|
||||
"secret": settings.WX_MINI_APP_SECRET,
|
||||
"code": code,
|
||||
"grant_type": "authorization_code"
|
||||
}
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(url, params=params)
|
||||
data = response.json()
|
||||
|
||||
if "errcode" in data and data["errcode"] != 0:
|
||||
log.error(f"微信授权失败: {data}")
|
||||
raise CustomException(msg=f"微信授权失败: {data.get('errmsg', '未知错误')}")
|
||||
|
||||
openid = data.get("openid")
|
||||
if not openid:
|
||||
raise CustomException(msg="获取openid失败")
|
||||
|
||||
# 更新用户表的wx_openid字段
|
||||
auth = AuthSchema(db=db)
|
||||
await UserCRUD(auth).bind_wx_openid_crud(id=user_id, wx_openid=openid)
|
||||
|
||||
log.info(f"用户{user_id}获取并存储openid成功: {openid}")
|
||||
return openid
|
||||
|
||||
@classmethod
|
||||
def _get_private_key(cls):
|
||||
"""获取商户私钥"""
|
||||
key_path = Path(settings.WX_PAY_PRIVATE_KEY_PATH)
|
||||
if not key_path.exists():
|
||||
raise CustomException(msg="商户私钥文件不存在")
|
||||
|
||||
with open(key_path, "rb") as f:
|
||||
private_key = serialization.load_pem_private_key(f.read(), password=None)
|
||||
return private_key
|
||||
|
||||
@classmethod
|
||||
def _sign(cls, message: str) -> str:
|
||||
"""RSA签名"""
|
||||
private_key = cls._get_private_key()
|
||||
signature = private_key.sign(
|
||||
message.encode("utf-8"),
|
||||
padding.PKCS1v15(),
|
||||
hashes.SHA256()
|
||||
)
|
||||
return base64.b64encode(signature).decode("utf-8")
|
||||
|
||||
@classmethod
|
||||
def _build_authorization(cls, method: str, url_path: str, body: str = "") -> str:
|
||||
"""构建Authorization头"""
|
||||
mchid = str(settings.WX_PAY_MCH_ID)
|
||||
serial_no = str(settings.WX_PAY_CERT_SERIAL_NO)
|
||||
if not mchid.isascii():
|
||||
raise CustomException(msg="微信支付配置错误: WX_PAY_MCH_ID 必须为ASCII字符")
|
||||
if not serial_no.isascii():
|
||||
raise CustomException(msg="微信支付配置错误: WX_PAY_CERT_SERIAL_NO 必须为ASCII字符")
|
||||
|
||||
timestamp = str(int(time.time()))
|
||||
nonce_str = cls._generate_nonce_str()
|
||||
|
||||
message = f"{method}\n{url_path}\n{timestamp}\n{nonce_str}\n{body}\n"
|
||||
signature = cls._sign(message)
|
||||
|
||||
return (
|
||||
f'WECHATPAY2-SHA256-RSA2048 '
|
||||
f'mchid="{mchid}",'
|
||||
f'nonce_str="{nonce_str}",'
|
||||
f'signature="{signature}",'
|
||||
f'timestamp="{timestamp}",'
|
||||
f'serial_no="{serial_no}"'
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def create_jsapi_order(
|
||||
cls,
|
||||
db: AsyncSession,
|
||||
user_id: int,
|
||||
openid: str,
|
||||
data: CreateOrderSchema
|
||||
) -> CreateOrderOutSchema:
|
||||
'''这是多行注释'''
|
||||
"""创建JSAPI支付订单(小程序支付)"""
|
||||
out_trade_no = cls._generate_out_trade_no()
|
||||
|
||||
# 调用微信支付API
|
||||
url_path = "/v3/pay/transactions/jsapi"
|
||||
log.info(f"支付价格为:{data.total_amount}元")
|
||||
# 微信支付金额单位是分,需要将元转换为分
|
||||
total_amount_fen = int((data.total_amount * Decimal("100")).quantize(Decimal("1"), rounding=ROUND_HALF_UP))
|
||||
log.info(f"支付价格转换为分后为:{total_amount_fen}分")
|
||||
|
||||
request_body = {
|
||||
"appid": settings.WX_MINI_APP_ID,
|
||||
"mchid": settings.WX_PAY_MCH_ID,
|
||||
"description": data.description,
|
||||
"out_trade_no": out_trade_no,
|
||||
"notify_url": settings.WX_PAY_NOTIFY_URL,
|
||||
"amount": {
|
||||
"total": total_amount_fen,
|
||||
"currency": "CNY"
|
||||
},
|
||||
"payer": {
|
||||
"openid": openid
|
||||
}
|
||||
}
|
||||
|
||||
body_str = json.dumps(request_body, ensure_ascii=False)
|
||||
authorization = cls._build_authorization("POST", url_path, body_str)
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.post(
|
||||
f"{cls.WX_PAY_BASE_URL}{url_path}",
|
||||
content=body_str,
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
"Authorization": authorization
|
||||
}
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
log.error(f"微信支付下单失败: {response.text}")
|
||||
raise CustomException(msg=f"微信支付下单失败: {response.text}")
|
||||
|
||||
result = response.json()
|
||||
prepay_id = result.get("prepay_id")
|
||||
|
||||
# 微信下单成功后再保存订单到数据库(方案A:失败不落库)
|
||||
crud = WxPayOrderCRUD(db)
|
||||
await crud.create_order(
|
||||
out_trade_no=out_trade_no,
|
||||
user_id=user_id,
|
||||
openid=openid,
|
||||
data=data,
|
||||
total_amount_fen=total_amount_fen
|
||||
)
|
||||
|
||||
# 生成小程序调起支付的参数
|
||||
timestamp = str(int(time.time()))
|
||||
nonce_str = cls._generate_nonce_str()
|
||||
package = f"prepay_id={prepay_id}"
|
||||
|
||||
# 签名
|
||||
sign_message = f"{settings.WX_MINI_APP_ID}\n{timestamp}\n{nonce_str}\n{package}\n"
|
||||
pay_sign = cls._sign(sign_message)
|
||||
|
||||
log.info(f"创建支付订单成功: {out_trade_no}")
|
||||
|
||||
return CreateOrderOutSchema(
|
||||
out_trade_no=out_trade_no,
|
||||
prepay_id=prepay_id,
|
||||
pay_type="jsapi",
|
||||
appId=settings.WX_MINI_APP_ID,
|
||||
timeStamp=timestamp,
|
||||
nonceStr=nonce_str,
|
||||
package=package,
|
||||
signType="RSA",
|
||||
paySign=pay_sign
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def create_h5_order(
|
||||
cls,
|
||||
db: AsyncSession,
|
||||
user_id: int,
|
||||
openid: str,
|
||||
data: CreateOrderSchema
|
||||
) -> CreateOrderOutSchema:
|
||||
"""创建H5支付订单"""
|
||||
out_trade_no = cls._generate_out_trade_no()
|
||||
|
||||
# 调用微信支付API
|
||||
url_path = "/v3/pay/transactions/h5"
|
||||
# 微信支付金额单位是分,需要将元转换为分
|
||||
total_amount_fen = int((data.total_amount * Decimal("100")).quantize(Decimal("1"), rounding=ROUND_HALF_UP))
|
||||
|
||||
request_body = {
|
||||
"appid": settings.WX_MINI_APP_ID,
|
||||
"mchid": settings.WX_PAY_MCH_ID,
|
||||
"description": data.description,
|
||||
"out_trade_no": out_trade_no,
|
||||
"notify_url": settings.WX_PAY_NOTIFY_URL,
|
||||
"amount": {
|
||||
"total": total_amount_fen,
|
||||
"currency": "CNY"
|
||||
},
|
||||
"scene_info": {
|
||||
"payer_client_ip": "127.0.0.1",
|
||||
"h5_info": {
|
||||
"type": "Wap"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
body_str = json.dumps(request_body, ensure_ascii=False)
|
||||
authorization = cls._build_authorization("POST", url_path, body_str)
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.post(
|
||||
f"{cls.WX_PAY_BASE_URL}{url_path}",
|
||||
content=body_str,
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
"Authorization": authorization
|
||||
}
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
log.error(f"微信H5支付下单失败: {response.text}")
|
||||
raise CustomException(msg=f"微信H5支付下单失败: {response.text}")
|
||||
|
||||
result = response.json()
|
||||
h5_url = result.get("h5_url")
|
||||
|
||||
# 微信下单成功后再保存订单到数据库
|
||||
crud = WxPayOrderCRUD(db)
|
||||
await crud.create_order(
|
||||
out_trade_no=out_trade_no,
|
||||
user_id=user_id,
|
||||
openid=openid,
|
||||
data=data,
|
||||
total_amount_fen=total_amount_fen
|
||||
)
|
||||
|
||||
log.info(f"创建H5支付订单成功: {out_trade_no}")
|
||||
|
||||
return CreateOrderOutSchema(
|
||||
out_trade_no=out_trade_no,
|
||||
prepay_id=result.get("prepay_id", ""),
|
||||
pay_type="h5",
|
||||
h5_url=h5_url
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def list_user_orders(cls, db: AsyncSession, user_id: int, page_no: int = 1, page_size: int = 10) -> OrderListSchema:
|
||||
"""分页获取当前用户订单列表"""
|
||||
if page_no < 1:
|
||||
page_no = 1
|
||||
if page_size < 1:
|
||||
page_size = 10
|
||||
if page_size > 100:
|
||||
page_size = 100
|
||||
|
||||
offset = (page_no - 1) * page_size
|
||||
crud = WxPayOrderCRUD(db)
|
||||
total, items = await crud.page_user_orders(user_id=user_id, offset=offset, limit=page_size)
|
||||
|
||||
# 补充用户昵称、邀请人昵称、邀请人手机号
|
||||
try:
|
||||
from sqlalchemy import select
|
||||
from app.api.v1.module_system.user.model import UserModel
|
||||
|
||||
user_ids = {o.user_id for o in items if getattr(o, "user_id", None) is not None}
|
||||
user_map: dict[int, UserModel] = {}
|
||||
inviter_map: dict[int, UserModel] = {}
|
||||
|
||||
if user_ids:
|
||||
users_result = await db.execute(select(UserModel).where(UserModel.id.in_(list(user_ids))))
|
||||
users = users_result.scalars().all()
|
||||
user_map = {u.id: u for u in users if u and u.id is not None}
|
||||
|
||||
inviter_ids = {u.inviter_id for u in users if getattr(u, "inviter_id", None)}
|
||||
if inviter_ids:
|
||||
inviters_result = await db.execute(select(UserModel).where(UserModel.id.in_(list(inviter_ids))))
|
||||
inviters = inviters_result.scalars().all()
|
||||
inviter_map = {u.id: u for u in inviters if u and u.id is not None}
|
||||
except Exception as e:
|
||||
log.error(f"[微信支付] 补充用户/邀请人信息失败: {e}")
|
||||
user_map = {}
|
||||
inviter_map = {}
|
||||
|
||||
enriched_items: list[OrderInfoSchema] = []
|
||||
for o in items:
|
||||
u = user_map.get(getattr(o, "user_id", None))
|
||||
inviter = inviter_map.get(getattr(u, "inviter_id", None)) if u else None
|
||||
enriched_items.append(
|
||||
OrderInfoSchema(
|
||||
id=o.id,
|
||||
uuid=getattr(o, "uuid", None),
|
||||
status=getattr(o, "status", None),
|
||||
created_time=getattr(o, "created_time", None),
|
||||
updated_time=getattr(o, "updated_time", None),
|
||||
order_no=getattr(o, "order_no", None),
|
||||
out_trade_no=o.out_trade_no,
|
||||
transaction_id=o.transaction_id,
|
||||
user_id=getattr(o, "user_id", None),
|
||||
user_name=u.name if u else None,
|
||||
user_mobile=u.mobile if u else None,
|
||||
inviter_name=inviter.name if inviter else None,
|
||||
inviter_mobile=inviter.mobile if inviter else None,
|
||||
openid=getattr(o, "openid", None),
|
||||
description=o.description,
|
||||
total_amount=o.total_amount,
|
||||
pay_amount=o.pay_amount,
|
||||
refund_amount=o.refund_amount,
|
||||
business_type=o.business_type,
|
||||
business_id=o.business_id,
|
||||
trade_state=o.trade_state,
|
||||
trade_state_desc=o.trade_state_desc,
|
||||
success_time=o.success_time,
|
||||
)
|
||||
)
|
||||
|
||||
return OrderListSchema(total=total, items=enriched_items)
|
||||
|
||||
@classmethod
|
||||
async def list_admin_orders(
|
||||
cls,
|
||||
db: AsyncSession,
|
||||
page_no: int = 1,
|
||||
page_size: int = 10,
|
||||
order_no: str | None = None,
|
||||
out_trade_no: str | None = None,
|
||||
transaction_id: str | None = None,
|
||||
user_id: int | None = None,
|
||||
user_name: str | None = None,
|
||||
user_mobile: str | None = None,
|
||||
trade_state: str | None = None,
|
||||
business_type: str | None = None,
|
||||
) -> OrderListSchema:
|
||||
"""管理员:分页获取全部订单列表"""
|
||||
if page_no < 1:
|
||||
page_no = 1
|
||||
if page_size < 1:
|
||||
page_size = 10
|
||||
if page_size > 100:
|
||||
page_size = 100
|
||||
|
||||
# 如果按用户昵称/手机号筛选,先解析出 user_ids
|
||||
user_ids_filter: list[int] | None = None
|
||||
if user_name or user_mobile:
|
||||
try:
|
||||
from sqlalchemy import select
|
||||
from app.api.v1.module_system.user.model import UserModel
|
||||
|
||||
user_where = []
|
||||
if user_name:
|
||||
user_where.append(UserModel.name.like(f"%{user_name}%"))
|
||||
if user_mobile:
|
||||
user_where.append(UserModel.mobile.like(f"%{user_mobile}%"))
|
||||
|
||||
users_result = await db.execute(select(UserModel.id).where(*user_where))
|
||||
user_ids = [int(x) for x in users_result.scalars().all()]
|
||||
user_ids_filter = user_ids
|
||||
except Exception as e:
|
||||
log.error(f"[微信支付][admin] 解析用户筛选条件失败: {e}")
|
||||
user_ids_filter = []
|
||||
|
||||
if not user_ids_filter:
|
||||
return OrderListSchema(total=0, items=[])
|
||||
|
||||
offset = (page_no - 1) * page_size
|
||||
crud = WxPayOrderCRUD(db)
|
||||
total, items = await crud.page_orders(
|
||||
offset=offset,
|
||||
limit=page_size,
|
||||
order_no=order_no,
|
||||
out_trade_no=out_trade_no,
|
||||
transaction_id=transaction_id,
|
||||
user_id=user_id,
|
||||
user_ids=user_ids_filter,
|
||||
trade_state=trade_state,
|
||||
business_type=business_type,
|
||||
)
|
||||
|
||||
# 补充用户昵称、邀请人昵称、邀请人手机号
|
||||
try:
|
||||
from sqlalchemy import select
|
||||
from app.api.v1.module_system.user.model import UserModel
|
||||
|
||||
user_ids = {o.user_id for o in items if getattr(o, "user_id", None) is not None}
|
||||
user_map: dict[int, UserModel] = {}
|
||||
inviter_map: dict[int, UserModel] = {}
|
||||
|
||||
if user_ids:
|
||||
users_result = await db.execute(select(UserModel).where(UserModel.id.in_(list(user_ids))))
|
||||
users = users_result.scalars().all()
|
||||
user_map = {u.id: u for u in users if u and u.id is not None}
|
||||
|
||||
inviter_ids = {u.inviter_id for u in users if getattr(u, "inviter_id", None)}
|
||||
if inviter_ids:
|
||||
inviters_result = await db.execute(select(UserModel).where(UserModel.id.in_(list(inviter_ids))))
|
||||
inviters = inviters_result.scalars().all()
|
||||
inviter_map = {u.id: u for u in inviters if u and u.id is not None}
|
||||
except Exception as e:
|
||||
log.error(f"[微信支付][admin] 补充用户/邀请人信息失败: {e}")
|
||||
user_map = {}
|
||||
inviter_map = {}
|
||||
|
||||
enriched_items: list[OrderInfoSchema] = []
|
||||
for o in items:
|
||||
u = user_map.get(getattr(o, "user_id", None))
|
||||
inviter = inviter_map.get(getattr(u, "inviter_id", None)) if u else None
|
||||
enriched_items.append(
|
||||
OrderInfoSchema(
|
||||
id=o.id,
|
||||
uuid=getattr(o, "uuid", None),
|
||||
status=getattr(o, "status", None),
|
||||
created_time=getattr(o, "created_time", None),
|
||||
updated_time=getattr(o, "updated_time", None),
|
||||
order_no=getattr(o, "order_no", None),
|
||||
out_trade_no=o.out_trade_no,
|
||||
transaction_id=o.transaction_id,
|
||||
user_id=getattr(o, "user_id", None),
|
||||
user_name=u.name if u else None,
|
||||
user_mobile=u.mobile if u else None,
|
||||
inviter_name=inviter.name if inviter else None,
|
||||
inviter_mobile=inviter.mobile if inviter else None,
|
||||
openid=getattr(o, "openid", None),
|
||||
description=o.description,
|
||||
total_amount=o.total_amount,
|
||||
pay_amount=o.pay_amount,
|
||||
refund_amount=o.refund_amount,
|
||||
business_type=o.business_type,
|
||||
business_id=o.business_id,
|
||||
trade_state=o.trade_state,
|
||||
trade_state_desc=o.trade_state_desc,
|
||||
success_time=o.success_time,
|
||||
)
|
||||
)
|
||||
|
||||
return OrderListSchema(total=total, items=enriched_items)
|
||||
|
||||
@classmethod
|
||||
async def query_order(cls, db: AsyncSession, out_trade_no: str) -> OrderInfoSchema:
|
||||
"""查询订单状态"""
|
||||
crud = WxPayOrderCRUD(db)
|
||||
order = await crud.get_by_out_trade_no(out_trade_no)
|
||||
|
||||
if not order:
|
||||
raise CustomException(msg="订单不存在")
|
||||
|
||||
# 如果订单未支付,同步查询微信支付状态
|
||||
if order.trade_state == "NOTPAY":
|
||||
url_path = f"/v3/pay/transactions/out-trade-no/{out_trade_no}?mchid={settings.WX_PAY_MCH_ID}"
|
||||
authorization = cls._build_authorization("GET", url_path)
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(
|
||||
f"{cls.WX_PAY_BASE_URL}{url_path}",
|
||||
headers={
|
||||
"Accept": "application/json",
|
||||
"Authorization": authorization
|
||||
}
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
trade_state = result.get("trade_state")
|
||||
|
||||
if trade_state != "NOTPAY":
|
||||
success_time = None
|
||||
if result.get("success_time"):
|
||||
success_time = datetime.fromisoformat(result["success_time"].replace("Z", "+00:00"))
|
||||
|
||||
order = await crud.update_order_status(
|
||||
out_trade_no=out_trade_no,
|
||||
transaction_id=result.get("transaction_id", ""),
|
||||
trade_state=trade_state,
|
||||
trade_state_desc=result.get("trade_state_desc"),
|
||||
success_time=success_time
|
||||
)
|
||||
|
||||
return OrderInfoSchema.model_validate(order)
|
||||
|
||||
@classmethod
|
||||
def decrypt_notify_resource(cls, resource: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""解密回调通知数据"""
|
||||
algorithm = resource.get("algorithm")
|
||||
if algorithm != "AEAD_AES_256_GCM":
|
||||
raise CustomException(msg=f"不支持的加密算法: {algorithm}")
|
||||
|
||||
nonce = resource.get("nonce")
|
||||
ciphertext = resource.get("ciphertext")
|
||||
associated_data = resource.get("associated_data", "")
|
||||
|
||||
key = settings.WX_PAY_API_V3_KEY.encode("utf-8")
|
||||
nonce_bytes = nonce.encode("utf-8")
|
||||
ciphertext_bytes = base64.b64decode(ciphertext)
|
||||
associated_data_bytes = associated_data.encode("utf-8")
|
||||
|
||||
aesgcm = AESGCM(key)
|
||||
plaintext = aesgcm.decrypt(nonce_bytes, ciphertext_bytes, associated_data_bytes)
|
||||
|
||||
return json.loads(plaintext.decode("utf-8"))
|
||||
|
||||
@classmethod
|
||||
async def handle_pay_notify(cls, db: AsyncSession, notify_data: Dict[str, Any]) -> bool:
|
||||
"""处理支付回调通知"""
|
||||
event_type = notify_data.get("event_type")
|
||||
|
||||
if event_type != "TRANSACTION.SUCCESS":
|
||||
log.warning(f"收到非成功支付通知: {event_type}")
|
||||
return True
|
||||
|
||||
# 解密数据
|
||||
resource = notify_data.get("resource", {})
|
||||
decrypted_data = cls.decrypt_notify_resource(resource)
|
||||
|
||||
out_trade_no = decrypted_data.get("out_trade_no")
|
||||
transaction_id = decrypted_data.get("transaction_id")
|
||||
trade_state = decrypted_data.get("trade_state")
|
||||
|
||||
success_time = None
|
||||
if decrypted_data.get("success_time"):
|
||||
success_time = datetime.fromisoformat(decrypted_data["success_time"].replace("Z", "+00:00"))
|
||||
|
||||
# 更新订单状态
|
||||
crud = WxPayOrderCRUD(db)
|
||||
order = await crud.get_by_out_trade_no(out_trade_no)
|
||||
|
||||
if not order:
|
||||
log.error(f"回调订单不存在: {out_trade_no}")
|
||||
return False
|
||||
|
||||
if order.trade_state == "SUCCESS":
|
||||
log.info(f"订单已处理过: {out_trade_no}")
|
||||
return True
|
||||
|
||||
await crud.update_order_status(
|
||||
out_trade_no=out_trade_no,
|
||||
transaction_id=transaction_id,
|
||||
trade_state=trade_state,
|
||||
trade_state_desc=decrypted_data.get("trade_state_desc"),
|
||||
success_time=success_time
|
||||
)
|
||||
|
||||
log.info(f"支付回调处理成功: {out_trade_no}")
|
||||
|
||||
# TODO: 这里可以添加业务处理逻辑,比如更新命名报告状态等
|
||||
# business_type = order.business_type
|
||||
# business_id = order.business_id
|
||||
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
async def close_order(cls, db: AsyncSession, out_trade_no: str) -> bool:
|
||||
"""关闭订单"""
|
||||
crud = WxPayOrderCRUD(db)
|
||||
order = await crud.get_by_out_trade_no(out_trade_no)
|
||||
|
||||
if not order:
|
||||
raise CustomException(msg="订单不存在")
|
||||
|
||||
if order.trade_state != "NOTPAY":
|
||||
raise CustomException(msg="只能关闭未支付的订单")
|
||||
|
||||
url_path = f"/v3/pay/transactions/out-trade-no/{out_trade_no}/close"
|
||||
request_body = {"mchid": settings.WX_PAY_MCH_ID}
|
||||
body_str = json.dumps(request_body)
|
||||
authorization = cls._build_authorization("POST", url_path, body_str)
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.post(
|
||||
f"{cls.WX_PAY_BASE_URL}{url_path}",
|
||||
content=body_str,
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": authorization
|
||||
}
|
||||
)
|
||||
|
||||
if response.status_code != 204:
|
||||
log.error(f"关闭订单失败: {response.text}")
|
||||
raise CustomException(msg="关闭订单失败")
|
||||
|
||||
await crud.update_order_status(
|
||||
out_trade_no=out_trade_no,
|
||||
transaction_id="",
|
||||
trade_state="CLOSED",
|
||||
trade_state_desc="商户主动关闭"
|
||||
)
|
||||
|
||||
log.info(f"订单已关闭: {out_trade_no}")
|
||||
return True
|
||||
Reference in New Issue
Block a user