补充公司log

This commit is contained in:
zk
2026-07-03 17:27:18 +08:00
parent fda30cd295
commit 5cebc1f929
8 changed files with 143 additions and 5 deletions
+7
View File
@@ -39,3 +39,10 @@ JOB_EXPIRE_DAYS=7
# 日志 # 日志
LOGGING_LEVEL=INFO LOGGING_LEVEL=INFO
# 阿里云 OSS
OSS_ACCESS_KEY_ID=LTAI5tEdLKKQUKhTyUpfH5Mk
OSS_ACCESS_KEY_SECRET=RjUdTrq0V5qA4b3BUElNhXqs3ZLp5k
OSS_ENDPOINT=oss-cn-guangzhou.aliyuncs.com
OSS_BUCKET=offerpie
OSS_DOMAIN=https://offerpie.oss-cn-guangzhou.aliyuncs.com
+7
View File
@@ -33,3 +33,10 @@ JOB_EXPIRE_DAYS=7
# 日志 # 日志
LOGGING_LEVEL=INFO LOGGING_LEVEL=INFO
# 阿里云 OSS
OSS_ACCESS_KEY_ID=LTAI5tEdLKKQUKhTyUpfH5Mk
OSS_ACCESS_KEY_SECRET=RjUdTrq0V5qA4b3BUElNhXqs3ZLp5k
OSS_ENDPOINT=oss-cn-guangzhou.aliyuncs.com
OSS_BUCKET=offerpie
OSS_DOMAIN=https://offerpie.oss-cn-guangzhou.aliyuncs.com
+7
View File
@@ -34,3 +34,10 @@ JOB_EXPIRE_DAYS=7
# 日志 # 日志
LOGGING_LEVEL=INFO LOGGING_LEVEL=INFO
# 阿里云 OSS
OSS_ACCESS_KEY_ID=LTAI5tEdLKKQUKhTyUpfH5Mk
OSS_ACCESS_KEY_SECRET=RjUdTrq0V5qA4b3BUElNhXqs3ZLp5k
OSS_ENDPOINT=oss-cn-guangzhou.aliyuncs.com
OSS_BUCKET=offerpie
OSS_DOMAIN=https://offerpie.oss-cn-guangzhou.aliyuncs.com
+10
View File
@@ -48,6 +48,16 @@ class Settings(BaseSettings):
# ──────────── 岗位下架参数 ──────────── # ──────────── 岗位下架参数 ────────────
job_expire_days: int = 7 job_expire_days: int = 7
# ──────────── 阿里云 OSS ────────────
oss_access_key_id: str = ""
oss_access_key_secret: str = ""
oss_endpoint: str = "oss-cn-guangzhou.aliyuncs.com"
oss_bucket: str = "offerpie"
# 固定上传目录(对象 key 前缀)
oss_upload_dir: str = "company/logo"
# 下载访问域名
oss_domain: str = "https://offerpie.oss-cn-guangzhou.aliyuncs.com"
# ──────────── 日志 ──────────── # ──────────── 日志 ────────────
logging_level: str = "INFO" logging_level: str = "INFO"
log_file_name: str = "cleaner.log" log_file_name: str = "cleaner.log"
+42
View File
@@ -0,0 +1,42 @@
"""阿里云 OSS 上传:往固定目录上传,返回下载地址"""
import asyncio
import base64
import os
from uuid import uuid4
import oss2
from app.config import settings
from app.core.logger import log
_auth = oss2.Auth(settings.oss_access_key_id, settings.oss_access_key_secret)
_bucket = oss2.Bucket(_auth, f"https://{settings.oss_endpoint}", settings.oss_bucket)
async def upload(data: bytes, file_name: str) -> str:
"""上传字节内容到固定目录,返回下载地址"""
key = f"{settings.oss_upload_dir}/{uuid4().hex[:18]}{os.path.splitext(file_name)[1]}"
await asyncio.to_thread(_bucket.put_object, key, data)
url = f"{settings.oss_domain}/{key}"
log.info("OSS 上传成功 [{}] -> {}", file_name, url)
return url
async def upload_base64(b64: str, file_name: str = "image.png") -> str | None:
"""上传 base64 图片到固定目录,返回下载地址;空值或解码失败返回 None
:param b64: base64 字符串,兼容 data URI 前缀(data:image/png;base64,xxx
:param file_name: 用于取后缀,默认 png
"""
if not b64 or not b64.strip():
return None
# 去掉 data URI 前缀
if "," in b64 and b64.strip().startswith("data:"):
b64 = b64.split(",", 1)[1]
try:
data = base64.b64decode(b64)
except Exception as e:
log.warning("base64 解码失败: {}", e)
return None
return await upload(data, file_name)
+35
View File
@@ -0,0 +1,35 @@
"""PostgreSQL: app_url_list 表模型"""
from datetime import datetime
from typing import Optional
from sqlalchemy import BigInteger, DateTime, Integer, String, Text
from sqlalchemy.orm import Mapped, mapped_column
from app.core.database import PgBase
class AppUrlList(PgBase):
"""爬虫 URL 任务列表"""
__tablename__ = "app_url_list"
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True, comment="自增主键")
status: Mapped[str] = mapped_column(String(32), default="pending", nullable=False, comment="任务状态")
input_company_name: Mapped[str] = mapped_column(String(255), nullable=False, comment="输入公司名")
input_url: Mapped[Optional[str]] = mapped_column(String(1024), comment="输入URL")
output_url: Mapped[Optional[str]] = mapped_column(String(1024), comment="输出URL")
post_login: Mapped[Optional[int]] = mapped_column(Integer, comment="发布登录")
crawl_login: Mapped[Optional[int]] = mapped_column(Integer, comment="爬取登录")
type_sz: Mapped[Optional[int]] = mapped_column(Integer, comment="类型sz")
type_sx: Mapped[Optional[int]] = mapped_column(Integer, comment="类型sx")
crawlok_at: Mapped[Optional[str]] = mapped_column(String(32), comment="-2没有数据或者数据全部过期或者数据缺失 -3 过滤后没数据, -4无法修复,")
pyname: Mapped[Optional[str]] = mapped_column(String(255), comment="脚本名字")
type_xz: Mapped[Optional[int]] = mapped_column(Integer, comment="类型xz")
error_message: Mapped[Optional[str]] = mapped_column(Text, comment="错误信息")
logo: Mapped[Optional[str]] = mapped_column(Text, comment="公司logo (base64编码的64x64 PNG图片)")
started_at: Mapped[Optional[datetime]] = mapped_column(DateTime, comment="开始时间")
finished_at: Mapped[Optional[datetime]] = mapped_column(DateTime, comment="完成时间")
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, comment="创建时间")
updated_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, comment="更新时间")
output_company_name: Mapped[Optional[str]] = mapped_column(String(255), comment="输出公司名")
+32 -3
View File
@@ -10,6 +10,7 @@ from sqlalchemy import text, insert
from app.config import settings from app.config import settings
from app.core.database import PgSession, MysqlSession from app.core.database import PgSession, MysqlSession
from app.core.logger import log from app.core.logger import log
from app.core.oss import upload_base64
from app.ai.model_config import JobCleanModel from app.ai.model_config import JobCleanModel
from app.ai.prompts import JOB_STRUCTURE_SYSTEM, MAJOR_MATCH_SYSTEM, SKILL_EXTRACT_SYSTEM from app.ai.prompts import JOB_STRUCTURE_SYSTEM, MAJOR_MATCH_SYSTEM, SKILL_EXTRACT_SYSTEM
from app.models.mysql.job import Job from app.models.mysql.job import Job
@@ -134,7 +135,7 @@ async def _do_clean(data: dict) -> None:
# 公司处理 # 公司处理
company_short_name = result.get("companyShortName") or data.get("company") or "" company_short_name = result.get("companyShortName") or data.get("company") or ""
company_id = await _find_or_create_company(company_short_name) company_id = await _find_or_create_company(company_short_name, data.get("urllistid"))
# 地区处理 # 地区处理
region_codes = [] region_codes = []
@@ -284,8 +285,8 @@ async def _find_or_create_skill_tag(name: str) -> int | None:
return row.scalar() return row.scalar()
async def _find_or_create_company(short_name: str) -> int: async def _find_or_create_company(short_name: str, urllistid: int | None = None) -> int:
"""查找或创建公司(加锁防并发重复)""" """查找或创建公司(加锁防并发重复);新建公司时若有 logo 则上传 OSS 并回填地址"""
async with _company_lock: async with _company_lock:
async with MysqlSession() as mysql: async with MysqlSession() as mysql:
row = await mysql.execute( row = await mysql.execute(
@@ -309,9 +310,37 @@ async def _find_or_create_company(short_name: str) -> int:
) )
) )
await mysql.commit() await mysql.commit()
# 锁外处理 logo:仅新建公司时执行,上传是网络IO,不阻塞其他协程;失败不影响主流程
try:
logo_b64 = await _get_logo_base64(urllistid)
if logo_b64:
logo_url = await upload_base64(logo_b64, "logo.png")
if logo_url:
async with MysqlSession() as mysql:
await mysql.execute(
text("UPDATE bg_company SET logo_url = :url, update_time = :t WHERE id = :id"),
{"url": logo_url, "t": datetime.now(), "id": company_id},
)
await mysql.commit()
except Exception as e:
log.warning("[company={}] logo 上传失败: {}", company_id, e)
return company_id return company_id
async def _get_logo_base64(urllistid: int | None) -> str | None:
"""从 PG app_url_list 按 urllistid 读取 logobase64"""
if not urllistid:
return None
async with PgSession() as pg:
row = await pg.execute(
text("SELECT logo FROM app_url_list WHERE id = :id"),
{"id": urllistid},
)
return row.scalar()
async def _update_pg_status(data_id: int, status: str) -> None: async def _update_pg_status(data_id: int, status: str) -> None:
"""更新 PG 清洗状态""" """更新 PG 清洗状态"""
async with PgSession() as pg: async with PgSession() as pg:
+1
View File
@@ -14,3 +14,4 @@ langchain-core>=0.3
loguru>=0.7 loguru>=0.7
snowflake-id>=1.0 snowflake-id>=1.0
json-repair>=0.30 json-repair>=0.30
oss2>=2.18