diff --git a/.gitignore b/.gitignore index 72f6b1a170402dd3dd4b1712b7e718c10e8bd9cf..14a23d3233df68313902ae6a6acca3ccbdd3553d 100644 --- a/.gitignore +++ b/.gitignore @@ -90,3 +90,20 @@ docs/_build/ # PyBuilder target/ +/backend/.venv/Include/site/python3.11/greenlet/greenlet.h +/backend/.venv/Scripts/Activate.ps1 +/backend/.venv/Scripts/activate +/backend/.venv/Scripts/activate.bat +/backend/.venv/Scripts/celery.exe +/backend/.venv/Scripts/deactivate.bat +/backend/.venv/Scripts/dotenv.exe +/backend/.venv/Scripts/gunicorn.exe +/backend/.venv/Scripts/jp.py +/backend/.venv/Scripts/normalizer.exe +/backend/.venv/Scripts/pip.exe +/backend/.venv/Scripts/pip3.11.exe +/backend/.venv/Scripts/pip3.exe +/backend/.venv/Scripts/python.exe +/backend/.venv/Scripts/pythonw.exe +/backend/.venv/Scripts/uvicorn.exe +/backend/.venv/pyvenv.cfg diff --git a/README.md b/README.md index cc392e68a7b3cb6074c7df981ff71db3991a25c4..1793ab24cc071c6963ab88bc0340f2e9d7964978 100644 --- a/README.md +++ b/README.md @@ -46,13 +46,19 @@ git clone https://github.com/baizunxian/zerorunner.git # 数据库脚本 将内容复制数据库执行 需要新建数据库 zerorunner backend/script/db_init.sql -# 修改对应的数据库地址,redis 地址 +# 修改对应的数据库地址,redis 地址 , redis 密码可能是空 backend/config.py # 或者 -backend/.env # 环境文件中的地址修改 +backend/.env # 环境文件中的地址修改 -> 这里需要拷贝 .env.exmaple 文件 改为.env + +# 创建虚拟环境 +python -m venv venv + +# 启动虚拟环境 +venv\Scripts\activate # Windows # 安装依赖 -pip install -r requirements +pip install -r requirements -i https://pypi.tuna.tsinghua.edu.cn/simple # 运行项目 zerorunner/backend 目录下执行 python main.py @@ -92,12 +98,16 @@ git clone https://github.com/baizunxian/zerorunner.git cd zerorunner/frontend # 安装依赖 -cnpm install +npm install + +# 会出现依赖版本错误 npm install monaco-editor@0.27.0 + # 或者 yarn insatll # 运行项目 -cnpm run dev +npm run dev + # 或者 yarn dev diff --git a/backend/.env.example b/backend/.env.example index 8d99baf24f77facbeade49630b32e908a8675839..f831dd7fc638c016c2e30bf4becaea0d7163376f 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -1,11 +1,14 @@ # redis -REDIS_URI=redis://:redis@localhost:6379/4 +REDIS_URI=redis://:@localhost:6379/4 # mysql 异步 -MYSQL_DATABASE_URI=mysql+asyncmy://root:123456@localhost:3306/zerorunner?charset=UTF8MB4 +MYSQL_DATABASE_URI=mysql+asyncmy://root:lsj123@localhost:3306/zerorunner?charset=UTF8MB4 # celery -CELERY_BROKER_URL=redis://:redis@localhost:6379/5 -CELERY_RESULT_BACKEND=redis://:redis@localhost:6379/5 -CELERY_BEAT_DB_URL=mysql+pymysql://root:123456@localhost:3306/zerorunner?charset=UTF8MB4 +CELERY_BROKER_URL=redis://:@localhost:6379/5 +CELERY_RESULT_BACKEND=redis://:@localhost:6379/5 +CELERY_BEAT_DB_URL=mysql+pymysql://root:lsj123@localhost:3306/zerorunner?charset=UTF8MB4 + +# 授权回调地址 +CALL_BACK_REDIRECT_URI="127.0.0.1/account/auth_callback" \ No newline at end of file diff --git a/backend/app/apis/api_router.py b/backend/app/apis/api_router.py index 88b6d54cf0735f6ec34e15da5615c959d9901014..2c274df23af7c69bec006867a885e6d26dea7aab 100644 --- a/backend/app/apis/api_router.py +++ b/backend/app/apis/api_router.py @@ -4,6 +4,7 @@ from fastapi import APIRouter from app.apis.system import user, menu, roles, lookup, id_center, file +from app.apis.ocean import account, product, report app_router = APIRouter() @@ -14,3 +15,6 @@ app_router.include_router(roles.router, prefix="/roles", tags=["roles"]) app_router.include_router(lookup.router, prefix="/lookup", tags=["lookup"]) app_router.include_router(id_center.router, prefix="/idCenter", tags=["idCenter"]) app_router.include_router(file.router, prefix="/file", tags=["file"]) +app_router.include_router(account.router, prefix="/account", tags=["account 账户数据"]) +app_router.include_router(product.router, prefix="/product", tags=["product 商品数据"]) +app_router.include_router(report.router, prefix="/report", tags=["report 用户报表,roi"]) diff --git a/backend/app/apis/ocean/__init__.py b/backend/app/apis/ocean/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..fb0deb282fe8981aac49eaa9ccc95807d18e2f82 --- /dev/null +++ b/backend/app/apis/ocean/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +# @author: xiaobai diff --git a/backend/app/apis/ocean/account.py b/backend/app/apis/ocean/account.py new file mode 100644 index 0000000000000000000000000000000000000000..a207975b84015c86627cd5501d10a92f9d16bc14 --- /dev/null +++ b/backend/app/apis/ocean/account.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- + + +from fastapi import APIRouter, Request + +from app.apis.ocean.account_result_data import _process_advertiser_list, _process_authed_advertisers, \ + _process_aweme_list, _process_account_balance +from app.corelibs.logger import logger +from app.services.ocean.account import AccountService +from app.schemas.ocean.account import GetAdvertiserListParam, GetAuthedAwemeListParam, GetAccountBalanceParam +from app.apis.ocean.base_controller import BaseController + +router = APIRouter() + + +@router.post("/advertiser/list/") +async def get_advertiser_list(param: GetAdvertiserListParam, request: Request): + """ + 获取广告主列表 + https://open.oceanengine.com/labels/12/docs/1796368918556803?origin=left_nav + """ + return await BaseController.handle_api_request( + request=request, + service_call=lambda token: AccountService.get_advertiser_list(param, token), + data_processor=_process_advertiser_list + ) + + +@router.get("/authed_advertiser/get/") +async def get_authed_advertisers(request: Request): + """ + 获取授权账号列表 + https://open.oceanengine.com/labels/12/docs/1697467748096067?origin=left_nav + """ + return await BaseController.handle_api_request( + request=request, + service_call=lambda token: AccountService.get_authed_advertisers(token), + data_processor=_process_authed_advertisers + ) + + +@router.post("/aweme/authorized/get/") +async def get_aweme_authed_list(param: GetAuthedAwemeListParam, request: Request): + """ + 获取千川账户下可投广抖音号 + https://open.oceanengine.com/labels/12/docs/1697467726080011?origin=left_nav + """ + return await BaseController.handle_api_request( + request=request, + service_call=lambda token: AccountService.get_aweme_authed_list(param, token), + data_processor=_process_aweme_list + ) + + +@router.post("/account_balance/get/") +async def get_account_balance(param: GetAccountBalanceParam, request: Request): + """ + 获取账户余额 + https://open.oceanengine.com/labels/12/docs/1783322092364800?origin=left_nav + """ + + return await BaseController.handle_api_request( + request=request, + service_call=lambda token: AccountService.get_account_balance(param, token), + data_processor=_process_account_balance + ) diff --git a/backend/app/apis/ocean/account_result_data.py b/backend/app/apis/ocean/account_result_data.py new file mode 100644 index 0000000000000000000000000000000000000000..f33a218157beba364f7f769ce82bb16a049c9e63 --- /dev/null +++ b/backend/app/apis/ocean/account_result_data.py @@ -0,0 +1,154 @@ +from typing import Dict +import math + + +def _process_advertiser_list(data: Dict) -> Dict: + """处理广告主列表数据""" + return { + "list": data["list"], + "page_info": { + "page": data["page_info"]["page"], + "page_size": data["page_info"]["page_size"], + "total_number": data["page_info"]["total_number"], + "total_page": math.ceil(data["page_info"]["total_number"] / data["page_info"]["page_size"]) + } + } + + +def _process_authed_advertisers(data: Dict) -> Dict: + """ + 处理授权广告主数据 + 返回结构: + { + "list": [{ + "advertiser_id": int, + "advertiser_name": str, + "account_role": str, + "is_valid": bool, + "company_list": list + }], + "total": int + } + """ + processed_list = [] + for item in data.get("list", []): + processed_list.append({ + "advertiser_id": item.get("advertiser_id"), + "advertiser_name": item.get("advertiser_name", ""), + "account_role": item.get("account_role"), + "is_valid": item.get("is_valid", False), + "company_list": item.get("company_list", []) + }) + + return { + "list": processed_list, + "total": len(processed_list) + } + + +def _process_aweme_list(data: Dict) -> Dict: + """ + 解析抖音号列表数据中的data部分 + 返回结构: + { + "aweme_id_list": [{ + "aweme_avatar": str, + "aweme_id": int, + "aweme_show_id": str, + "aweme_name": str, + "aweme_status": str, + "bind_type": List[str], + "aweme_has_video_permission": bool, + "aweme_has_live_permission": bool, + "aweme_has_uni_prom": bool, + "aweme_has_publish_permission": bool + }], + "page_info": { + "page": int, + "page_size": int, + "total_number": int, + "total_page": int + } + } + """ + # 处理抖音号列表 + aweme_list = data.get("aweme_id_list", []) + processed_aweme_list = [] + for item in aweme_list: + processed_aweme_list.append({ + "aweme_avatar": item.get("aweme_avatar", ""), + "aweme_id": item.get("aweme_id", 0), + "aweme_show_id": item.get("aweme_show_id", ""), + "aweme_name": item.get("aweme_name", ""), + "aweme_status": item.get("aweme_status", "NORMAL"), + "bind_type": item.get("bind_type", []), + "aweme_has_video_permission": item.get("aweme_has_video_permission", False), + "aweme_has_live_permission": item.get("aweme_has_live_permission", False), + "aweme_has_uni_prom": item.get("aweme_has_uni_prom", False), + "aweme_has_publish_permission": item.get("aweme_has_publish_permission", False) + }) + + # 处理分页信息 + page_info = data.get("page_info", {}) + processed_page_info = { + "page": page_info.get("page", 1), + "page_size": page_info.get("page_size", 10), + "total_number": page_info.get("total_number", 0), + "total_page": page_info.get("total_page", 0) + } + + return { + "aweme_id_list": processed_aweme_list, + "page_info": processed_page_info + } + + +def _process_account_balance(data: Dict) -> Dict: + """ + 解析账户余额数据(单位:千分之一分) + 返回结构: + { + "advertiser_id": int, + "account_total": int, + "account_valid": int, + "account_frozen": int, + "account_general_total": int, + "account_general_valid": int, + "account_general_frozen": int, + "account_bidding_total": int, + "account_bidding_valid": int, + "account_bidding_frozen": int, + "account_brand_total": int, + "account_brand_valid": int, + "account_brand_frozen": int, + "share_grant_total": int, + "share_wallet_general_valid": int, + "share_wallet_bidding_valid": int, + "share_wallet_brand_valid": int, + "share_wallet_id": str, + "share_wallet_name": str, + "share_wallet_total": int + } + """ + return { + "advertiser_id": data.get("advertiser_id", 0), + "account_total": data.get("account_total", 0), + "account_valid": data.get("account_valid", 0), + "account_frozen": data.get("account_frozen", 0), + "account_general_total": data.get("account_general_total", 0), + "account_general_valid": data.get("account_general_valid", 0), + "account_general_frozen": data.get("account_general_frozen", 0), + "account_bidding_total": data.get("account_bidding_total", 0), + "account_bidding_valid": data.get("account_bidding_valid", 0), + "account_bidding_frozen": data.get("account_bidding_frozen", 0), + "account_brand_total": data.get("account_brand_total", 0), + "account_brand_valid": data.get("account_brand_valid", 0), + "account_brand_frozen": data.get("account_brand_frozen", 0), + "share_grant_total": data.get("share_grant_total", 0), + "share_wallet_general_valid": data.get("share_wallet_general_valid", 0), + "share_wallet_bidding_valid": data.get("share_wallet_bidding_valid", 0), + "share_wallet_brand_valid": data.get("share_wallet_brand_valid", 0), + "share_wallet_id": data.get("share_wallet_id", ""), + "share_wallet_name": data.get("share_wallet_name", ""), + "share_wallet_total": data.get("share_wallet_total", 0) + } diff --git a/backend/app/apis/ocean/base_controller.py b/backend/app/apis/ocean/base_controller.py new file mode 100644 index 0000000000000000000000000000000000000000..d819b7e7d07145defd2e73f41d389c122dcbb74a --- /dev/null +++ b/backend/app/apis/ocean/base_controller.py @@ -0,0 +1,70 @@ +from fastapi import Request, status +from loguru import logger +from typing import Callable, Dict, Any +from app.corelibs.http_response import partner_success +from app.exceptions.exceptions import ThirdPartyAPIError + + +class BaseController: + @classmethod + async def handle_api_request( + cls, + request: Request, + service_call: Callable, + data_processor: Callable[[Dict], Dict] = None, + required_token: bool = True + ): + """ + 统一处理API请求 + :param service_call: 服务层调用方法 + :param data_processor: 数据处理函数 + :param required_token: 是否必须token + """ + try: + # Token验证 + token = request.headers.get("token") + if required_token and not token: + return partner_success( + data=None, + code=401, + msg="未提供访问令牌", + http_code=status.HTTP_401_UNAUTHORIZED + ) + + # 调用服务层 + service_result = await service_call(token if required_token else None) + + logger.info(f"handle_api_request ==== {service_result}") + # 数据处理 + response_data = data_processor(service_result["data"]) if data_processor else service_result["data"] + logger.info(f"handle_api_request response_data ==== {response_data}") + return partner_success( + data=response_data, + http_code=status.HTTP_200_OK + ) + + except ThirdPartyAPIError as e: + logger.error(f"第三方API错误: {str(e)}") + return cls._handle_error(e) + except Exception as e: + logger.exception("服务处理异常") + return cls._handle_error(e) + + @staticmethod + def _handle_error(e: Exception): + """统一错误处理""" + if isinstance(e, ThirdPartyAPIError): + http_code = status.HTTP_502_BAD_GATEWAY if e.code >= 500 else status.HTTP_400_BAD_REQUEST + return partner_success( + data=None, + code=e.code, + msg=f"第三方服务错误: {e.message}", + http_code=http_code + ) + else: + return partner_success( + data=None, + code=500, + msg=f"服务端错误: {str(e)}", + http_code=status.HTTP_500_INTERNAL_SERVER_ERROR + ) diff --git a/backend/app/apis/ocean/product.py b/backend/app/apis/ocean/product.py new file mode 100644 index 0000000000000000000000000000000000000000..bc5179747d566bf6470f8cad77693923a53bab52 --- /dev/null +++ b/backend/app/apis/ocean/product.py @@ -0,0 +1,49 @@ +from fastapi import APIRouter, Request +from fastapi import Header + +from app.apis.ocean.base_controller import BaseController +from app.apis.ocean.product_result_data import _process_product_data, _process_video_data, _process_image_data +from app.schemas.ocean.product import GetProductListParam, GetVideoListParam, GetImageListParam +from app.services.ocean.product import ProductService + +router = APIRouter() + + +@router.post("/get/") +async def get_product_list(param: GetProductListParam, request: Request): + """ + 获取商品列表 + https://open.oceanengine.com/labels/12/docs/1825216033296576?origin=left_nav + https://open.oceanengine.com/labels/12/docs/1825216221095947 + """ + return await BaseController.handle_api_request( + request=request, + service_call=lambda token: ProductService.get_product_list(param, token), + data_processor=_process_product_data + ) + + +@router.post("/video/get/") +async def get_video_list(param: GetVideoListParam, request: Request): + """ + 获取视频素材 + https://open.oceanengine.com/labels/12/docs/1739309912219663 + """ + return await BaseController.handle_api_request( + request=request, + service_call=lambda token: ProductService.get_video_list(param, token), + data_processor=_process_video_data + ) + + +@router.post("/image/get/") +async def get_image_list(param: GetImageListParam, request: Request): + """ + 获取图片素材 + https://open.oceanengine.com/labels/12/docs/1739304248623182?origin=left_nav + """ + return await BaseController.handle_api_request( + request=request, + service_call=lambda token: ProductService.get_image_list(param, token), + data_processor=_process_image_data + ) diff --git a/backend/app/apis/ocean/product_result_data.py b/backend/app/apis/ocean/product_result_data.py new file mode 100644 index 0000000000000000000000000000000000000000..c4c5d6c90fcf473ef945845169ce1c72d22f377d --- /dev/null +++ b/backend/app/apis/ocean/product_result_data.py @@ -0,0 +1,191 @@ +from typing import Dict +import math + +from typing import Dict, List, Optional + + +def _process_product_data(data: Dict) -> Dict: + """ + 解析商品列表数据 + 返回结构: + { + "product_list": [{ + "id": int, + "name": str, + "img": str, + "tag": List[str], + "category_name": str, + "sell_num": int, + "gray_reason": List[str], + "square_image_list": List[str], + "channel_id": Optional[int], + "channel_type": Optional[str] + }], + "page_info": { + "cursor": int, + "has_more": bool + } + } + """ + # 处理商品列表 + product_list = data.get("product_list", []) + processed_products = [] + for product in product_list: + processed_products.append({ + "id": product.get("id", 0), + "name": product.get("name", ""), + "img": product.get("img", ""), + "tag": product.get("tag", []), + "category_name": product.get("category_name", ""), + "sell_num": product.get("sell_num", 0), + "gray_reason": product.get("gray_reason", []), + "square_image_list": product.get("square_image_list", []), + "channel_id": product.get("channel_id"), + "channel_type": product.get("channel_type") + }) + + # 处理分页信息 + page_info = data.get("page_info", {}) + processed_page_info = { + "cursor": page_info.get("cursor", 0), + "has_more": page_info.get("has_more", False) + } + + return { + "product_list": processed_products, + "page_info": processed_page_info + } + + +def _process_video_data(data: Dict) -> Dict: + """ + 解析视频列表数据 + 返回结构: + { + "list": [{ + "id": str, + "size": int, + "width": int, + "height": int, + "url": str, + "signature": str, + "poster_url": str, + "bit_rate": int, + "duration": float, + "material_id": int, + "filename": str, + "image_mode": str, + "tags": List[str], + "source": str, + "create_time": str, + "format": str, + "is_ai_create": bool + }], + "page_info": { + "page": int, + "page_size": int, + "total_page": int, + "total_number": int + } + } + """ + # 处理视频列表 + video_list = data.get("list", []) + processed_videos = [] + for video in video_list: + processed_videos.append({ + "id": video.get("id", ""), + "size": video.get("size", 0), + "width": video.get("width", 0), + "height": video.get("height", 0), + "url": video.get("url", ""), + "signature": video.get("signature", ""), + "poster_url": video.get("poster_url", ""), + "bit_rate": video.get("bit_rate", 0), + "duration": video.get("duration", 0.0), + "material_id": video.get("material_id", 0), + "filename": video.get("filename", ""), + "image_mode": video.get("image_mode", ""), + "tags": video.get("tags", []), + "source": video.get("source", ""), + "create_time": video.get("create_time", ""), + "format": video.get("format", ""), + "is_ai_create": video.get("is_ai_create", False) + }) + + # 处理分页信息 + page_info = data.get("page_info", {}) + processed_page_info = { + "page": page_info.get("page", 1), + "page_size": page_info.get("page_size", 100), + "total_page": page_info.get("total_page", 1), + "total_number": page_info.get("total_number", 0) + } + + return { + "list": processed_videos, + "page_info": processed_page_info + } + + +def _process_image_data(data: Dict) -> Dict: + """ + 解析图片列表数据 + 返回结构: + { + "list": [{ + "id": str, + "size": int, + "width": int, + "height": int, + "url": str, + "signature": str, + "material_id": int, + "filename": str, + "image_mode": str, + "tag": List[str], + "create_time": str, + "source": str, + "is_ai_create": bool + }], + "page_info": { + "page": int, + "page_size": int, + "total_page": int, + "total_number": int + } + } + """ + # 处理图片列表 + image_list = data.get("list", []) + processed_images = [] + for image in image_list: + processed_images.append({ + "id": image.get("id", ""), + "size": image.get("size", 0), + "width": image.get("width", 0), + "height": image.get("height", 0), + "url": image.get("url", ""), + "signature": image.get("signature", ""), + "material_id": image.get("material_id", 0), + "filename": image.get("filename", ""), + "image_mode": image.get("image_mode", ""), + "tag": image.get("tag", []), + "create_time": image.get("create_time", ""), + "source": image.get("source", ""), + "is_ai_create": image.get("is_ai_create", False) + }) + + # 处理分页信息 + page_info = data.get("page_info", {}) + processed_page_info = { + "page": page_info.get("page", 1), + "page_size": page_info.get("page_size", 100), + "total_page": page_info.get("total_page", 1), + "total_number": page_info.get("total_number", 0) + } + + return { + "list": processed_images, + "page_info": processed_page_info + } diff --git a/backend/app/apis/ocean/report.py b/backend/app/apis/ocean/report.py new file mode 100644 index 0000000000000000000000000000000000000000..cf8a1d298232299398ea80d2f313803f3c2e49d6 --- /dev/null +++ b/backend/app/apis/ocean/report.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- + +from fastapi import APIRouter, Request +from app.apis.ocean.report_result_data import _process_advertiser_report_data +from app.schemas.ocean.report import GetAdvertiserDataParam +from app.apis.ocean.base_controller import BaseController +from app.services.ocean.report import ReportService + +router = APIRouter() + + +@router.post("/advertiser_data/get/") +async def get_advertiser_report_data(param: GetAdvertiserDataParam, request: Request): + """ + 获取全域推广账户维度数据 + https://open.oceanengine.com/labels/12/docs/1770675169146947?origin=left_nav + """ + return await BaseController.handle_api_request( + request=request, + service_call=lambda token: ReportService.get_advertiser_report_data(param, token), + data_processor=_process_advertiser_report_data + ) diff --git a/backend/app/apis/ocean/report_result_data.py b/backend/app/apis/ocean/report_result_data.py new file mode 100644 index 0000000000000000000000000000000000000000..3c91a5bb8497669d0f98720c23ec1327b4ac816a --- /dev/null +++ b/backend/app/apis/ocean/report_result_data.py @@ -0,0 +1,39 @@ +from typing import Dict +import math + + +def _process_advertiser_report_data(data: Dict) -> Dict: + """ + 处理广告主报表数据 + 返回结构: + { + "stat_cost": float, + "total_prepay_and_pay_order_roi2": float, + "total_pay_order_gmv_for_roi2": float, + "total_pay_order_count_for_roi2": float, + "total_cost_per_pay_order_for_roi2": float, + "total_prepay_order_count_for_roi2": float, + "total_prepay_order_gmv_for_roi2": float, + "total_unfinished_estimate_order_gmv_for_roi2": float, + "total_pay_order_coupon_amount_for_roi2": float, + "total_ecom_platform_subsidy_amount_for_roi2": float, + "total_pay_order_gmv_include_coupon_for_roi2": float + } + """ + return { + "stat_cost": round(float(data.get("stat_cost", 0.0)), 2), + "total_prepay_and_pay_order_roi2": round(float(data.get("total_prepay_and_pay_order_roi2", 0.0)), 2), + "total_pay_order_gmv_for_roi2": round(float(data.get("total_pay_order_gmv_for_roi2", 0.0)), 2), + "total_pay_order_count_for_roi2": float(data.get("total_pay_order_count_for_roi2", 0.0)), + "total_cost_per_pay_order_for_roi2": round(float(data.get("total_cost_per_pay_order_for_roi2", 0.0)), 2), + "total_prepay_order_count_for_roi2": float(data.get("total_prepay_order_count_for_roi2", 0.0)), + "total_prepay_order_gmv_for_roi2": round(float(data.get("total_prepay_order_gmv_for_roi2", 0.0)), 2), + "total_unfinished_estimate_order_gmv_for_roi2": round( + float(data.get("total_unfinished_estimate_order_gmv_for_roi2", 0.0)), 2), + "total_pay_order_coupon_amount_for_roi2": round(float(data.get("total_pay_order_coupon_amount_for_roi2", 0.0)), + 2), + "total_ecom_platform_subsidy_amount_for_roi2": round( + float(data.get("total_ecom_platform_subsidy_amount_for_roi2", 0.0)), 2), + "total_pay_order_gmv_include_coupon_for_roi2": round( + float(data.get("total_pay_order_gmv_include_coupon_for_roi2", 0.0)), 2) + } diff --git a/backend/app/apis/system/user.py b/backend/app/apis/system/user.py index 64334ad9275cb4908154beed22a55b46cce13c98..050e4238e25b627ac44bd9611f14599266f3cb30 100644 --- a/backend/app/apis/system/user.py +++ b/backend/app/apis/system/user.py @@ -1,13 +1,15 @@ # -*- coding: utf-8 -*- # @author: xiaobai -from fastapi import APIRouter, Request - +from fastapi import APIRouter, Request, Header +from app.corelibs.logger import logger from app.corelibs import g from app.corelibs.codes import CodeEnum from app.corelibs.http_response import partner_success -from app.schemas.system.user import UserLogin, UserQuery, UserIn, UserResetPwd, UserDel +from app.schemas.system.user import UserLogin, UserQuery, UserIn, UserResetPwd, UserDel, AuthInfo from app.services.system.user import UserService +from app.utils.auth_tool import get_auth_url, handle_callback +from fastapi.responses import RedirectResponse router = APIRouter() @@ -51,7 +53,7 @@ async def get_user_info(request: Request): return partner_success(user_info) -@router.post('/userRegister', description="新增用户") +@router.post('/register', description="新增用户") async def user_register(user_info: UserIn): data = await UserService.user_register(user_info) return partner_success(data) @@ -81,3 +83,17 @@ async def authorize_token(request: Request): async def get_menu_by_token(): user_info = await UserService.get_menu_by_token(g.token) return partner_success(user_info) + + +@router.post('/auth', description="授权操作") +async def start_auth(auth_info: AuthInfo, token: str = Header(...)): + auth_data = await UserService.start_auth(auth_info, token) + return partner_success({"url": auth_data}) + + +@router.get("/auth_callback", response_model=None) +async def oauth_callback(app_id: str, auth_code: str, state: str): + handle_success = await UserService.auth_handle_callback(app_id, auth_code, state) + # 重定向到应用首页 + frontend_home_url = f"http://localhost:8893/#/home?auth_success={handle_success}" + return RedirectResponse(url=frontend_home_url) diff --git a/backend/app/db/redis.py b/backend/app/db/redis.py index 729189674b78c925109bbfd8e32ea34c1fb1a81b..c77889d16ae3cc03c1a089b5fbf2e3cd7f7b4781 100644 --- a/backend/app/db/redis.py +++ b/backend/app/db/redis.py @@ -4,13 +4,15 @@ import json import typing -from aioredis import Redis, DataError +from redis import asyncio as aioredis # 统一使用 redis 库的异步接口 +from redis.exceptions import DataError # 从 redis 库导入异常类 + from redis.typing import KeyT, FieldT, EncodableT, AnyFieldT from config import config -class MyRedis(Redis): +class MyRedis(aioredis.Redis): """ 继承Redis,并添加自己的方法 """ # 写 __init__ 的话就取消下面注释 diff --git a/backend/app/exceptions/exceptions.py b/backend/app/exceptions/exceptions.py index 62e2bd3de2fd3cd127c6d19594c7be21f29f88eb..6f67c52949be3526658fb2141a7c97a47910293c 100644 --- a/backend/app/exceptions/exceptions.py +++ b/backend/app/exceptions/exceptions.py @@ -78,3 +78,12 @@ class ParameterError(MyBaseException): def __init__(self, err_code: typing.Union[CodeEnum, str]): super(ParameterError, self).__init__(err_code) + +class ThirdPartyAPIError(Exception): + """第三方API异常""" + + def __init__(self, message: str, code: int = 400, request_id: str = None): + self.message = message + self.code = code + self.request_id = request_id + super().__init__(message) diff --git a/backend/app/models/system_models.py b/backend/app/models/system_models.py index 93f1108b1facdcb12cf867039a27b8d4db843905..bb0530a87bead4b0eacca5c2cf184063791778f9 100644 --- a/backend/app/models/system_models.py +++ b/backend/app/models/system_models.py @@ -26,6 +26,9 @@ class User(Base): remarks = Column(String(255), nullable=False, comment='用户描述') avatar = Column(Text, nullable=False, comment='头像') tags = Column(JSON, nullable=False, comment='标签') + auth_token = Column(Text, nullable=True, comment='抖音授权token信息') + app_id = Column(Text, nullable=True, comment='抖音后台appId') + secret = Column(Text, nullable=True, comment='抖音后台secrets') @classmethod async def get_list(cls, params: UserQuery): @@ -344,4 +347,3 @@ class FileInfo(Base): original_name = Column(String(255), nullable=True, comment='原名称') content_type = Column(String(255), nullable=True, comment='文件类型') file_size = Column(String(255), nullable=True, comment='文件大小') - diff --git a/backend/app/schemas/base.py b/backend/app/schemas/base.py index 9ddc892eaed43fd99575c9c649f9fa81a2fd3c29..85ab5dfd4e604fe066296a22c5c1837ca623fafd 100644 --- a/backend/app/schemas/base.py +++ b/backend/app/schemas/base.py @@ -1,6 +1,25 @@ # -*- coding: utf-8 -*- -# @author: xiaobai -from pydantic import BaseModel, validator +from pydantic import BaseModel, validator, Field +from typing import List, Optional + + +class PageParam(BaseModel): + page: int = Field(default=1, description='页码') + page_size: int = Field(default=100, description='每页数量 默认值:100,最大值:100') + + +class PageInfo(BaseModel): + page: int + page_size: int + total_number: int + total_page: int + + +class ThirdPartyResponse(BaseModel): + code: int + message: str + request_id: str + data: Optional[dict] class BaseSchema(BaseModel): diff --git a/backend/app/schemas/ocean/__init__.py b/backend/app/schemas/ocean/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/backend/app/schemas/ocean/account.py b/backend/app/schemas/ocean/account.py new file mode 100644 index 0000000000000000000000000000000000000000..7998f0af67b03bfc61d552b42857c1038136d8f2 --- /dev/null +++ b/backend/app/schemas/ocean/account.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +import typing + +from pydantic import BaseModel, Field +from app.schemas.base import BaseSchema, PageParam +from app.utils.des import decrypt_rsa_password +from pydantic import BaseModel +from typing import List, Optional, Dict + + +class GetAdvertiserListParam(BaseModel): + cc_account_id: str = Field(..., description='纵横账户ID,默认为实例的enterprise_id 企业号id') + page: PageParam = Field(..., description='查询翻页') + account_source: str = Field(default="DEFAULT_ACCOUNT_SOURCE", description='账户来源') + + +class AdvertiserItem(BaseModel): + advertiser_id: int + advertiser_name: str + advertiser_type: str + + +class AuthedAdvertiserItem(BaseModel): + advertiser_id: int + advertiser_name: str + is_valid: bool + account_role: str + + +class GetAuthedAwemeListParam(BaseModel): + advertiser_id: int = Field(..., description='广告主id') + page: PageParam = Field(..., description='查询翻页') + + +class GetAccountBalanceParam(BaseModel): + """获取账户余额请求参数""" + advertiser_id: int = Field(..., description="广告主id") diff --git a/backend/app/schemas/ocean/product.py b/backend/app/schemas/ocean/product.py new file mode 100644 index 0000000000000000000000000000000000000000..6f961060553d061e1daa9df49998e5cb658803b5 --- /dev/null +++ b/backend/app/schemas/ocean/product.py @@ -0,0 +1,158 @@ +from pydantic import Field, conlist +from pydantic import BaseModel +from typing import List, Optional, Literal + + +class ProductFilter(BaseModel): + """商品查询过滤器""" + tab: str = Field( + default="ALL", + description="商品分类标签 ALL, BREAKTHROUGH_PRODUCT, GOOD_BOOST, NEW_PRODUCT, SEARCH_TREND" + ) + create_roi2_limit_product: bool = Field( + None, + description="是否只看未投放商品(仅ALL/GOOD_BOOST有效)" + ) + product_name: str = Field( + None, + description="商品名称搜索(1-50字符)" + ) + product_ids: list[int] = Field( + None, + description="商品ID列表筛选(1-50个)" + ) + order_field: str = Field( + None, + description="排序字段(仅ALL/GOOD_BOOST有效) SELL_NUM, STOCK, AUDIT_TIME" + ) + order_type: str = Field( + None, + description="排序方式(仅ALL/GOOD_BOOST有效) ASC, DESC" + ) + platform: str = Field( + None, + description="下单平台 ECP_AWEME, QIANCHUAN" + ) + + +class ProductPagination(BaseModel): + """分页参数""" + page: int = 1 + page_size: int = 100 + cursor: int = Field( + None, + description="游标值(ALL/GOOD_BOOST/SEARCH_TREND无效)" + ) + + +class VideoFilter(BaseModel): + """ + 视频素材过滤条件 + 注意:video_ids、material_ids、signatures三个参数互斥,只能选择其中一个过滤条件 + """ + video_ids: list[str] = Field( + None, + description="视频ID列表,示例: ['86adb23eaa21229fc04ef932b5089bb8'],数量限制:1-100" + ) + + material_ids: list[str] = Field( + None, + description="素材ID列表,数量限制:1-100" + ) + + signatures: list[str] = Field( + None, + description="素材MD5值列表,数量限制:1-100" + ) + + image_mode: list[str] = Field( + None, + description="素材类型枚举值 VIDEO, IMAGE, CAROUSEL" + ) + + tags: list[str] = Field( + None, + description="素材标签列表" + ) + + sources: list[str] = Field( + None, + description="素材来源枚举 ARTHUR, BP, CREATIVE_CENTER, E_COMMERCE, IVE_HIGHLIGHT, STAR, TADA, VIDEO_CAPTURE, AGENT" + ) + + start_time: str = Field( + None, + description="视频上传时间过滤起始时间,格式:yyyy-mm-dd" + ) + + end_time: str = Field( + None, + description="视频上传时间过滤截止时间,格式:yyyy-mm-dd" + ) + + +class ImageFilter(BaseModel): + """ + 图片素材过滤条件 + 注意:image_ids、material_ids、signatures三个参数互斥,只能选择其中一个过滤条件 + """ + image_ids: list[str] = Field( + None, + description="图片ID列表(创意中使用的图片key),数量限制:1-100" + ) + + material_ids: list[str] = Field( + None, + description="素材ID列表(素材报表使用的唯一ID),数量限制:1-100" + ) + + signatures: list[str] = Field( + None, + description="素材MD5值列表,数量限制:1-100" + ) + + image_mode: list[str] = Field( + None, + description="素材类型枚举值" + ) + + tags: list[str] = Field( + None, + description="素材标签列表" + ) + + sources: list[str] = Field( + None, + description="素材来源:巨量创意(CREATIVE_CENTER)/本地上传(E_COMMERCE)" + ) + + start_time: str = Field( + None, + description="图片上传时间过滤起始时间,格式:yyyy-mm-dd" + ) + + end_time: str = Field( + None, + description="图片上传时间过滤截止时间,格式:yyyy-mm-dd" + ) + + +class GetProductListParam(BaseModel): + """获取商品列表请求参数""" + advertiser_id: int = Field(..., gt=0, description="千川广告主ID") + aweme_id: int = Field(..., gt=0, description="抖音号ID") + filtering: ProductFilter = Field(..., description="筛选条件") + + +class GetVideoListParam(BaseModel): + advertiser_id: int = Field(..., gt=0, description="千川广告主ID") + filtering: VideoFilter = Field(None, description="筛选条件") + page: int = 1 # 明确标注为 int 类型 + page_size: int = 100 # 明确标注为 int 类型 + + +class GetImageListParam(BaseModel): + advertiser_id: int = Field(..., gt=0, description="千川广告主ID") + filtering: ImageFilter = Field(None, description="筛选条件") + page: int = 1 + page_size: int = 100 diff --git a/backend/app/schemas/ocean/report.py b/backend/app/schemas/ocean/report.py new file mode 100644 index 0000000000000000000000000000000000000000..20ed66ca00f3bc2317a8d3a5879cf7f7061132ba --- /dev/null +++ b/backend/app/schemas/ocean/report.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +import typing + +from pydantic import BaseModel, Field +from app.schemas.base import BaseSchema, PageParam +from app.utils.des import decrypt_rsa_password +from pydantic import BaseModel +from typing import List, Optional, Dict + + +class GetAdvertiserDataParam(BaseModel): + """获取全域推广账户维度数据请求参数""" + advertiser_id: int = Field(..., description="广告主id") + start_date: str = Field(..., description="开始时间,格式 2021-04-05 00:00:00") + end_date: str = Field(..., description="结束时间,格式 2021-04-05 23:59:59") + marketing_goal: str = Field(..., description="营销目标 ALL LIVE_PROM_GOODS VIDEO_PROM_GOODS") + + order_platform: str = Field(..., description="下单平台 ALL QIANCHUAN ECP_AWEME") diff --git a/backend/app/schemas/system/user.py b/backend/app/schemas/system/user.py index dccc916207f7eb9ea3314afd435eaf90507311ae..de40209365cd332741eeae4ce72140c74326132b 100644 --- a/backend/app/schemas/system/user.py +++ b/backend/app/schemas/system/user.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- -# @author: xiaobai import typing from pydantic import BaseModel, Field @@ -79,3 +78,8 @@ class UserLoginRecordQuery(BaseModel): ret_code: str = Field(None, description="返回code") address: str = Field(None, description="地址") source_type: str = Field(None, description="来源") + + +class AuthInfo(BaseModel): + appId: str = Field(..., description='appId') + secret: str = Field(..., description='secret') diff --git a/backend/app/services/ocean/__init__.py b/backend/app/services/ocean/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/backend/app/services/ocean/account.py b/backend/app/services/ocean/account.py new file mode 100644 index 0000000000000000000000000000000000000000..367877276a468807b49066d9a4c66d4716629825 --- /dev/null +++ b/backend/app/services/ocean/account.py @@ -0,0 +1,83 @@ +from typing import Dict +from app.schemas.ocean.account import GetAdvertiserListParam, GetAuthedAwemeListParam, GetAccountBalanceParam +from app.services.ocean.token_manager import TokenManager +from .base_service import BaseAPIService + + +class AccountService(BaseAPIService): + + @staticmethod + async def get_advertiser_list(param: GetAdvertiserListParam, token: str) -> Dict: + """ + 获取广告主列表 + https://open.oceanengine.com/labels/12/docs/1796368918556803?origin=left_nav + """ + access_token = await TokenManager.get_access_token(token) + + return await AccountService._make_api_request( + url="https://ad.oceanengine.com/open_api/2/customer_center/advertiser/list/", + method="GET", + headers={ + "Content-Type": "application/json", + "Access-Token": access_token + }, + params={ + "cc_account_id": param.cc_account_id, + "account_source": param.account_source, + "page": param.page.page, + "page_size": param.page.page_size + } + ) + + @staticmethod + async def get_authed_advertisers(token: str) -> Dict: + """ + 获取授权账号列表 + https://open.oceanengine.com/labels/12/docs/1697467748096067?origin=left_nav + """ + access_token = await TokenManager.get_access_token(token) + return await AccountService._make_api_request( + url="https://ad.oceanengine.com/open_api/oauth2/advertiser/get/", + method="GET", + headers={ + "Content-Type": "application/json", + "Access-Token": access_token + }, + params={"access_token": access_token} + ) + + @staticmethod + async def get_aweme_authed_list(param: GetAuthedAwemeListParam, token: str) -> Dict: + """ + 获取千川账户下可投广抖音号 + https://open.oceanengine.com/labels/12/docs/1697467726080011?origin=left_nav + """ + + access_token = await TokenManager.get_access_token(token) + return await AccountService._make_api_request( + url="https://ad.oceanengine.com/open_api/v1.0/qianchuan/aweme/authorized/get/", + method="GET", + headers={ + "Content-Type": "application/json", + "Access-Token": access_token + }, + params={"advertiser_id": param.advertiser_id, "page": param.page.page, "page_size": param.page.page_size} + ) + + @staticmethod + async def get_account_balance(param: GetAccountBalanceParam, token: str) -> Dict: + """ + 获取账户余额 + https://open.oceanengine.com/labels/12/docs/1783322092364800?origin=left_nav + """ + + access_token = await TokenManager.get_access_token(token) + return await AccountService._make_api_request( + url="https://api.oceanengine.com/open_api/v1.0/qianchuan/account/balance/get/", + method="GET", + headers={ + "Content-Type": "application/json", + "Access-Token": access_token + }, + params={"advertiser_id": param.advertiser_id} + ) diff --git a/backend/app/services/ocean/base_service.py b/backend/app/services/ocean/base_service.py new file mode 100644 index 0000000000000000000000000000000000000000..6966eff8535ddac351bdc1463eaf5e8a3a629e5c --- /dev/null +++ b/backend/app/services/ocean/base_service.py @@ -0,0 +1,57 @@ +from typing import Dict, Optional +import requests +from loguru import logger +from app.exceptions.exceptions import ThirdPartyAPIError +from app.schemas.base import ThirdPartyResponse + + +class BaseAPIService: + @staticmethod + async def _make_api_request( + url: str, + method: str = "GET", + headers: Optional[Dict] = None, + params: Optional[Dict] = None, + data: Optional[Dict] = None + ) -> Dict: + """ + 封装基础API请求逻辑 + :return: { + "success": bool, + "data": dict, + "request_id": Optional[str] + } + """ + try: + response = requests.request( + method=method, + url=url, + headers=headers or {}, + params=params, + json=data + ) + logger.info(f"response==== {response}") + response.raise_for_status() + + api_response = ThirdPartyResponse(**response.json()) + logger.info(f"api_response.code==== {api_response.code}") + if api_response.code != 0: + raise ThirdPartyAPIError( + api_response.message, + code=api_response.code, + request_id=api_response.request_id + ) + + logger.info(f"api_response==== {api_response.data}") + return { + "success": True, + "data": api_response.data, + "request_id": api_response.request_id + } + + except requests.exceptions.RequestException as e: + logger.error(f"API请求失败: {str(e)}") + raise ThirdPartyAPIError(f"网络请求失败: {str(e)}", code=500) + except Exception as e: + logger.error(f"API数据处理失败: {str(e)}") + raise ThirdPartyAPIError(f"数据处理失败: {str(e)}", code=500) diff --git a/backend/app/services/ocean/product.py b/backend/app/services/ocean/product.py new file mode 100644 index 0000000000000000000000000000000000000000..ecc879347b4103f3f47b368a77132e450869ff68 --- /dev/null +++ b/backend/app/services/ocean/product.py @@ -0,0 +1,98 @@ +# coding=utf-8 +from pycparser.ply.yacc import string_types + +from typing import Dict +from app.services.ocean.token_manager import TokenManager +from .base_service import BaseAPIService +import json + +from ...schemas.ocean.product import GetProductListParam, GetVideoListParam, GetImageListParam + + +class ProductService(BaseAPIService): + """商品数据服务""" + + @staticmethod + async def get_product_list(param: GetProductListParam, token: str): + access_token = await TokenManager.get_access_token(token) + filtering_dict = param.filtering.dict(exclude_none=True) + params = { + "advertiser_id": param.advertiser_id, + "aweme_id": param.aweme_id, + "filtering": json.dumps(filtering_dict, ensure_ascii=False) # 序列化为 JSON 字符串 + } + # todo 有两个 现在应该都是全域达人 + # https://api.oceanengine.com/open_api/v1.0/qianchuan/uni_promotion/product/aweme/get/ 全域达人 + # https://api.oceanengine.com/open_api/v1.0/qianchuan/uni_promotion/product/get/ 全域商家 + + return await ProductService._make_api_request( + url="https://api.oceanengine.com/open_api/v1.0/qianchuan/uni_promotion/product/aweme/get/", + method="GET", + headers={ + "Content-Type": "application/json", + "Access-Token": access_token + }, + params=params + ) + + @staticmethod + async def get_video_list(param: GetVideoListParam, token: str): + """ + 获取视频素材 + https://open.oceanengine.com/labels/12/docs/1739309912219663 + """ + access_token = await TokenManager.get_access_token(token) + filtering_dict = ( + param.filtering.dict(exclude_none=True) + if param.filtering is not None + else {} + ) + + params = { + "advertiser_id": param.advertiser_id, + "page": param.page, + "page_size": param.page_size + } + if filtering_dict: + params["filtering"] = json.dumps(filtering_dict, ensure_ascii=False) + + return await ProductService._make_api_request( + url="https://ad.oceanengine.com/open_api/v1.0/qianchuan/video/get/", + method="GET", + headers={ + "Content-Type": "application/json", + "Access-Token": access_token + }, + params=params + ) + + @staticmethod + async def get_image_list(param: GetImageListParam, token: str): + """ + 获取图片素材 + https://open.oceanengine.com/labels/12/docs/1739304248623182?origin=left_nav + """ + access_token = await TokenManager.get_access_token(token) + filtering_dict = ( + param.filtering.dict(exclude_none=True) + if param.filtering is not None + else {} + ) + + params = { + "advertiser_id": param.advertiser_id, + "page": param.page, + "page_size": param.page_size + } + if filtering_dict: + params["filtering"] = json.dumps(filtering_dict, ensure_ascii=False) + + return await ProductService._make_api_request( + url="https://ad.oceanengine.com/open_api/v1.0/qianchuan/image/get/", + method="GET", + headers={ + "Content-Type": "application/json", + "Access-Token": access_token + }, + params=params + ) diff --git a/backend/app/services/ocean/report.py b/backend/app/services/ocean/report.py new file mode 100644 index 0000000000000000000000000000000000000000..082373406269956ab43cb86edb13ff4909a7d673 --- /dev/null +++ b/backend/app/services/ocean/report.py @@ -0,0 +1,38 @@ +from typing import Dict +from app.schemas.ocean.account import GetAdvertiserListParam, GetAuthedAwemeListParam +from app.services.ocean.token_manager import TokenManager +from .base_service import BaseAPIService +import json + +from ...schemas.ocean.report import GetAdvertiserDataParam + + +class ReportService(BaseAPIService): + + @staticmethod + async def get_advertiser_report_data(param: GetAdvertiserDataParam, token: str) -> Dict: + access_token = await TokenManager.get_access_token(token) + filtering_dict = ["stat_cost", "total_prepay_and_pay_order_roi2", "total_pay_order_gmv_for_roi2", + "total_pay_order_count_for_roi2", "total_cost_per_pay_order_for_roi2", + "total_prepay_order_count_for_roi2", "total_prepay_order_gmv_for_roi2", + "total_unfinished_estimate_order_gmv_for_roi2", "total_pay_order_coupon_amount_for_roi2", + "total_ecom_platform_subsidy_amount_for_roi2", "total_pay_order_gmv_include_coupon_for_roi2"] + + params = { + "advertiser_id": param.advertiser_id, + "start_date": param.start_date, + "end_date": param.end_date, + "marketing_goal": param.marketing_goal, + "order_platform": param.order_platform, + "filtering": json.dumps(filtering_dict, ensure_ascii=False), + } + + return await ReportService._make_api_request( + url="https://api.oceanengine.com/open_api/v1.0/qianchuan/report/uni_promotion/get/", + method="GET", + headers={ + "Content-Type": "application/json", + "Access-Token": access_token + }, + params=params + ) diff --git a/backend/app/services/ocean/token_manager.py b/backend/app/services/ocean/token_manager.py new file mode 100644 index 0000000000000000000000000000000000000000..5fcb643540a5e405d3356b7c6f49d017be73707b --- /dev/null +++ b/backend/app/services/ocean/token_manager.py @@ -0,0 +1,94 @@ +import time + +import requests +from typing import Dict, Optional +from loguru import logger +from app.utils.current_user import current_user +from app.corelibs import g +from app.corelibs.consts import TEST_USER_INFO, CACHE_DAY +from app.models.system_models import User +import json + +token_refresh_buffer_time = 300 # 提前5分钟刷新避免过期 + + +class TokenManager: + @staticmethod + async def update_tokens(token_response: Dict[str, str], token: str): + """更新并持久化令牌数据""" + # 计算精确过期时间点 + logger.info(f"update token data:{token_response}") + current_time = time.time() + token_response['access_token_expires_at'] = current_time + token_response['expires_in'] + token_response['refresh_token_expires_at'] = current_time + token_response['refresh_token_expires_in'] + + user_info = await g.redis.get(TEST_USER_INFO.format(token)) + user_info["auth_token"] = token_response + await g.redis.set(TEST_USER_INFO.format(token), user_info, CACHE_DAY) + + # 授权完成后 存储 数据到user表中 + param = { + "id": user_info["id"], + "auth_token": json.dumps(token_response), + } + await User.create_or_update(param) + + logger.info(f"save token: {token_response} success") + + @staticmethod + def is_token_expired(token_data: Dict, token_type: str = "access") -> bool: + """检测令牌是否过期""" + key = f"{token_type}_token_expires_at" + return time.time() >= (token_data.get(key, 0) - token_refresh_buffer_time) + + @staticmethod + async def refresh_token(token_data: Dict, token=None) -> Dict: + """使用刷新令牌获取新访问令牌""" + current_user_info = await current_user(token) + app_id = current_user_info.get('app_id') + secret = current_user_info.get('secret') + payload = { + "app_id": app_id, + "secret": secret, + "grant_type": "refresh_token", + "refresh_token": token_data["refresh_token"] + } + logger.info(f"need refresh token") + refresh_ulr = "https://ad.oceanengine.com/open_api/oauth2/refresh_token/" + response = requests.post(refresh_ulr, json=payload) + if response.status_code != 200: + logger.error(f"Token refresh failed: {response.text}") + return {} + + logger.info(f"need refresh token") + new_tokens = response.json()['data'] + + await TokenManager.update_tokens(new_tokens, token) + logger.info(f"refresh token finish") + return new_tokens + + @staticmethod + async def get_access_token(token: str) -> str | None: + """获取有效访问令牌(自动刷新过期令牌)""" + current_user_info = await current_user(token) + if not current_user_info: + logger.error(f"用户信息已过期") + return None + + token_data = current_user_info.get('auth_token') + logger.info(f"get_access_token ==== token_data:{token_data}") + if not token_data: + logger.error(f"token data is null") + return None + + # 检查并刷新过期令牌 + if TokenManager.is_token_expired(token_data, token_type="access"): + if TokenManager.is_token_expired(token_data, token_type="refresh"): + logger.error(f"Refresh token expired - reauthentication required") + # 返回空就认为是过期要重新授权 + return None + token_data = TokenManager.refresh_token(token_data, token) + if not token_data: + return None + + return token_data['access_token'] diff --git a/backend/app/services/system/user.py b/backend/app/services/system/user.py index 2a1b581f5424086ea734797df79e145b2fa846df..4dfd30b80b74f0e5a14db8a9b7614408e36e1acc 100644 --- a/backend/app/services/system/user.py +++ b/backend/app/services/system/user.py @@ -2,18 +2,21 @@ import typing from datetime import datetime import traceback import uuid - +import base64 from app.corelibs import g from loguru import logger from app.corelibs.codes import CodeEnum from app.corelibs.consts import TEST_USER_INFO, CACHE_DAY from app.models.system_models import User, Menu, Roles, UserLoginRecord from app.schemas.system.user import UserLogin, UserIn, UserResetPwd, UserDel, UserQuery, \ - UserLoginRecordIn, UserLoginRecordQuery + UserLoginRecordIn, UserLoginRecordQuery, AuthInfo +from app.services.ocean.token_manager import TokenManager from app.services.system.menu import MenuService from app.utils.current_user import current_user from app.utils.des import encrypt_rsa_password, decrypt_rsa_password from app.utils.serialize import default_serialize +from app.utils.auth_tool import get_auth_url, handle_callback +import json class UserService: @@ -46,7 +49,8 @@ class UserService: "login_time": login_time, "username": user_info["username"], "roles": roles if roles else [], - "tags": tags if tags else [] + "tags": tags if tags else [], + "auth_token": user_info["auth_token"] } await g.redis.set(TEST_USER_INFO.format(token), token_user_info, CACHE_DAY) logger.info('用户 [{}] 登录了系统'.format(user_info["username"])) @@ -67,6 +71,7 @@ class UserService: await UserService.user_login_record(params) except Exception as err: logger.error(f"登录日志记录错误\n{err}") + logger.info(f"token_user_info {token_user_info}") return token_user_info @staticmethod @@ -193,7 +198,7 @@ class UserService: "nickname": user_info.nickname, "roles": user_info.roles, "tags": user_info.tags, - "login_time": token_user_info.get("login_time", None) + "login_time": token_user_info.get("login_time", None), } @staticmethod @@ -227,6 +232,57 @@ class UserService: result = await UserLoginRecord.create_or_update(params.dict()) return result + @staticmethod + async def auth(auth_info: AuthInfo): + auth_url = get_auth_url(app_id=auth_info.app_id, state=auth_info.app_secret) + return auth_url + + @staticmethod + async def start_auth(authInfo: AuthInfo, token: str): + param = { + "secret": authInfo.secret, + "token": token, + } + state = json.dumps(param) + state_json = json.dumps(param) + # 2. Base64 编码(URL 安全版) + state_encoded = base64.urlsafe_b64encode(state_json.encode('utf-8')).decode('utf-8').rstrip("=") + return get_auth_url(app_id=authInfo.appId, state=state_encoded) + + @staticmethod + async def auth_handle_callback(app_id: str, auth_code: str, state: str) -> int: + # 1. Base64 解码(自动处理 URL 安全字符) + padding = len(state) % 4 + if padding: + state += "=" * (4 - padding) # 补足等号 + state_decoded = base64.urlsafe_b64decode(state).decode('utf-8') + + # 2. 解析 JSON + state_dict = json.loads(state_decoded) + + secret = state_dict.get('secret') + token = state_dict.get('token') + auth_token = handle_callback(app_id, secret, auth_code) + + # 授权完成后 auth_token 数据存到 缓存中 + await TokenManager.update_tokens(auth_token, token) + + user_info = await g.redis.get(TEST_USER_INFO.format(token)) + user_info["app_id"] = app_id + user_info["secret"] = secret + await g.redis.set(TEST_USER_INFO.format(token), user_info, CACHE_DAY) + + # 授权完成后 存储 数据到user表中 + param = { + "id": user_info["id"], + "auth_token": json.dumps(auth_token), + "app_id": app_id, + "secret": secret + } + await User.create_or_update(param) + + return 1 + class LoginRecordService: @staticmethod diff --git a/backend/app/utils/auth_tool.py b/backend/app/utils/auth_tool.py new file mode 100644 index 0000000000000000000000000000000000000000..0c4a2e77fac1d42cf0acb8d2483a49b4e50d0de5 --- /dev/null +++ b/backend/app/utils/auth_tool.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +from config import config +import requests +import random +import string + + +def random_string(length=10): + # 可选字符集:字母+数字(string.ascii_letters + string.digits) + chars = string.ascii_letters + string.digits + return ''.join(random.choices(chars, k=length)) + + +def get_auth_url(scope=None, app_id: str = "", state="custom_state", redirect_uri=config.CALL_BACK_REDIRECT_URI): + base_url = "https://qianchuan.jinritemai.com/openapi/qc/audit/oauth.html" + + params = { + "app_id": app_id, + "redirect_uri": redirect_uri, + "state": state, + "material_auth": 1, + "rid": random_string(), + } + if scope: + params["scope"] = scope + url = f"{base_url}?{'&'.join(f'{k}={v}' for k, v in params.items())}" + return url # 返回结构化数据 + + +def handle_callback(app_id, app_secret, auth_code): + payload = { + "app_id": app_id, + "secret": app_secret, + "grant_type": "auth_code", + "auth_code": auth_code + } + ocean_api_oauth_url = f"https://ad.oceanengine.com/open_api/oauth2/access_token/" + response = requests.post(ocean_api_oauth_url, json=payload) + tokens = response.json()['data'] + return tokens diff --git a/backend/app/utils/common.py b/backend/app/utils/common.py index b91cd04c2c36cb224cde99cbc4e9cf3a20ec7032..a367fcd32489aa0bd612d802dbe904188541629b 100644 --- a/backend/app/utils/common.py +++ b/backend/app/utils/common.py @@ -2,6 +2,8 @@ # @author: xiaobai import uuid - def get_str_uuid(): return str(uuid.uuid4()).replace("-", "") + + + diff --git a/backend/app/utils/request_handler.py b/backend/app/utils/request_handler.py new file mode 100644 index 0000000000000000000000000000000000000000..9504412fc214b2c3f5bacc7dd3742162d83361b4 --- /dev/null +++ b/backend/app/utils/request_handler.py @@ -0,0 +1,165 @@ +import requests +from app.core.logger import logger + +from typing import Dict, Any, Optional, TypeVar, Generic +from dataclasses import dataclass +import json +from requests.models import Response +from requests.exceptions import RequestException + +T = TypeVar('T') # 泛型类型 + + +@dataclass +class APIResponse(Generic[T]): + """标准API响应封装""" + success: bool + code: int + message: str + data: Optional[T] = None + request_id: Optional[str] = None + raw_response: Optional[Response] = None + + +class APIRequest: + def __init__(self, token_manager): + self.token_manager = token_manager + self.headers = { + "Content-Type": "application/json", + "Access-Token": None + } + + def _update_token(self): + """更新请求头中的Access-Token""" + self.headers["Access-Token"] = self.token_manager.get_access_token() + + def _handle_response(self, response: Response) -> APIResponse[Dict[str, Any]]: + """ + 统一处理API响应 + :param response: requests.Response对象 + :return: 标准化的APIResponse + """ + try: + response.raise_for_status() + result = response.json() + + # 解析巨量引擎API标准格式 + return APIResponse( + success=result.get("code", -1) == 0, + code=result.get("code", response.status_code), + message=result.get("message", "Success"), + data=result.get("data"), + request_id=result.get("request_id"), + raw_response=response + ) + + except json.JSONDecodeError: + logger.error(f"JSON解析失败 - URL: {response.url} Status: {response.status_code}") + return APIResponse( + success=False, + code=response.status_code, + message="Invalid JSON response", + raw_response=response + ) + + except RequestException as e: + logger.error(f"请求异常 - {str(e)}") + return APIResponse( + success=False, + code=getattr(e.response, 'status_code', 500), + message=str(e), + raw_response=getattr(e, 'response', None) + ) + + def get(self, url: str, params: Optional[Dict] = None) -> APIResponse: + """封装GET请求""" + self._update_token() + + try: + logger.debug(f"GET >>> {url} Body:\n {params}") + response = requests.get(url, headers=self.headers, params=params) + resp_data = response.json() + logger.debug( + f"GET <<< {url} | Code: {response.status_code} | Res: {resp_data.get('code')} | Data: \n{resp_data.get('data', {})}") + return self._handle_response(response) + + except Exception as e: + logger.error(f"GET请求异常 - {url}") + return APIResponse( + success=False, + code=500, + message=f"Unexpected error: {str(e)}" + ) + + def post(self, url: str, data: Optional[Dict] = None) -> APIResponse: + """封装POST请求""" + self._update_token() + + try: + logger.debug(f"POST >>> {url} Body:\n {json.dumps(data, indent=2, ensure_ascii=False)}") + response = requests.post(url, headers=self.headers, json=data) + resp_data = response.json() + logger.debug( + f"POST <<< {url} | Code: {response.status_code} | Res: {resp_data.get('code')} | Data: \n{resp_data.get('data', {})}") + return self._handle_response(response) + + except Exception as e: + logger.error(f"POST请求异常 - {url}") + return APIResponse( + success=False, + code=500, + message=f"Unexpected error: {str(e)}" + ) + + def put(self, url: str, params: Optional[Dict] = None) -> APIResponse: + """封装PUT请求""" + self._update_token() + + try: + logger.debug(f"PUT >>> {url} \n {json.dumps(params, indent=2, ensure_ascii=False)}") + response = requests.put(url, headers=self.headers, json=params) + resp_data = response.json() + logger.debug( + f"PUT <<< {url} | Code: {response.status_code} | Res: {resp_data.get('code')} | Data: \n{resp_data.get('data', {})}") + return self._handle_response(response) + + except Exception as e: + logger.error(f"PUT请求异常 - {url}") + return APIResponse( + success=False, + code=500, + message=f"Unexpected error: {str(e)}" + ) + + def get_direct(self, url: str, params: Optional[Dict] = None, api_base_url: str = API_BASE_URL): + """封装GET请求""" + self._update_token() + + try: + logger.debug(f"GET >>> {url} Body:\n {params}") + response = requests.get(url, headers=self.headers, params=params) + resp_data = response.json() + logger.debug( + f"GET <<< {url} | Code: {response.status_code} | Res: {resp_data.get('code')} | Data: \n{resp_data.get('data', {})}") + return resp_data + + except Exception as e: + logger.error(f"GET请求异常 - {url}") + return None + + def post_direct(self, url: str, data: Optional[Dict] = None, api_base_url: str = API_BASE_URL): + """封装POST请求""" + self._update_token() + + try: + logger.info(f"POST >>> {url} Body:\n {json.dumps(data, indent=2, ensure_ascii=False)}") + response = requests.post(url, headers=self.headers, json=data) + logger.info(f"POST >>> {url} Response:\n {response}") + resp_data = response.json() + logger.debug( + f"POST <<< {url} | Code: {response.status_code} | Res: {resp_data.get('code')} | Data: \n{resp_data.get('data', {})}") + return resp_data + + except Exception as e: + logger.error(f"POST请求异常 - {url}") + return None diff --git a/backend/config.py b/backend/config.py index 8cb4888ab8e3e738b810b68c29bbf3ce23a3bff9..8943db5f5923eaec5fbddd67d43e4ab88be429cc 100644 --- a/backend/config.py +++ b/backend/config.py @@ -22,19 +22,22 @@ class Configs(BaseSettings): STATIC_DIR: str = 'static' # 静态文件目录 GLOBAL_ENCODING: str = 'utf8' # 全局编码 CORS_ORIGINS: typing.List[typing.Any] = ["*"] # 跨域请求 - WHITE_ROUTER = ["/api/user/login"] # 路由白名单,不需要鉴权 + WHITE_ROUTER = ["/api/user/login", "/api/user/auth_callback"] # 路由白名单,不需要鉴权 SECRET_KEY: str = "kPBDjVk0o3Y1wLxdODxBpjwEjo7-Euegg4kdnzFIRjc" # 密钥(每次重启服务密钥都会改变, token解密失败导致过期, 可设置为常量) ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 1 # token过期时间: 60 minutes * 24 hours * 1 days = 1 days # redis REDIS_URI: str = Field(..., env="REDIS_URI") # redis - + # DATABASE_URI: str = "sqlite+aiosqlite:///./sql_app.db?check_same_thread=False" # Sqlite(异步) DATABASE_URI: str = Field(..., env="MYSQL_DATABASE_URI") # MySQL(异步) # DATABASE_URI: str = "postgresql+asyncpg://postgres:123456@localhost:5432/postgres" # PostgreSQL(异步) DATABASE_ECHO: bool = False # 是否打印数据库日志 (可看到创建表、表数据增删改查的信息) + # CALL_BACK_REDIRECT_URI="127.0.0.1/account/auth_callback" + CALL_BACK_REDIRECT_URI: str = Field(..., env="CALL_BACK_REDIRECT_URI") # 授权回调地址 + # logger LOGGER_DIR: str = "logs" # 日志文件夹名 LOGGER_NAME: str = 'zerorunner.log' # 日志文件名 (时间格式 {time:YYYY-MM-DD_HH-mm-ss}.log) diff --git a/backend/db_script/db_init.sql b/backend/db_script/db_init.sql index eac43f4158928b9ec8e41da8f2fb227e77ef9b6d..8015439e02b112aa7090fe6fcd1cff698244a55c 100644 --- a/backend/db_script/db_init.sql +++ b/backend/db_script/db_init.sql @@ -387,6 +387,9 @@ CREATE TABLE `user` ( `avatar` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '头像数据', `tags` json DEFAULT NULL COMMENT '用户标签', `trace_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT 'trace_id', + `auth_token` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '抖音授权token信息', + `app_id` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '抖音后台appId', + `secret` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '抖音后台secrets', PRIMARY KEY (`id`) USING BTREE, INDEX `ix_user_email`(`email`) USING BTREE, INDEX `ix_user_password`(`password`) USING BTREE, @@ -397,8 +400,8 @@ CREATE TABLE `user` ( -- ---------------------------- -- Records of user -- ---------------------------- -INSERT INTO `user` VALUES (NULL, NULL, 1, 0, '系统', '', NULL, '[1]', '系统', 1, NULL, NULL, 10, NULL, NULL, NULL, NULL); -INSERT INTO `user` VALUES ('2022-03-09 16:03:43', '2023-02-23 14:36:44', 1, 1, 'admin', 'o1qooQ2aDAxzq2r7YAxbk7mNHvyDQ0iyFngiSpp6rkBUwzpCqYwyd4hpXKk8x4ZUKKEKUbCIZSS+1lEnHhOH67COnHszbOq/vWdVGHZOXehYv02yj3jO/q7/Moh9KoLWHSpBJN8MfqdxvdmvowfWzeQz2DbD81BlyKXTSwyYeek=', '12546@qq.com', '[1]', 'admin', 1, 7, 7, 10, '富在术数,不在劳身;利在势居,不在力耕。', 'data:image/jpeg;base64,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', '[\"测试\", \"123\"]', NULL); +INSERT INTO `user` VALUES (NULL, NULL, 1, 0, '系统', '', NULL, '[1]', '系统', 1, NULL, NULL, 10, NULL, NULL, NULL, NULL, NULL, NULL, NULL); +INSERT INTO `user` VALUES ('2022-03-09 16:03:43', '2023-02-23 14:36:44', 1, 1, 'admin', 'o1qooQ2aDAxzq2r7YAxbk7mNHvyDQ0iyFngiSpp6rkBUwzpCqYwyd4hpXKk8x4ZUKKEKUbCIZSS+1lEnHhOH67COnHszbOq/vWdVGHZOXehYv02yj3jO/q7/Moh9KoLWHSpBJN8MfqdxvdmvowfWzeQz2DbD81BlyKXTSwyYeek=', '12546@qq.com', '[1]', 'admin', 1, 7, 7, 10, '富在术数,不在劳身;利在势居,不在力耕。', 'data:image/jpeg;base64,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', '[\"测试\", \"123\"]', NULL,"","",""); -- ---------------------------- -- Table structure for user_login_record diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 6ea63f3f1f316eae2816ddeb38a6a6f898704e59..80793f98d9278deb300da8ceb7883db05096cb86 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { - "name": "vue-next-admin-template", - "version": "2.4.21", + "name": "fast-element-admin", + "version": "2.0.1", "lockfileVersion": 2, "requires": true, "packages": { "": { - "name": "vue-next-admin-template", - "version": "2.4.21", + "name": "fast-element-admin", + "version": "2.0.1", "license": "MIT", "dependencies": { "@element-plus/icons-vue": "^2.0.10", @@ -32,14 +32,21 @@ "@typescript-eslint/parser": "^5.46.0", "@vitejs/plugin-vue": "^4.0.0", "@vue/compiler-sfc": "^3.2.45", - "eslint": "^8.29.0", + "cropperjs": "^1.5.13", + "eslint": "8.22.0", "eslint-plugin-vue": "^9.8.0", + "monaco-editor": "^0.27.0", "prettier": "^2.8.1", "sass": "^1.56.2", + "splitpanes": "^3.1.5", "typescript": "^4.9.4", "vite": "^4.0.0", + "vite-plugin-monaco-editor": "1.0.5", "vite-plugin-vue-setup-extend": "^0.4.0", - "vue-eslint-parser": "^9.1.0" + "vue-eslint-parser": "^9.1.0", + "vuedraggable": "^4.1.0", + "xterm": "^5.1.0", + "xterm-addon-fit": "^0.7.0" }, "engines": { "node": ">=16.0.0", @@ -462,27 +469,27 @@ } }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.7", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.7.tgz", - "integrity": "sha512-kBbPWzN8oVMLb0hOUYXhmxggL/1cJE6ydvjDIGi9EnAGUyA7cLVKQg+d/Dsm+KZwx2czGHrCmMVLiyg8s5JPKw==", + "version": "0.10.7", + "resolved": "https://registry.npmmirror.com/@humanwhocodes/config-array/-/config-array-0.10.7.tgz", + "integrity": "sha512-MDl6D6sBsaV452/QSdX+4CXIjZhIcI0PELsxUjk4U828yd58vk3bTIvk/6w5FY+4hIy9sLW0sfrV7K7Kc++j/w==", + "deprecated": "Use @eslint/config-array instead", "dev": true, + "license": "Apache-2.0", "dependencies": { "@humanwhocodes/object-schema": "^1.2.1", "debug": "^4.1.1", - "minimatch": "^3.0.5" + "minimatch": "^3.0.4" }, "engines": { "node": ">=10.10.0" } }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "node_modules/@humanwhocodes/gitignore-to-minimatch": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/@humanwhocodes/gitignore-to-minimatch/-/gitignore-to-minimatch-1.0.2.tgz", + "integrity": "sha512-rSqmMJDdLFUsyxR6FMtD00nfQKKLFb1kv+qBbOVKqErvloEIJLo5bDTJTQNTYgeyp78JsA7u/NPi5jT1GR/MuA==", "dev": true, - "engines": { - "node": ">=12.22" - }, + "license": "Apache-2.0", "funding": { "type": "github", "url": "https://github.com/sponsors/nzakas" @@ -490,9 +497,11 @@ }, "node_modules/@humanwhocodes/object-schema": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "resolved": "https://registry.npmmirror.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", - "dev": true + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", @@ -1244,6 +1253,13 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "node_modules/cropperjs": { + "version": "1.6.2", + "resolved": "https://registry.npmmirror.com/cropperjs/-/cropperjs-1.6.2.tgz", + "integrity": "sha512-nhymn9GdnV3CqiEHJVai54TULFAE3VshJTXSqSJKa8yXAKyBKDWdhHarnlIPrshJ0WMFTGuFvG02YjLXfPiuOA==", + "dev": true, + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -1429,15 +1445,16 @@ } }, "node_modules/eslint": { - "version": "8.29.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.29.0.tgz", - "integrity": "sha512-isQ4EEiyUjZFbEKvEGJKKGBwXtvXX+zJbkVKCgTuB9t/+jUBcy8avhkEwWJecI15BkRkOYmvIM5ynbhRjEkoeg==", + "version": "8.22.0", + "resolved": "https://registry.npmmirror.com/eslint/-/eslint-8.22.0.tgz", + "integrity": "sha512-ci4t0sz6vSRKdmkOGmprBo6fmI4PrphDFMy5JEq/fNS0gQkJM3rLmrqcp8ipMcdobH3KtUP40KniAE9W19S4wA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, + "license": "MIT", "dependencies": { - "@eslint/eslintrc": "^1.3.3", - "@humanwhocodes/config-array": "^0.11.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", + "@eslint/eslintrc": "^1.3.0", + "@humanwhocodes/config-array": "^0.10.4", + "@humanwhocodes/gitignore-to-minimatch": "^1.0.2", "ajv": "^6.10.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", @@ -1447,21 +1464,21 @@ "eslint-scope": "^7.1.1", "eslint-utils": "^3.0.0", "eslint-visitor-keys": "^3.3.0", - "espree": "^9.4.0", + "espree": "^9.3.3", "esquery": "^1.4.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", "find-up": "^5.0.0", - "glob-parent": "^6.0.2", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^6.0.1", "globals": "^13.15.0", + "globby": "^11.1.0", "grapheme-splitter": "^1.0.4", "ignore": "^5.2.0", "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-sdsl": "^4.1.4", "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", @@ -1472,7 +1489,8 @@ "regexpp": "^3.2.0", "strip-ansi": "^6.0.1", "strip-json-comments": "^3.1.0", - "text-table": "^0.2.0" + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" }, "bin": { "eslint": "bin/eslint.js" @@ -1829,6 +1847,13 @@ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, + "node_modules/functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", + "dev": true, + "license": "MIT" + }, "node_modules/get-intrinsic": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", @@ -2064,15 +2089,6 @@ "node": ">=0.12.0" } }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -2087,16 +2103,6 @@ "node": ">=12" } }, - "node_modules/js-sdsl": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.2.0.tgz", - "integrity": "sha512-dyBIzQBDkCqCu+0upx25Y2jGdbTGxE9fshMsCdK0ViOongpV+n5tXRcZY9v7CaVQ79AGS9KA1KHtojxiM7aXSQ==", - "dev": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/js-sdsl" - } - }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -2258,6 +2264,13 @@ "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.0.tgz", "integrity": "sha512-7dX2/10ITVyqh4aOSVI9gdape+t9l2/8QxHrFmUXu4EEUpdlxl6RudZUPZoc+zuY2hk1j7XxVroIVIan/pD/SQ==" }, + "node_modules/monaco-editor": { + "version": "0.27.0", + "resolved": "https://registry.npmmirror.com/monaco-editor/-/monaco-editor-0.27.0.tgz", + "integrity": "sha512-UhwP78Wb8w0ZSYoKXQNTV/0CHObp6NS3nCt51QfKE6sKyBo5PBsvuDOHoI2ooBakc6uIwByRLHVeT7+yXQe2fQ==", + "dev": true, + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -2848,6 +2861,19 @@ "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", "deprecated": "Please use @jridgewell/sourcemap-codec instead" }, + "node_modules/splitpanes": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/splitpanes/-/splitpanes-3.2.0.tgz", + "integrity": "sha512-K+WKxWdqtKShV33gPjQl769wHxB3glypTOReCvYu/AJd38J+abHlpiF8rK6uBNPMrgw5thHZCI5JkEwsAqa9XA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antoniandre" + }, + "peerDependencies": { + "vue": "^3.2.0" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -2997,6 +3023,13 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true }, + "node_modules/v8-compile-cache": { + "version": "2.4.0", + "resolved": "https://registry.npmmirror.com/v8-compile-cache/-/v8-compile-cache-2.4.0.tgz", + "integrity": "sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw==", + "dev": true, + "license": "MIT" + }, "node_modules/vite": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/vite/-/vite-4.0.0.tgz", @@ -3046,6 +3079,16 @@ } } }, + "node_modules/vite-plugin-monaco-editor": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/vite-plugin-monaco-editor/-/vite-plugin-monaco-editor-1.0.5.tgz", + "integrity": "sha512-vstgFhRMfLfkE5yGQ0cHO3UaW7+eqtOKsBEWLL6ZNmCNwgBxsaB1otWGiJEoHgc9gVoBdeKOOjgW1pACTS1gVQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "monaco-editor": "0.25.x || 0.26.x || 0.27.x" + } + }, "node_modules/vite-plugin-vue-setup-extend": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/vite-plugin-vue-setup-extend/-/vite-plugin-vue-setup-extend-0.4.0.tgz", @@ -3139,6 +3182,26 @@ "vue": "^3.2.0" } }, + "node_modules/vuedraggable": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/vuedraggable/-/vuedraggable-4.1.0.tgz", + "integrity": "sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==", + "dev": true, + "license": "MIT", + "dependencies": { + "sortablejs": "1.14.0" + }, + "peerDependencies": { + "vue": "^3.0.1" + } + }, + "node_modules/vuedraggable/node_modules/sortablejs": { + "version": "1.14.0", + "resolved": "https://registry.npmmirror.com/sortablejs/-/sortablejs-1.14.0.tgz", + "integrity": "sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w==", + "dev": true, + "license": "MIT" + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -3178,6 +3241,25 @@ "node": ">=12" } }, + "node_modules/xterm": { + "version": "5.3.0", + "resolved": "https://registry.npmmirror.com/xterm/-/xterm-5.3.0.tgz", + "integrity": "sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg==", + "deprecated": "This package is now deprecated. Move to @xterm/xterm instead.", + "dev": true, + "license": "MIT" + }, + "node_modules/xterm-addon-fit": { + "version": "0.7.0", + "resolved": "https://registry.npmmirror.com/xterm-addon-fit/-/xterm-addon-fit-0.7.0.tgz", + "integrity": "sha512-tQgHGoHqRTgeROPnvmtEJywLKoC/V9eNs4bLLz7iyJr1aW/QFzRwfd3MGiJ6odJd9xEfxcW36/xRU47JkD5NKQ==", + "deprecated": "This package is now deprecated. Move to @xterm/addon-fit instead.", + "dev": true, + "license": "MIT", + "peerDependencies": { + "xterm": "^5.0.0" + } + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", @@ -3407,25 +3489,25 @@ } }, "@humanwhocodes/config-array": { - "version": "0.11.7", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.7.tgz", - "integrity": "sha512-kBbPWzN8oVMLb0hOUYXhmxggL/1cJE6ydvjDIGi9EnAGUyA7cLVKQg+d/Dsm+KZwx2czGHrCmMVLiyg8s5JPKw==", + "version": "0.10.7", + "resolved": "https://registry.npmmirror.com/@humanwhocodes/config-array/-/config-array-0.10.7.tgz", + "integrity": "sha512-MDl6D6sBsaV452/QSdX+4CXIjZhIcI0PELsxUjk4U828yd58vk3bTIvk/6w5FY+4hIy9sLW0sfrV7K7Kc++j/w==", "dev": true, "requires": { "@humanwhocodes/object-schema": "^1.2.1", "debug": "^4.1.1", - "minimatch": "^3.0.5" + "minimatch": "^3.0.4" } }, - "@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "@humanwhocodes/gitignore-to-minimatch": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/@humanwhocodes/gitignore-to-minimatch/-/gitignore-to-minimatch-1.0.2.tgz", + "integrity": "sha512-rSqmMJDdLFUsyxR6FMtD00nfQKKLFb1kv+qBbOVKqErvloEIJLo5bDTJTQNTYgeyp78JsA7u/NPi5jT1GR/MuA==", "dev": true }, "@humanwhocodes/object-schema": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "resolved": "https://registry.npmmirror.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, @@ -3961,6 +4043,12 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "cropperjs": { + "version": "1.6.2", + "resolved": "https://registry.npmmirror.com/cropperjs/-/cropperjs-1.6.2.tgz", + "integrity": "sha512-nhymn9GdnV3CqiEHJVai54TULFAE3VshJTXSqSJKa8yXAKyBKDWdhHarnlIPrshJ0WMFTGuFvG02YjLXfPiuOA==", + "dev": true + }, "cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -4104,15 +4192,14 @@ "dev": true }, "eslint": { - "version": "8.29.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.29.0.tgz", - "integrity": "sha512-isQ4EEiyUjZFbEKvEGJKKGBwXtvXX+zJbkVKCgTuB9t/+jUBcy8avhkEwWJecI15BkRkOYmvIM5ynbhRjEkoeg==", + "version": "8.22.0", + "resolved": "https://registry.npmmirror.com/eslint/-/eslint-8.22.0.tgz", + "integrity": "sha512-ci4t0sz6vSRKdmkOGmprBo6fmI4PrphDFMy5JEq/fNS0gQkJM3rLmrqcp8ipMcdobH3KtUP40KniAE9W19S4wA==", "dev": true, "requires": { - "@eslint/eslintrc": "^1.3.3", - "@humanwhocodes/config-array": "^0.11.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", + "@eslint/eslintrc": "^1.3.0", + "@humanwhocodes/config-array": "^0.10.4", + "@humanwhocodes/gitignore-to-minimatch": "^1.0.2", "ajv": "^6.10.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", @@ -4122,21 +4209,21 @@ "eslint-scope": "^7.1.1", "eslint-utils": "^3.0.0", "eslint-visitor-keys": "^3.3.0", - "espree": "^9.4.0", + "espree": "^9.3.3", "esquery": "^1.4.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", "find-up": "^5.0.0", - "glob-parent": "^6.0.2", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^6.0.1", "globals": "^13.15.0", + "globby": "^11.1.0", "grapheme-splitter": "^1.0.4", "ignore": "^5.2.0", "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-sdsl": "^4.1.4", "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", @@ -4147,7 +4234,8 @@ "regexpp": "^3.2.0", "strip-ansi": "^6.0.1", "strip-json-comments": "^3.1.0", - "text-table": "^0.2.0" + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" }, "dependencies": { "eslint-scope": { @@ -4406,6 +4494,12 @@ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, + "functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", + "dev": true + }, "get-intrinsic": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", @@ -4578,12 +4672,6 @@ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true }, - "is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true - }, "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -4595,12 +4683,6 @@ "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.1.tgz", "integrity": "sha512-+0rgsUXZu4ncpPxRL+lNEptWMOWl9etvPHc/koSRp6MPwpRYAhmk0dUG00J4bxVV3r9uUzfo24wW0knS07SKSw==" }, - "js-sdsl": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.2.0.tgz", - "integrity": "sha512-dyBIzQBDkCqCu+0upx25Y2jGdbTGxE9fshMsCdK0ViOongpV+n5tXRcZY9v7CaVQ79AGS9KA1KHtojxiM7aXSQ==", - "dev": true - }, "js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -4728,6 +4810,12 @@ "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.0.tgz", "integrity": "sha512-7dX2/10ITVyqh4aOSVI9gdape+t9l2/8QxHrFmUXu4EEUpdlxl6RudZUPZoc+zuY2hk1j7XxVroIVIan/pD/SQ==" }, + "monaco-editor": { + "version": "0.27.0", + "resolved": "https://registry.npmmirror.com/monaco-editor/-/monaco-editor-0.27.0.tgz", + "integrity": "sha512-UhwP78Wb8w0ZSYoKXQNTV/0CHObp6NS3nCt51QfKE6sKyBo5PBsvuDOHoI2ooBakc6uIwByRLHVeT7+yXQe2fQ==", + "dev": true + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -5097,6 +5185,13 @@ "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==" }, + "splitpanes": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/splitpanes/-/splitpanes-3.2.0.tgz", + "integrity": "sha512-K+WKxWdqtKShV33gPjQl769wHxB3glypTOReCvYu/AJd38J+abHlpiF8rK6uBNPMrgw5thHZCI5JkEwsAqa9XA==", + "dev": true, + "requires": {} + }, "strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -5205,6 +5300,12 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true }, + "v8-compile-cache": { + "version": "2.4.0", + "resolved": "https://registry.npmmirror.com/v8-compile-cache/-/v8-compile-cache-2.4.0.tgz", + "integrity": "sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw==", + "dev": true + }, "vite": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/vite/-/vite-4.0.0.tgz", @@ -5218,6 +5319,13 @@ "rollup": "^3.7.0" } }, + "vite-plugin-monaco-editor": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/vite-plugin-monaco-editor/-/vite-plugin-monaco-editor-1.0.5.tgz", + "integrity": "sha512-vstgFhRMfLfkE5yGQ0cHO3UaW7+eqtOKsBEWLL6ZNmCNwgBxsaB1otWGiJEoHgc9gVoBdeKOOjgW1pACTS1gVQ==", + "dev": true, + "requires": {} + }, "vite-plugin-vue-setup-extend": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/vite-plugin-vue-setup-extend/-/vite-plugin-vue-setup-extend-0.4.0.tgz", @@ -5289,6 +5397,23 @@ "@vue/devtools-api": "^6.4.5" } }, + "vuedraggable": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/vuedraggable/-/vuedraggable-4.1.0.tgz", + "integrity": "sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==", + "dev": true, + "requires": { + "sortablejs": "1.14.0" + }, + "dependencies": { + "sortablejs": { + "version": "1.14.0", + "resolved": "https://registry.npmmirror.com/sortablejs/-/sortablejs-1.14.0.tgz", + "integrity": "sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w==", + "dev": true + } + } + }, "which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -5316,6 +5441,19 @@ "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", "dev": true }, + "xterm": { + "version": "5.3.0", + "resolved": "https://registry.npmmirror.com/xterm/-/xterm-5.3.0.tgz", + "integrity": "sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg==", + "dev": true + }, + "xterm-addon-fit": { + "version": "0.7.0", + "resolved": "https://registry.npmmirror.com/xterm-addon-fit/-/xterm-addon-fit-0.7.0.tgz", + "integrity": "sha512-tQgHGoHqRTgeROPnvmtEJywLKoC/V9eNs4bLLz7iyJr1aW/QFzRwfd3MGiJ6odJd9xEfxcW36/xRU47JkD5NKQ==", + "dev": true, + "requires": {} + }, "yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 6c5d9b8b912f2a8c315ed82a087fab8dca993bba..1567790e4bb1ed0bd384eae98c1b22dde23bbd14 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -36,7 +36,7 @@ "cropperjs": "^1.5.13", "eslint": "8.22.0", "eslint-plugin-vue": "^9.8.0", - "monaco-editor": "^0.34.1", + "monaco-editor": "^0.27.0", "prettier": "^2.8.1", "sass": "^1.56.2", "splitpanes": "^3.1.5", @@ -75,4 +75,4 @@ "type": "git", "url": "https://github.com/baizunxian/zerorunner.git" } -} +} \ No newline at end of file diff --git a/frontend/src/api/useSystemApi/account.ts b/frontend/src/api/useSystemApi/account.ts new file mode 100644 index 0000000000000000000000000000000000000000..554d9aa7a41d48d0db90a1db15223313843dbe3c --- /dev/null +++ b/frontend/src/api/useSystemApi/account.ts @@ -0,0 +1,26 @@ +import request from '/@/utils/request'; +import config from "/@/config/config" +import { ApiResponse } from '/@/types/oceanBase'; +import { AuthedAccountListResponse } from '/@/types/advertiser'; + +/** + * 账户接口-对接千川数据的接口 + */ +export function useAccountApi() { + return { + fetchAuthedAdvertisers: (): Promise> => { + return request({ + url: '/account/authed_advertiser/get/', + method: 'GET' + }); + }, + fetchAdvertisers: (data: object) => { + return request({ + url: '/account/advertiser/list/', + method: 'POST', + data, + }); + }, + + }; +} diff --git a/frontend/src/api/useSystemApi/user.ts b/frontend/src/api/useSystemApi/user.ts index 0d63352d5ffe768fb5fd3f0c9d97cef01f9650c7..d8bc0ee581219bffefc2fe2832b63c40b5e985cc 100644 --- a/frontend/src/api/useSystemApi/user.ts +++ b/frontend/src/api/useSystemApi/user.ts @@ -1,5 +1,5 @@ import request from '/@/utils/request'; - +import config from "/@/config/config" /** * 用户接口 * @method getUserList 获取用户列表 @@ -56,6 +56,13 @@ export function useUserApi() { method: 'POST', data: {} }) + }, + getAuthUrl(data: { appId: string; secret: string }) { + return request({ + url: '/user/auth', + method: 'POST', + data: { appId: data.appId, secret: data.secret } // 参数通过 body 传递 + }); } }; } diff --git a/frontend/src/types/advertiser.d.ts b/frontend/src/types/advertiser.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..9bf94189598fca7aee5b258cb2cb1bdf8e215229 --- /dev/null +++ b/frontend/src/types/advertiser.d.ts @@ -0,0 +1,24 @@ +// 广告主项类型 +export interface AccountItem { + account_id: number; + account_name: string; + account_role: string; + account_string_id: string; + account_type: string; + advertiser_id: number; + advertiser_name: string; + company_list: any[]; // 可以根据实际情况定义更具体的类型 + is_valid: boolean; +} + +// 广告主列表响应类型 +export interface AuthedAccountListResponse { + list: AccountItem[]; + total: number; +} + +// 组件状态类型 +export interface AdvertiserListState { + advertiserList: AccountItem[]; + loading: boolean; +} \ No newline at end of file diff --git a/frontend/src/types/oceanBase.d.ts b/frontend/src/types/oceanBase.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..997cfe1ce79fb1e765bd9ec3336f39981d13aac7 --- /dev/null +++ b/frontend/src/types/oceanBase.d.ts @@ -0,0 +1,8 @@ +// API响应基础类型 +export interface ApiResponse { + code: number + msg?: string + data: T + request_id?: string + trace_id?: string +} diff --git a/frontend/src/types/views.d.ts b/frontend/src/types/views.d.ts index 4fbffe15844f1b98cca63a414b9fd809161e2eea..315947d34b5fd2c932b0d91bb74d7bd60ac829e3 100644 --- a/frontend/src/types/views.d.ts +++ b/frontend/src/types/views.d.ts @@ -6,6 +6,39 @@ type NewInfo = { date: string; link: string; }; + +type AdvertiserItem = { + advertiser_name: string + advertiser_id: number + advertiser_type: string +}; + +type PageInfo = { + page: number + pageSize: number + total: number +} + +export interface AdvertiserResponse { + code: number + message: string + request_id: string + data: { + page_info: { + page: number + total_number: number + page_size: number + total_page: number + } + list: AdvertiserItem[] + } +} + +type AdvertiserListInfo = { + advertiserList: AdvertiserItem[]; + pageInfo: PageInfo; +}; + type Recommend = { title: string; msg: string; @@ -15,6 +48,7 @@ type Recommend = { }; declare type PersonalState = { newsInfoList: NewInfo[]; + advertiserListInfo: AdvertiserListInfo; recommendList: Recommend[]; personalForm: { username: string; @@ -24,9 +58,14 @@ declare type PersonalState = { email: string; tags: string; }; + authFrom: { + appId: string; + secret: string; + }; editTag: boolean; tagValue: string; showEditPage: boolean; + showAuthPage: boolean; }; /** diff --git a/frontend/src/views/home/index.vue b/frontend/src/views/home/index.vue index 4d8bad9c3bb8374bea47b6e759d97d5e956c1940..4bf7379742840156e977dcb359fea54243e221c0 100644 --- a/frontend/src/views/home/index.vue +++ b/frontend/src/views/home/index.vue @@ -72,7 +72,8 @@ import * as echarts from 'echarts'; import {storeToRefs} from 'pinia'; import {useThemeConfig} from '/@/stores/themeConfig'; import {useTagsViewRoutes} from '/@/stores/tagsViewRoutes'; - +import { useRoute } from 'vue-router' +import { ElMessage } from 'element-plus' // 定义变量内容 const homeLineRef = ref(); const homePieRef = ref(); @@ -81,6 +82,7 @@ const storesTagsViewRoutes = useTagsViewRoutes(); const storesThemeConfig = useThemeConfig(); const {themeConfig} = storeToRefs(storesThemeConfig); const {isTagsViewCurrenFull} = storeToRefs(storesTagsViewRoutes); + const state = reactive({ global: { homeChartOne: null, @@ -505,6 +507,10 @@ const initEchartsResize = () => { // 页面加载时 onMounted(() => { initEchartsResize(); + const route = useRoute() + if (route.query.auth_success === '1') { + ElMessage.success('授权成功!') + } }); // 由于页面缓存原因,keep-alive onActivated(() => { diff --git a/frontend/src/views/login/index.vue b/frontend/src/views/login/index.vue index d59833258556bed52f622f433246b0bc11953555..c1bde547f026f4881af1af677f262f08cd048089 100644 --- a/frontend/src/views/login/index.vue +++ b/frontend/src/views/login/index.vue @@ -24,9 +24,9 @@ - - - + @@ -54,7 +54,6 @@ import loginMain from '/@/assets/login-main.svg'; const Account = defineAsyncComponent(() => import('/@/views/login/component/account.vue')); const Mobile = defineAsyncComponent(() => import('/@/views/login/component/mobile.vue')); const Scan = defineAsyncComponent(() => import('/@/views/login/component/scan.vue')); - // 定义变量内容 const storesThemeConfig = useThemeConfig(); const {themeConfig} = storeToRefs(storesThemeConfig); diff --git a/frontend/src/views/system/personal/AdvertiserList.vue b/frontend/src/views/system/personal/AdvertiserList.vue new file mode 100644 index 0000000000000000000000000000000000000000..7b547f44401a8f474986759dad63c923052a69f6 --- /dev/null +++ b/frontend/src/views/system/personal/AdvertiserList.vue @@ -0,0 +1,176 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/system/personal/AuthedAccountList.vue b/frontend/src/views/system/personal/AuthedAccountList.vue new file mode 100644 index 0000000000000000000000000000000000000000..4f627a4084a643dd26cf3b3aa4298267fd527fce --- /dev/null +++ b/frontend/src/views/system/personal/AuthedAccountList.vue @@ -0,0 +1,235 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/system/personal/index.vue b/frontend/src/views/system/personal/index.vue index 8c98f71edfef00667cb574ddcc8c6fe7f6cd713e..34555f7a730c146c9debffb9b1eb9c92a95fcab8 100644 --- a/frontend/src/views/system/personal/index.vue +++ b/frontend/src/views/system/personal/index.vue @@ -72,6 +72,13 @@ 更新个人信息 + + + + + + 授权查询 + @@ -97,23 +104,7 @@ - - - - -
- -
-
-
- - + + + + + + +
授权信息
+ + + + + + + + + + + + + + +
+
+
+ +
@@ -194,6 +216,10 @@ import {formatAxis} from '/@/utils/formatTime'; import {useUserInfo} from "/@/stores/userInfo"; import {useUserApi} from "/@/api/useSystemApi/user"; import {ElMessage} from "element-plus"; +import { PersonalState } from '/@/types/views'; +import { fa } from 'element-plus/es/locale'; +import { useAccountApi } from '/@/api/useSystemApi/account'; +import AuthedAccountList from './AuthedAccountList.vue'; const SeePictures = defineAsyncComponent(() => import("/@/components/seePictures/index.vue")) const SeePicturesRef = ref(); @@ -206,6 +232,14 @@ const userStores = useUserInfo() // 定义变量内容 const state = reactive({ newsInfoList: [], + advertiserListInfo: { + advertiserList: [], + pageInfo: { + page: 1, + pageSize: 20, + total: 0 + } + }, recommendList: [], personalForm: { username: '', @@ -215,11 +249,14 @@ const state = reactive({ email: '', tags: '', }, + authFrom:{ + appId:'', + secret:'', + }, editTag: false, tagValue: "", - showEditPage: false, - cropperImg: '', + showAuthPage: false, }); const getUserInfo = async () => { @@ -269,6 +306,11 @@ const save = async () => { ElMessage.success("更新成功!") } +const auth = async () => { + let response = await useUserApi().getAuthUrl(state.authFrom); + window.location.href = response.data.url; // 后端返回{url: '...'} +} + const updateAvatar = (img: String) => { state.personalForm.avatar = img save() @@ -282,6 +324,89 @@ onMounted(() => {