feat: complete huihui square avatar workflows
This commit is contained in:
@@ -7,7 +7,7 @@ import uuid
|
||||
import hashlib
|
||||
import hmac
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional
|
||||
from typing import Optional, Any
|
||||
import httpx
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, update
|
||||
@@ -45,6 +45,31 @@ class NewsPlatformService:
|
||||
"orgId": await self._cfg(db, "platform_org_id", ""),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _api_root(base_url: str) -> str:
|
||||
if "/api/" in base_url:
|
||||
return base_url.split("/api/", 1)[0] + "/api"
|
||||
return base_url.rstrip("/")
|
||||
|
||||
@staticmethod
|
||||
def _resolve_synced_nickname(user: VirtualUser, sync_nickname: str, sync_real_name: str) -> str:
|
||||
"""避免登录同步把有意义的本地昵称覆盖回手机号。"""
|
||||
current_nickname = (user.nickname or "").strip()
|
||||
current_real_name = (user.real_name or "").strip()
|
||||
sync_nickname = (sync_nickname or "").strip()
|
||||
sync_real_name = (sync_real_name or "").strip()
|
||||
account = (user.account or "").strip()
|
||||
|
||||
if current_nickname and current_nickname != account:
|
||||
return current_nickname
|
||||
if sync_nickname and sync_nickname != account:
|
||||
return sync_nickname
|
||||
if sync_real_name:
|
||||
return sync_real_name
|
||||
if current_real_name:
|
||||
return current_real_name
|
||||
return sync_nickname or current_nickname or account
|
||||
|
||||
# ─── 签名(完全对应 sign.js 逻辑) ─────────────────────────
|
||||
@staticmethod
|
||||
def _make_sign(params: dict, secret_key: str, sign_type: str = "MD5") -> str:
|
||||
@@ -188,6 +213,7 @@ class NewsPlatformService:
|
||||
sync_real_name = user_info.get("realName") or ""
|
||||
sync_sex = int(user_info.get("sex") or 0)
|
||||
sync_avatar = user_info.get("avatar") or ""
|
||||
preferred_nickname = self._resolve_synced_nickname(user, sync_nickname, sync_real_name)
|
||||
|
||||
await set_session(user.id, {
|
||||
"token": access_token,
|
||||
@@ -196,7 +222,7 @@ class NewsPlatformService:
|
||||
"org_id": org_id or cfg.get("orgId", ""),
|
||||
"login_time": datetime.now().isoformat(),
|
||||
# 缓存用户信息供 sync 使用
|
||||
"nickname": sync_nickname,
|
||||
"nickname": preferred_nickname,
|
||||
"real_name": sync_real_name,
|
||||
"sex": sync_sex,
|
||||
"avatar": sync_avatar,
|
||||
@@ -209,7 +235,7 @@ class NewsPlatformService:
|
||||
last_login_at=datetime.now(),
|
||||
platform_uid=platform_uid,
|
||||
)
|
||||
if sync_nickname: update_vals["nickname"] = sync_nickname
|
||||
if preferred_nickname: update_vals["nickname"] = preferred_nickname
|
||||
if sync_real_name: update_vals["real_name"] = sync_real_name
|
||||
if sync_sex: update_vals["sex"] = sync_sex
|
||||
if sync_avatar: update_vals["avatar_url"] = sync_avatar
|
||||
@@ -719,51 +745,287 @@ class NewsPlatformService:
|
||||
return False
|
||||
|
||||
async def post_comment(self, db, user, news_id, news_title, content, news_author_id="", org_id="") -> tuple[bool, str]:
|
||||
success, err, _ = await self.post_comment_with_record_id(
|
||||
db, user, news_id, news_title, content,
|
||||
news_author_id=news_author_id, org_id=org_id,
|
||||
)
|
||||
return success, err
|
||||
|
||||
async def post_comment_with_record_id(
|
||||
self, db, user, news_id, news_title, content, news_author_id="", org_id=""
|
||||
) -> tuple[bool, str, str]:
|
||||
sess = await get_session(user.id)
|
||||
if not sess:
|
||||
return False, "未登录"
|
||||
return False, "未登录", ""
|
||||
biz = await self._biz_url(db)
|
||||
cfg = await self._client(db)
|
||||
uid = sess.get("platform_uid", "")
|
||||
# org_id 优先取文章自带的(从广场数据获取),否则取 session/配置
|
||||
final_org_id = org_id or sess.get("org_id") or cfg.get("orgId") or ""
|
||||
if not final_org_id:
|
||||
final_org_id = await self._get_article_org_id(db, sess["token"], news_id)
|
||||
body = {
|
||||
"module": "news", "topicId": news_id, "title": news_title,
|
||||
"content": content, "orgId": final_org_id,
|
||||
"toUserId": news_author_id or uid, "userId": uid,
|
||||
"userName": user.nickname, "avatar": user.avatar_url or "",
|
||||
}
|
||||
return await self._json_post(f"{biz}/message/comment", self._bearer(sess["token"]), body)
|
||||
success, err, record_id = await self._json_post_with_record_id(
|
||||
f"{biz}/message/comment", self._bearer(sess["token"]), body
|
||||
)
|
||||
if success and not record_id:
|
||||
record_id, _ = await self.find_comment_id(db, user, news_id, content)
|
||||
return success, err, record_id
|
||||
|
||||
async def post_reply(self, db, user, news_id, comment_id, content) -> tuple[bool, str]:
|
||||
async def post_reply(
|
||||
self, db, user, news_id, comment_id, content,
|
||||
parent_comment: dict | None = None,
|
||||
reply_to: dict | None = None,
|
||||
article_title: str = "",
|
||||
org_id: str = "",
|
||||
) -> tuple[bool, str]:
|
||||
success, err, _ = await self.post_reply_with_record_id(
|
||||
db, user, news_id, comment_id, content,
|
||||
parent_comment=parent_comment,
|
||||
reply_to=reply_to,
|
||||
article_title=article_title,
|
||||
org_id=org_id,
|
||||
)
|
||||
return success, err
|
||||
|
||||
async def post_reply_with_record_id(
|
||||
self, db, user, news_id, comment_id, content,
|
||||
parent_comment: dict | None = None,
|
||||
reply_to: dict | None = None,
|
||||
article_title: str = "",
|
||||
org_id: str = "",
|
||||
) -> tuple[bool, str, str]:
|
||||
sess = await get_session(user.id)
|
||||
if not sess:
|
||||
return False, "未登录"
|
||||
return False, "未登录", ""
|
||||
biz = await self._biz_url(db)
|
||||
uid = sess.get("platform_uid", "")
|
||||
cfg = await self._client(db)
|
||||
parent_comment = parent_comment or {}
|
||||
reply_to = reply_to or None
|
||||
parent_comment_id = str(parent_comment.get("id") or parent_comment.get("commentId") or comment_id or "")
|
||||
parent_user_id = str(
|
||||
parent_comment.get("createUser")
|
||||
or parent_comment.get("userId")
|
||||
or parent_comment.get("commentUserId")
|
||||
or parent_comment.get("fromUserId")
|
||||
or ""
|
||||
)
|
||||
parent_user_name = (
|
||||
parent_comment.get("userName")
|
||||
or parent_comment.get("fromUserName")
|
||||
or parent_comment.get("nickName")
|
||||
or ""
|
||||
)
|
||||
reply_target = reply_to or parent_comment
|
||||
reply_target_id = str(
|
||||
reply_target.get("id")
|
||||
or reply_target.get("replyId")
|
||||
or reply_target.get("commentId")
|
||||
or parent_comment_id
|
||||
)
|
||||
reply_target_user_id = str(
|
||||
reply_target.get("createUser")
|
||||
or reply_target.get("fromUserId")
|
||||
or reply_target.get("userId")
|
||||
or reply_target.get("commentUserId")
|
||||
or parent_user_id
|
||||
or ""
|
||||
)
|
||||
reply_target_user_name = (
|
||||
reply_target.get("fromUserName")
|
||||
or reply_target.get("userName")
|
||||
or reply_target.get("nickName")
|
||||
or parent_user_name
|
||||
or ""
|
||||
)
|
||||
from_user_name = (
|
||||
(user.real_name or "").strip()
|
||||
or (user.nickname or "").strip()
|
||||
or (user.account or "").strip()
|
||||
)
|
||||
final_org_id = org_id or sess.get("org_id") or cfg.get("orgId") or ""
|
||||
if not final_org_id:
|
||||
final_org_id = await self._get_article_org_id(db, sess["token"], news_id)
|
||||
body = {
|
||||
"module": "news", "topicId": news_id, "commentId": comment_id,
|
||||
"commentUserId": uid, "content": content,
|
||||
"fromUserName": user.nickname, "avatar": user.avatar_url or "",
|
||||
"module": "news",
|
||||
"modules": "news",
|
||||
"topicId": news_id,
|
||||
"commentId": parent_comment_id,
|
||||
"commentUserId": parent_user_id,
|
||||
"replyId": reply_target_id,
|
||||
"toUserId": reply_target_user_id,
|
||||
"replyType": "2" if reply_to else "1",
|
||||
"toUserName": reply_target_user_name,
|
||||
"content": content,
|
||||
"title": article_title or parent_comment.get("title") or "",
|
||||
"orgId": final_org_id,
|
||||
"fromUserName": from_user_name,
|
||||
"avatar": user.avatar_url or "",
|
||||
}
|
||||
return await self._json_post(f"{biz}/message/comment/reply", self._bearer(sess["token"]), body)
|
||||
success, err, reply_id = await self._json_post_with_record_id(
|
||||
f"{biz}/message/comment/reply", self._bearer(sess["token"]), body
|
||||
)
|
||||
if success and not reply_id:
|
||||
reply_id, _ = await self.find_reply_id(
|
||||
db, user, news_id,
|
||||
content=content,
|
||||
parent_comment_id=parent_comment_id,
|
||||
)
|
||||
return success, err, reply_id
|
||||
|
||||
async def _get_article_org_id(self, db, token: str, news_id: str) -> str:
|
||||
"""从文章详情补齐 H5 评论/回复所需的组织上下文。"""
|
||||
if not news_id:
|
||||
return ""
|
||||
biz = await self._biz_url(db)
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=8) as c:
|
||||
r = await c.get(f"{biz}/news/{news_id}", headers=self._bearer(token))
|
||||
if r.status_code != 200:
|
||||
return ""
|
||||
data = (r.json().get("data") or {})
|
||||
return str(
|
||||
data.get("orgId")
|
||||
or data.get("alumnusId")
|
||||
or data.get("publishOrgId")
|
||||
or data.get("initialPublishOrgId")
|
||||
or ""
|
||||
)
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
async def get_comments(self, db, user, news_id) -> list:
|
||||
data = await self.get_comments_payload(db, user, news_id)
|
||||
if isinstance(data, dict):
|
||||
rows = data.get("data") or data.get("records") or data.get("list") or data.get("rows")
|
||||
return rows if isinstance(rows, list) else []
|
||||
return data if isinstance(data, list) else []
|
||||
|
||||
async def get_comments_payload(self, db, user, news_id) -> Any:
|
||||
sess = await get_session(user.id)
|
||||
if not sess:
|
||||
return []
|
||||
biz = await self._biz_url(db)
|
||||
cfg = await self._client(db)
|
||||
try:
|
||||
params = self._build_form({"module": "news", "topicId": news_id, "pageNum": 1, "pageSize": 20}, cfg)
|
||||
params = {"module": "news", "topicId": news_id, "pageNum": 1, "pageSize": 20}
|
||||
async with httpx.AsyncClient(timeout=10) as c:
|
||||
r = await c.get(f"{biz}/message/comment", headers=self._bearer(sess["token"]), params=params)
|
||||
r = await c.get(
|
||||
f"{self._api_root(biz)}/interaction/open/selectInteractionCommentList",
|
||||
headers=self._bearer(sess["token"]),
|
||||
params=params,
|
||||
)
|
||||
if r.status_code == 200:
|
||||
return r.json().get("data", {}).get("data") or []
|
||||
return r.json().get("data") or []
|
||||
except Exception:
|
||||
pass
|
||||
return []
|
||||
|
||||
async def find_comment_id(self, db, user, news_id: str, content: str = "") -> tuple[str, int | None]:
|
||||
"""Best-effort lookup for old records created before platform IDs were saved."""
|
||||
sess = await get_session(user.id)
|
||||
my_uid = str(sess.get("platform_uid", "")) if sess else ""
|
||||
payload = await self.get_comments_payload(db, user, news_id)
|
||||
comment_count = None
|
||||
comments = payload if isinstance(payload, list) else []
|
||||
if isinstance(payload, dict):
|
||||
try:
|
||||
comment_count = int(payload.get("commentCount"))
|
||||
except (TypeError, ValueError):
|
||||
comment_count = None
|
||||
comments = (
|
||||
payload.get("data")
|
||||
or payload.get("records")
|
||||
or payload.get("list")
|
||||
or payload.get("rows")
|
||||
or []
|
||||
)
|
||||
if not isinstance(comments, list):
|
||||
comments = []
|
||||
|
||||
target_content = (content or "").strip()
|
||||
for item in comments:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
author_id = str(item.get("userId") or item.get("createUser") or item.get("commentUserId") or "")
|
||||
item_content = (item.get("content") or "").strip()
|
||||
if my_uid and author_id and author_id != my_uid:
|
||||
continue
|
||||
if target_content and item_content != target_content:
|
||||
continue
|
||||
comment_id = str(item.get("id") or item.get("commentId") or item.get("messageId") or "")
|
||||
if comment_id:
|
||||
return comment_id, comment_count
|
||||
return "", comment_count
|
||||
|
||||
async def find_reply_id(
|
||||
self, db, user, news_id: str, content: str = "", parent_comment_id: str = ""
|
||||
) -> tuple[str, int | None]:
|
||||
"""Best-effort lookup for reply records created before platform IDs were saved."""
|
||||
sess = await get_session(user.id)
|
||||
my_uid = str(sess.get("platform_uid", "")) if sess else ""
|
||||
payload = await self.get_comments_payload(db, user, news_id)
|
||||
reply_count = None
|
||||
comments = payload if isinstance(payload, list) else []
|
||||
if isinstance(payload, dict):
|
||||
for count_key in ("replyCount", "commentCount", "total"):
|
||||
try:
|
||||
if payload.get(count_key) is not None:
|
||||
reply_count = int(payload.get(count_key))
|
||||
break
|
||||
except (TypeError, ValueError):
|
||||
reply_count = None
|
||||
comments = (
|
||||
payload.get("data")
|
||||
or payload.get("records")
|
||||
or payload.get("list")
|
||||
or payload.get("rows")
|
||||
or []
|
||||
)
|
||||
if not isinstance(comments, list):
|
||||
comments = []
|
||||
|
||||
target_content = (content or "").strip()
|
||||
target_parent_id = str(parent_comment_id or "")
|
||||
for item in comments:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
current_parent_id = str(item.get("id") or item.get("commentId") or "")
|
||||
if target_parent_id and current_parent_id != target_parent_id:
|
||||
continue
|
||||
replies = (
|
||||
item.get("respList")
|
||||
or item.get("replyList")
|
||||
or item.get("children")
|
||||
or item.get("replies")
|
||||
or []
|
||||
)
|
||||
if not isinstance(replies, list):
|
||||
continue
|
||||
for reply in replies:
|
||||
if not isinstance(reply, dict):
|
||||
continue
|
||||
author_id = str(
|
||||
reply.get("fromUserId")
|
||||
or reply.get("createUser")
|
||||
or reply.get("userId")
|
||||
or reply.get("commentUserId")
|
||||
or ""
|
||||
)
|
||||
reply_content = (reply.get("content") or "").strip()
|
||||
if my_uid and author_id and author_id != my_uid:
|
||||
continue
|
||||
if target_content and reply_content != target_content:
|
||||
continue
|
||||
reply_id = str(reply.get("id") or reply.get("replyId") or reply.get("commentId") or "")
|
||||
if reply_id:
|
||||
return reply_id, reply_count
|
||||
return "", reply_count
|
||||
|
||||
async def like_news(self, db, user, news_id, org_id="", to_user_id="", title="") -> tuple[bool, str]:
|
||||
sess = await get_session(user.id)
|
||||
if not sess:
|
||||
@@ -825,13 +1087,41 @@ class NewsPlatformService:
|
||||
return True, ""
|
||||
return False, f"HTTP {resp.status_code}"
|
||||
|
||||
@staticmethod
|
||||
def _extract_record_id(data: Any) -> str:
|
||||
if data is None:
|
||||
return ""
|
||||
if isinstance(data, (str, int)):
|
||||
return str(data)
|
||||
if isinstance(data, dict):
|
||||
for key in ("id", "replyId", "commentId", "messageId", "recordId"):
|
||||
if data.get(key):
|
||||
return str(data[key])
|
||||
for key in ("data", "record", "reply", "comment", "message"):
|
||||
nested = NewsPlatformService._extract_record_id(data.get(key))
|
||||
if nested:
|
||||
return nested
|
||||
return ""
|
||||
|
||||
async def _json_post(self, url, headers, body) -> tuple[bool, str]:
|
||||
success, err, _ = await self._json_post_with_record_id(url, headers, body)
|
||||
return success, err
|
||||
|
||||
async def _json_post_with_record_id(self, url, headers, body) -> tuple[bool, str, str]:
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=15) as c:
|
||||
r = await c.post(url, json=body, headers=headers)
|
||||
return self._ok(r)
|
||||
if r.status_code not in [200, 201]:
|
||||
return False, f"HTTP {r.status_code}", ""
|
||||
try:
|
||||
d = r.json()
|
||||
except Exception:
|
||||
return True, "", ""
|
||||
if d.get("code") in [0, 200]:
|
||||
return True, "", self._extract_record_id(d.get("data"))
|
||||
return False, d.get("message") or "业务失败", ""
|
||||
except Exception as e:
|
||||
return False, str(e)
|
||||
return False, str(e), ""
|
||||
|
||||
async def _write_login_log(self, db, user, action, session_id=None, error_msg=None):
|
||||
try:
|
||||
@@ -898,6 +1188,25 @@ class NewsPlatformService:
|
||||
except Exception as e:
|
||||
return False, str(e)
|
||||
|
||||
async def cancel_reply(self, db, user, news_id: str, reply_id: str) -> tuple[bool, str]:
|
||||
"""DELETE /message/comment/reply/{topicId}/{id} 删除评论回复"""
|
||||
sess = await get_session(user.id)
|
||||
if not sess:
|
||||
return False, "未登录"
|
||||
biz = await self._biz_url(db)
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10) as c:
|
||||
r = await c.delete(
|
||||
f"{biz}/message/comment/reply/{news_id}/{reply_id}",
|
||||
headers=self._bearer(sess["token"]),
|
||||
)
|
||||
d = r.json()
|
||||
if d.get("code") in [0, 200]:
|
||||
return True, ""
|
||||
return False, d.get("message", "删除回复失败")
|
||||
except Exception as e:
|
||||
return False, str(e)
|
||||
|
||||
async def cancel_collect(self, db, user, news_id: str, org_id: str = "", to_user_id: str = "", title: str = "") -> tuple[bool, str]:
|
||||
"""取消收藏(复用取消点赞接口)"""
|
||||
return await self.cancel_like(db, user, news_id, org_id=org_id, to_user_id=to_user_id, title=title)
|
||||
|
||||
Reference in New Issue
Block a user