更新新功能
This commit is contained in:
parent
a8e6edf296
commit
7b50e2768b
@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { apiRequest } from '../../utils/api'
|
||||
@ -15,16 +15,18 @@ const editingId = ref(null)
|
||||
const registrationVisible = ref(false)
|
||||
const registrationLoading = ref(false)
|
||||
const registrationDetail = ref(null)
|
||||
|
||||
const registrationFilters = reactive({
|
||||
gender: '',
|
||||
status: ''
|
||||
})
|
||||
const filteredRegistrations = ref([])
|
||||
|
||||
const filters = reactive({
|
||||
keyword: '',
|
||||
category: '',
|
||||
publishState: ''
|
||||
})
|
||||
|
||||
const form = reactive({
|
||||
title: '',
|
||||
description: '',
|
||||
@ -42,6 +44,7 @@ const form = reactive({
|
||||
require_audit: true,
|
||||
is_published: false
|
||||
})
|
||||
|
||||
const editForm = reactive({
|
||||
title: '',
|
||||
description: '',
|
||||
@ -60,18 +63,24 @@ const editForm = reactive({
|
||||
is_published: false
|
||||
})
|
||||
|
||||
function mapActivity(item) {
|
||||
return {
|
||||
...item,
|
||||
statusText: item.status === 0 ? '草稿' : item.status === 1 ? '报名中' : item.status === 4 ? '已结束' : '其他',
|
||||
statusTagType: item.status === 1 ? 'success' : item.status === 4 ? 'info' : 'warning',
|
||||
publishText: item.is_published ? '已发布' : '未发布',
|
||||
publishTagType: item.is_published ? 'success' : 'info',
|
||||
requireAuditText: item.require_audit ? '需审核' : '免审核',
|
||||
shareUrlLinkStatusText: item.share_url_link ? '可用' : '不可用',
|
||||
shareUrlLinkStatusType: item.share_url_link ? 'success' : 'danger'
|
||||
}
|
||||
}
|
||||
|
||||
async function loadActivities() {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await apiRequest('/admin/activities')
|
||||
allActivities.value = (Array.isArray(data) ? data : []).map((item) => ({
|
||||
...item,
|
||||
statusText: item.status === 0 ? '草稿' : item.status === 1 ? '报名中' : item.status === 4 ? '已结束' : '其他',
|
||||
statusTagType: item.status === 1 ? 'success' : item.status === 4 ? 'info' : 'warning',
|
||||
publishText: item.is_published ? '已发布' : '未发布',
|
||||
publishTagType: item.is_published ? 'success' : 'info',
|
||||
requireAuditText: item.require_audit ? '需审核' : '免审核'
|
||||
}))
|
||||
allActivities.value = (Array.isArray(data) ? data : []).map(mapActivity)
|
||||
applyFilters()
|
||||
} catch (error) {
|
||||
ElMessage.error(error.message)
|
||||
@ -84,8 +93,8 @@ async function loadActivities() {
|
||||
|
||||
function applyFilters() {
|
||||
activities.value = allActivities.value.filter((item) => {
|
||||
const keywordPassed = !filters.keyword || item.title.toLowerCase().includes(filters.keyword.toLowerCase())
|
||||
const categoryPassed = filters.category === '' || item.category === filters.category
|
||||
const keywordPassed = !filters.keyword || (item.title || '').toLowerCase().includes(filters.keyword.toLowerCase())
|
||||
const categoryPassed = !filters.category || item.category === filters.category
|
||||
const publishPassed = filters.publishState === '' || String(item.is_published) === String(filters.publishState)
|
||||
return keywordPassed && categoryPassed && publishPassed
|
||||
})
|
||||
@ -127,7 +136,7 @@ async function createActivity() {
|
||||
async function togglePublish(row) {
|
||||
try {
|
||||
await apiRequest(`/admin/activities/${row.id}/publish`, { method: 'PUT' })
|
||||
ElMessage.success(row.is_published ? '已下架' : '已发布')
|
||||
ElMessage.success(row.is_published ? '活动已下架' : '活动已发布')
|
||||
await loadActivities()
|
||||
} catch (error) {
|
||||
ElMessage.error(error.message)
|
||||
@ -136,7 +145,7 @@ async function togglePublish(row) {
|
||||
|
||||
async function deleteActivity(row) {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确认删除活动「${row.title}」吗?`, '删除活动', {
|
||||
await ElMessageBox.confirm(`确认删除活动《${row.title}》吗?`, '删除活动', {
|
||||
confirmButtonText: '删除',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
@ -152,6 +161,23 @@ async function deleteActivity(row) {
|
||||
}
|
||||
}
|
||||
|
||||
function mapRegistration(item) {
|
||||
const user = item.user || {}
|
||||
return {
|
||||
...item,
|
||||
statusText: item.status === 1 ? '已报名' : item.status === 2 ? '已确认' : item.status === 3 ? '已取消' : item.status === 4 ? '已签到' : '其他',
|
||||
user: {
|
||||
...user,
|
||||
nicknameText: user.nickname || '未填写昵称',
|
||||
realNameText: user.real_name || '未填写',
|
||||
genderText: user.gender === 1 ? '男' : user.gender === 2 ? '女' : '未填写',
|
||||
cityText: user.city || '未填写',
|
||||
educationText: user.education || '未填写',
|
||||
companyText: user.job_company || '未填写'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function openRegistrations(row) {
|
||||
registrationVisible.value = true
|
||||
registrationLoading.value = true
|
||||
@ -159,21 +185,8 @@ async function openRegistrations(row) {
|
||||
const data = await apiRequest(`/admin/activities/${row.id}/registrations`)
|
||||
registrationDetail.value = {
|
||||
...data,
|
||||
registrations: (data.registrations || []).map((item) => ({
|
||||
...item,
|
||||
statusText: item.status === 1 ? '已报名' : item.status === 2 ? '已确认' : item.status === 3 ? '已取消' : item.status === 4 ? '已签到' : '其他',
|
||||
user: {
|
||||
...item.user,
|
||||
nicknameText: item.user.nickname || '未填写昵称',
|
||||
realNameText: item.user.real_name || '未填写',
|
||||
genderText: item.user.gender === 1 ? '男' : item.user.gender === 2 ? '女' : '未填写',
|
||||
cityText: item.user.city || '未填写',
|
||||
educationText: item.user.education || '未填写',
|
||||
companyText: item.user.job_company || '未填写'
|
||||
}
|
||||
}))
|
||||
registrations: (data.registrations || []).map(mapRegistration)
|
||||
}
|
||||
applyRegistrationFilters()
|
||||
} catch (error) {
|
||||
ElMessage.error(error.message)
|
||||
} finally {
|
||||
@ -181,14 +194,14 @@ async function openRegistrations(row) {
|
||||
}
|
||||
}
|
||||
|
||||
function applyRegistrationFilters() {
|
||||
const registrations = registrationDetail.value ? registrationDetail.value.registrations : []
|
||||
filteredRegistrations.value = registrations.filter((item) => {
|
||||
const filteredRegistrations = computed(() => {
|
||||
const items = registrationDetail.value?.registrations || []
|
||||
return items.filter((item) => {
|
||||
const genderPassed = registrationFilters.gender === '' || String(item.user.gender) === String(registrationFilters.gender)
|
||||
const statusPassed = registrationFilters.status === '' || String(item.status) === String(registrationFilters.status)
|
||||
return genderPassed && statusPassed
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
function exportRegistrations() {
|
||||
if (!filteredRegistrations.value.length) {
|
||||
@ -260,26 +273,66 @@ async function updateActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
async function copyShareLink(row) {
|
||||
const value = row.share_link || row.share_path || row.share_token
|
||||
async function writeClipboard(value, successMessage, emptyMessage) {
|
||||
if (!value) {
|
||||
ElMessage.warning('当前活动暂无分享链接')
|
||||
ElMessage.warning(emptyMessage)
|
||||
return
|
||||
}
|
||||
try {
|
||||
await navigator.clipboard.writeText(value)
|
||||
ElMessage.success('分享链接已复制')
|
||||
ElMessage.success(successMessage)
|
||||
} catch (error) {
|
||||
ElMessage.error('复制失败,请手动复制')
|
||||
}
|
||||
}
|
||||
|
||||
function withAbsoluteUrl(url) {
|
||||
if (!url) {
|
||||
return ''
|
||||
}
|
||||
if (/^https?:\/\//i.test(url)) {
|
||||
return url
|
||||
}
|
||||
const apiBase = import.meta.env.VITE_API_BASE_URL || 'https://ghxiangqin.com/api/v1'
|
||||
const base = apiBase.replace(/\/api\/v1\/?$/, '')
|
||||
return url.startsWith('/') ? `${base}${url}` : `${base}/${url}`
|
||||
}
|
||||
|
||||
async function copyShareLink(row) {
|
||||
await writeClipboard(row.share_link || row.share_path || row.share_token, '分享页链接已复制', '当前活动暂无分享页链接')
|
||||
}
|
||||
|
||||
async function copyShareApi(row) {
|
||||
await writeClipboard(row.share_api_url || row.share_token, '接口链接已复制', '当前活动暂无接口链接')
|
||||
}
|
||||
|
||||
async function copyUrlLink(row) {
|
||||
await writeClipboard(
|
||||
row.share_url_link,
|
||||
'微信 URL Link 已复制',
|
||||
row.share_url_link_error || '当前活动暂无微信 URL Link,请确认小程序主体和发布环境可用'
|
||||
)
|
||||
}
|
||||
|
||||
async function refreshShareAssets(row) {
|
||||
try {
|
||||
await apiRequest(`/admin/activities/${row.id}/refresh-share-assets`, {
|
||||
method: 'POST'
|
||||
})
|
||||
ElMessage.success('分享资源已重新生成')
|
||||
await loadActivities()
|
||||
} catch (error) {
|
||||
ElMessage.error(error.message)
|
||||
}
|
||||
}
|
||||
|
||||
function previewQrCode(row) {
|
||||
if (!row.share_qr_url) {
|
||||
const url = withAbsoluteUrl(row.share_qr_url)
|
||||
if (!url) {
|
||||
ElMessage.warning('当前活动暂无二维码')
|
||||
return
|
||||
}
|
||||
window.open(row.share_qr_url, '_blank')
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
|
||||
function goCreatePage() {
|
||||
@ -358,6 +411,11 @@ onMounted(loadActivities)
|
||||
<el-table-column prop="match_deadline" label="匹配截止" min-width="180" />
|
||||
<el-table-column prop="selection_limit" label="上限" width="90" />
|
||||
<el-table-column prop="share_token" label="分享标识" min-width="140" />
|
||||
<el-table-column label="URL Link" width="110">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.shareUrlLinkStatusType">{{ scope.row.shareUrlLinkStatusText }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="110">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.statusTagType">{{ scope.row.statusText }}</el-tag>
|
||||
@ -366,23 +424,27 @@ onMounted(loadActivities)
|
||||
<el-table-column prop="requireAuditText" label="报名要求" width="110" />
|
||||
<el-table-column label="发布状态" width="120">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.publishTagType">
|
||||
{{ scope.row.publishText }}
|
||||
</el-tag>
|
||||
<el-tag :type="scope.row.publishTagType">{{ scope.row.publishText }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="320" fixed="right">
|
||||
<el-table-column label="操作" width="700" fixed="right">
|
||||
<template #default="scope">
|
||||
<div class="row-actions">
|
||||
<el-button size="small" @click="openRegistrations(scope.row)">报名详情</el-button>
|
||||
<el-button size="small" @click="copyShareLink(scope.row)">复制分享</el-button>
|
||||
<el-button size="small" @click="copyShareLink(scope.row)">复制分享页</el-button>
|
||||
<el-button size="small" type="primary" plain @click="copyUrlLink(scope.row)">复制 URL Link</el-button>
|
||||
<el-button size="small" @click="copyShareApi(scope.row)">复制接口</el-button>
|
||||
<el-button size="small" @click="previewQrCode(scope.row)">查看二维码</el-button>
|
||||
<el-button size="small" @click="refreshShareAssets(scope.row)">重新生成分享</el-button>
|
||||
<el-button size="small" @click="openEdit(scope.row)">编辑</el-button>
|
||||
<el-button size="small" @click="togglePublish(scope.row)">
|
||||
{{ scope.row.is_published ? '下架' : '发布' }}
|
||||
</el-button>
|
||||
<el-button size="small" type="danger" plain @click="deleteActivity(scope.row)">删除</el-button>
|
||||
</div>
|
||||
<div v-if="scope.row.share_url_link_error" class="share-error-text">
|
||||
URL Link 错误:{{ scope.row.share_url_link_error }}
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@ -443,11 +505,11 @@ onMounted(loadActivities)
|
||||
|
||||
<div class="registration-toolbar">
|
||||
<div class="filter-inline">
|
||||
<el-select v-model="registrationFilters.gender" placeholder="性别筛选" clearable @change="applyRegistrationFilters">
|
||||
<el-select v-model="registrationFilters.gender" placeholder="性别筛选" clearable>
|
||||
<el-option label="男" :value="1" />
|
||||
<el-option label="女" :value="2" />
|
||||
</el-select>
|
||||
<el-select v-model="registrationFilters.status" placeholder="状态筛选" clearable @change="applyRegistrationFilters">
|
||||
<el-select v-model="registrationFilters.status" placeholder="状态筛选" clearable>
|
||||
<el-option label="已报名" :value="1" />
|
||||
<el-option label="已确认" :value="2" />
|
||||
<el-option label="已取消" :value="3" />
|
||||
@ -614,6 +676,14 @@ onMounted(loadActivities)
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.share-error-text {
|
||||
margin-top: 8px;
|
||||
color: #b91c1c;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.dialog-form {
|
||||
padding: 0;
|
||||
box-shadow: none;
|
||||
|
||||
@ -0,0 +1,25 @@
|
||||
"""add activity share url link
|
||||
|
||||
Revision ID: 202605180001
|
||||
Revises: 202605160002
|
||||
Create Date: 2026-05-18 10:00:00
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision = "202605180001"
|
||||
down_revision = "202605160002"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column("activities", sa.Column("share_url_link", sa.String(length=1000), nullable=True))
|
||||
op.add_column("activities", sa.Column("share_url_link_error", sa.String(length=500), nullable=True))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column("activities", "share_url_link_error")
|
||||
op.drop_column("activities", "share_url_link")
|
||||
@ -27,8 +27,10 @@ class Settings(BaseSettings):
|
||||
wx_appid: str = ""
|
||||
wx_secret: str = ""
|
||||
wx_miniprogram_page_activity_detail: str = "pages/activity-detail/activity-detail"
|
||||
wx_miniprogram_env_version: str = "release"
|
||||
wx_template_audit_result: str = ""
|
||||
wx_template_match_success: str = ""
|
||||
public_site_url: str = ""
|
||||
|
||||
cos_region: str = ""
|
||||
cos_secret_id: str = ""
|
||||
|
||||
@ -3,11 +3,12 @@ from pathlib import Path
|
||||
from fastapi import FastAPI, HTTPException, Request
|
||||
from fastapi.exceptions import RequestValidationError
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi.responses import HTMLResponse, JSONResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
from app.config import settings
|
||||
from app.routers import api_router
|
||||
from app.routers.activities import activity_share_landing
|
||||
|
||||
|
||||
def create_app() -> FastAPI:
|
||||
@ -85,6 +86,13 @@ def create_app() -> FastAPI:
|
||||
static_dir.mkdir(parents=True, exist_ok=True)
|
||||
app.mount("/static", StaticFiles(directory=static_dir), name="static")
|
||||
|
||||
app.add_api_route(
|
||||
"/share/activity/{share_token}",
|
||||
activity_share_landing,
|
||||
methods=["GET"],
|
||||
response_class=HTMLResponse,
|
||||
include_in_schema=False,
|
||||
)
|
||||
app.include_router(api_router, prefix=settings.api_v1_prefix)
|
||||
return app
|
||||
|
||||
|
||||
@ -23,6 +23,8 @@ class Activity(TimestampMixin, Base):
|
||||
selection_limit: Mapped[int] = mapped_column(SmallInteger, default=3, nullable=False)
|
||||
share_token: Mapped[str | None] = mapped_column(String(64), unique=True)
|
||||
share_qr_url: Mapped[str | None] = mapped_column(String(500))
|
||||
share_url_link: Mapped[str | None] = mapped_column(String(1000))
|
||||
share_url_link_error: Mapped[str | None] = mapped_column(String(500))
|
||||
status: Mapped[int] = mapped_column(SmallInteger, default=0, nullable=False)
|
||||
is_published: Mapped[int] = mapped_column(SmallInteger, default=0, nullable=False)
|
||||
require_audit: Mapped[int] = mapped_column(SmallInteger, default=1, nullable=False)
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from fastapi.responses import HTMLResponse
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database import get_db
|
||||
@ -14,6 +15,9 @@ from app.schemas.activity_match import (
|
||||
)
|
||||
from app.services.activity_match_service import (
|
||||
bind_activity_user,
|
||||
build_activity_share_link,
|
||||
build_activity_share_path,
|
||||
build_activity_url_link,
|
||||
build_activity_share_info,
|
||||
cancel_activity_choice,
|
||||
create_activity_choice,
|
||||
@ -87,6 +91,66 @@ async def get_activity_by_share(
|
||||
return {"code": 0, "message": "ok", "data": data}
|
||||
|
||||
|
||||
@router.get("/share/{share_token}/landing", response_class=HTMLResponse, include_in_schema=False)
|
||||
async def activity_share_landing(
|
||||
share_token: str,
|
||||
session: AsyncSession = Depends(get_db),
|
||||
) -> HTMLResponse:
|
||||
activity = await get_activity_by_share_token(session, share_token)
|
||||
if activity is None or activity.is_published != 1:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Activity not found")
|
||||
|
||||
share_path = await build_activity_share_path(session, activity)
|
||||
url_link = await build_activity_url_link(session, activity)
|
||||
title = activity.title or "活动报名"
|
||||
description = activity.description or "点击打开小程序查看活动详情并报名"
|
||||
cover_image = activity.cover_image or ""
|
||||
safe_title = title.replace("&", "&").replace("<", "<").replace(">", ">")
|
||||
safe_description = description.replace("&", "&").replace("<", "<").replace(">", ">").replace("\n", "<br>")
|
||||
open_hint = "请点击右上角,选择“打开小程序”或使用活动二维码进入。"
|
||||
html = f"""<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||
<title>{safe_title}</title>
|
||||
<style>
|
||||
body {{ margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: linear-gradient(180deg, #f7f1e8 0%, #fffdf8 100%); color: #1f2937; }}
|
||||
.wrap {{ max-width: 720px; margin: 0 auto; padding: 28px 20px 40px; }}
|
||||
.card {{ background: rgba(255,255,255,0.92); border-radius: 24px; overflow: hidden; box-shadow: 0 18px 50px rgba(15,23,42,0.12); }}
|
||||
.cover {{ width: 100%; aspect-ratio: 16 / 9; object-fit: cover; background: #e5e7eb; display: block; }}
|
||||
.body {{ padding: 24px; }}
|
||||
h1 {{ margin: 0 0 12px; font-size: 28px; line-height: 1.2; }}
|
||||
p {{ margin: 0; color: #4b5563; line-height: 1.7; }}
|
||||
.actions {{ display: flex; flex-wrap: wrap; gap: 12px; margin-top: 24px; }}
|
||||
.btn {{ display: inline-flex; align-items: center; justify-content: center; padding: 14px 18px; border-radius: 999px; text-decoration: none; font-weight: 600; }}
|
||||
.btn-primary {{ background: #111827; color: #fff; }}
|
||||
.btn-secondary {{ background: #f3f4f6; color: #111827; }}
|
||||
.hint {{ margin-top: 18px; font-size: 14px; color: #6b7280; }}
|
||||
.path {{ margin-top: 14px; padding: 12px 14px; border-radius: 14px; background: #f8fafc; font-size: 13px; color: #475569; word-break: break-all; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<div class="card">
|
||||
{"<img class='cover' src='" + cover_image + "' alt='活动封面'>" if cover_image else "<div class='cover'></div>"}
|
||||
<div class="body">
|
||||
<h1>{safe_title}</h1>
|
||||
<p>{safe_description}</p>
|
||||
<div class="actions">
|
||||
<a class="btn btn-primary" href="{url_link or 'weixin://'}">打开小程序</a>
|
||||
<a class="btn btn-secondary" href="/static/uploads/activities/{activity.id}/share_qrcode.png" target="_blank" rel="noreferrer">查看活动二维码</a>
|
||||
</div>
|
||||
<div class="hint">{open_hint}</div>
|
||||
<div class="path">小程序页面路径:{share_path}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>"""
|
||||
return HTMLResponse(content=html)
|
||||
|
||||
|
||||
@router.get("/{activity_id}")
|
||||
async def get_activity(
|
||||
activity_id: int,
|
||||
|
||||
@ -1,14 +1,18 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.config import settings
|
||||
from app.database import get_db
|
||||
from app.dependencies import get_current_admin
|
||||
from app.schemas.activity import AdminActivityUpsertRequest
|
||||
from app.services.activity_match_service import (
|
||||
build_activity_share_api_url,
|
||||
build_activity_share_link,
|
||||
build_activity_share_path,
|
||||
build_activity_url_link,
|
||||
ensure_activity_share_qrcode,
|
||||
ensure_activity_share_token,
|
||||
refresh_activity_share_assets,
|
||||
)
|
||||
from app.services.admin_service import get_system_config_value
|
||||
from app.services.activity_service import (
|
||||
@ -32,6 +36,7 @@ async def get_activities(
|
||||
_ = admin_payload
|
||||
activities = await list_admin_activities(session)
|
||||
api_base_url = await get_system_config_value(session, "api_base_url", "")
|
||||
public_site_url = settings.public_site_url or api_base_url.replace("/api/v1", "").rstrip("/")
|
||||
data = []
|
||||
for item in activities:
|
||||
item = await ensure_activity_share_token(session, item)
|
||||
@ -58,12 +63,44 @@ async def get_activities(
|
||||
"share_token": item.share_token,
|
||||
"share_qr_url": item.share_qr_url,
|
||||
"share_path": await build_activity_share_path(session, item),
|
||||
"share_link": await build_activity_share_link(session, item, api_base_url=api_base_url),
|
||||
"share_link": await build_activity_share_link(session, item, public_site_url=public_site_url),
|
||||
"share_api_url": await build_activity_share_api_url(session, item, api_base_url=api_base_url),
|
||||
"share_url_link": await build_activity_url_link(session, item),
|
||||
"share_url_link_error": item.share_url_link_error,
|
||||
}
|
||||
)
|
||||
return {"code": 0, "message": "ok", "data": data}
|
||||
|
||||
|
||||
@router.post("/{activity_id}/refresh-share-assets")
|
||||
async def refresh_share_assets(
|
||||
activity_id: int,
|
||||
admin_payload: dict = Depends(get_current_admin),
|
||||
session: AsyncSession = Depends(get_db),
|
||||
) -> dict:
|
||||
_ = admin_payload
|
||||
activity = await get_activity_by_id(session, activity_id)
|
||||
if activity is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Activity not found")
|
||||
|
||||
activity = await refresh_activity_share_assets(session, activity)
|
||||
api_base_url = await get_system_config_value(session, "api_base_url", "")
|
||||
public_site_url = settings.public_site_url or api_base_url.replace("/api/v1", "").rstrip("/")
|
||||
return {
|
||||
"code": 0,
|
||||
"message": "ok",
|
||||
"data": {
|
||||
"id": activity.id,
|
||||
"share_qr_url": activity.share_qr_url,
|
||||
"share_path": await build_activity_share_path(session, activity),
|
||||
"share_link": await build_activity_share_link(session, activity, public_site_url=public_site_url),
|
||||
"share_api_url": await build_activity_share_api_url(session, activity, api_base_url=api_base_url),
|
||||
"share_url_link": activity.share_url_link,
|
||||
"share_url_link_error": activity.share_url_link_error,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@router.post("")
|
||||
async def create(
|
||||
payload: AdminActivityUpsertRequest,
|
||||
|
||||
@ -6,6 +6,7 @@ from app.dependencies import get_current_admin
|
||||
from app.schemas.admin import AdminAuditRequest, AdminBanRequest
|
||||
from app.services.notify_service import send_audit_result
|
||||
from app.services.admin_service import audit_user, ban_user, get_user_detail, list_users
|
||||
from app.utils.media import sanitize_user_media_payload
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@ -28,7 +29,7 @@ async def get_pending_users(
|
||||
only_pending=only_pending,
|
||||
)
|
||||
data = [
|
||||
{
|
||||
sanitize_user_media_payload({
|
||||
"id": user.id,
|
||||
"nickname": user.nickname,
|
||||
"real_name": user.real_name,
|
||||
@ -39,7 +40,7 @@ async def get_pending_users(
|
||||
"audit_status": user.audit_status,
|
||||
"updated_at": user.updated_at.isoformat() if user.updated_at else None,
|
||||
"self_intro": user.self_intro,
|
||||
}
|
||||
})
|
||||
for user in users
|
||||
]
|
||||
return {"code": 0, "message": "ok", "data": data}
|
||||
@ -91,7 +92,7 @@ async def get_user(
|
||||
if user is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
|
||||
|
||||
data = {
|
||||
data = sanitize_user_media_payload({
|
||||
"id": user.id,
|
||||
"nickname": user.nickname,
|
||||
"real_name": user.real_name,
|
||||
@ -110,7 +111,7 @@ async def get_user(
|
||||
"audit_remark": user.audit_remark,
|
||||
"is_active": user.is_active,
|
||||
"ban_reason": user.ban_reason,
|
||||
}
|
||||
})
|
||||
return {"code": 0, "message": "ok", "data": data}
|
||||
|
||||
|
||||
|
||||
@ -6,6 +6,7 @@ from app.dependencies import get_current_user_id
|
||||
from app.schemas.user import SubmitAuditResponse, UserProfileResponse, UserUpdateRequest
|
||||
from app.services.match_service import get_public_user_info
|
||||
from app.services.user_service import get_user_by_id, submit_user_audit, update_user_profile
|
||||
from app.utils.media import sanitize_user_media_payload
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@ -19,7 +20,7 @@ async def get_me(
|
||||
if user is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
|
||||
|
||||
data = UserProfileResponse.model_validate(user).model_dump()
|
||||
data = sanitize_user_media_payload(UserProfileResponse.model_validate(user).model_dump())
|
||||
return {"code": 0, "message": "ok", "data": data}
|
||||
|
||||
|
||||
@ -38,7 +39,7 @@ async def update_me(
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
|
||||
|
||||
data = UserProfileResponse.model_validate(updated_user).model_dump()
|
||||
data = sanitize_user_media_payload(UserProfileResponse.model_validate(updated_user).model_dump())
|
||||
return {"code": 0, "message": "ok", "data": data}
|
||||
|
||||
|
||||
|
||||
@ -14,7 +14,8 @@ from app.models.registration import Registration
|
||||
from app.models.user import User
|
||||
from app.config import settings
|
||||
from app.services.match_service import _level3_info
|
||||
from app.utils.wx_api import generate_unlimited_qrcode
|
||||
from app.utils.media import sanitize_user_media_payload
|
||||
from app.utils.wx_api import generate_unlimited_qrcode, generate_url_link
|
||||
|
||||
|
||||
ACTIVE_REGISTRATION_STATUSES = {1, 2, 4}
|
||||
@ -63,6 +64,34 @@ def _list_item(user: User, *, selected_at: datetime | None, matched_at: datetime
|
||||
}
|
||||
|
||||
|
||||
def _candidate_item(user: User, selected: bool = False) -> dict:
|
||||
return sanitize_user_media_payload({
|
||||
"user_id": user.id,
|
||||
"nickname": user.nickname,
|
||||
"birth_year_range": _birth_year_range(user.birth_year),
|
||||
"city": user.city,
|
||||
"personality_tags": user.personality_tags if isinstance(user.personality_tags, list) else [],
|
||||
"avatar_blur_url": user.avatar_blur_url,
|
||||
"selected": selected,
|
||||
})
|
||||
|
||||
|
||||
def _list_item(user: User, *, selected_at: datetime | None, matched_at: datetime | None, matched: bool) -> dict:
|
||||
return sanitize_user_media_payload({
|
||||
"user_id": user.id,
|
||||
"nickname": user.nickname,
|
||||
"avatar_url": user.avatar_url,
|
||||
"city": user.city,
|
||||
"education": user.education,
|
||||
"hobbies": user.hobbies if isinstance(user.hobbies, list) else [],
|
||||
"selected_at": _format_datetime(selected_at),
|
||||
"matched_at": _format_datetime(matched_at),
|
||||
"match_status": "success" if matched else "pending",
|
||||
"match_status_text": "ƥ<EFBFBD><EFBFBD>ɹ<EFBFBD>" if matched else "<EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ӧ",
|
||||
"can_view_detail": matched,
|
||||
})
|
||||
|
||||
|
||||
async def get_activity_by_id(session: AsyncSession, activity_id: int) -> Activity | None:
|
||||
result = await session.execute(select(Activity).where(Activity.id == activity_id))
|
||||
return result.scalar_one_or_none()
|
||||
@ -502,7 +531,7 @@ async def build_activity_share_path(session: AsyncSession, activity: Activity) -
|
||||
return f"pages/activity-detail/activity-detail?share_token={activity.share_token}"
|
||||
|
||||
|
||||
async def build_activity_share_link(session: AsyncSession, activity: Activity, api_base_url: str | None = None) -> str:
|
||||
async def build_activity_share_api_url(session: AsyncSession, activity: Activity, api_base_url: str | None = None) -> str:
|
||||
activity = await ensure_activity_share_token(session, activity)
|
||||
base_url = (api_base_url or "").rstrip("/")
|
||||
if base_url:
|
||||
@ -510,6 +539,42 @@ async def build_activity_share_link(session: AsyncSession, activity: Activity, a
|
||||
return activity.share_token or ""
|
||||
|
||||
|
||||
async def build_activity_share_link(session: AsyncSession, activity: Activity, public_site_url: str | None = None) -> str:
|
||||
activity = await ensure_activity_share_token(session, activity)
|
||||
base_url = (public_site_url or "").rstrip("/")
|
||||
if base_url:
|
||||
return f"{base_url}/share/activity/{activity.share_token}"
|
||||
return f"/share/activity/{activity.share_token}"
|
||||
|
||||
|
||||
async def build_activity_url_link(session: AsyncSession, activity: Activity) -> str | None:
|
||||
activity = await ensure_activity_share_token(session, activity)
|
||||
page = settings.wx_miniprogram_page_activity_detail or "pages/activity-detail/activity-detail"
|
||||
if activity.share_url_link:
|
||||
return activity.share_url_link
|
||||
|
||||
url_link, error_message = await generate_url_link(path=page, query=f"share_token={activity.share_token}")
|
||||
activity.share_url_link = url_link
|
||||
activity.share_url_link_error = error_message
|
||||
session.add(activity)
|
||||
await session.commit()
|
||||
await session.refresh(activity)
|
||||
return activity.share_url_link
|
||||
|
||||
|
||||
async def refresh_activity_share_assets(session: AsyncSession, activity: Activity) -> Activity:
|
||||
activity.share_qr_url = None
|
||||
activity.share_url_link = None
|
||||
activity.share_url_link_error = None
|
||||
session.add(activity)
|
||||
await session.commit()
|
||||
await session.refresh(activity)
|
||||
activity = await ensure_activity_share_qrcode(session, activity)
|
||||
await build_activity_url_link(session, activity)
|
||||
await session.refresh(activity)
|
||||
return activity
|
||||
|
||||
|
||||
async def get_bound_activity_by_id(session: AsyncSession, activity_id: int, current_user: User) -> Activity:
|
||||
activity = await get_activity_by_id(session, activity_id)
|
||||
if activity is None:
|
||||
|
||||
@ -16,6 +16,7 @@ from app.services.activity_match_service import (
|
||||
ensure_activity_share_token,
|
||||
resolve_activity_stage,
|
||||
)
|
||||
from app.utils.media import sanitize_media_url
|
||||
|
||||
|
||||
async def get_activity_by_id(session: AsyncSession, activity_id: int) -> Activity | None:
|
||||
@ -69,7 +70,7 @@ async def _registered_preview(
|
||||
{
|
||||
"user_id": row.id,
|
||||
"nickname": row.nickname,
|
||||
"avatar_blur_url": row.avatar_blur_url,
|
||||
"avatar_blur_url": sanitize_media_url(row.avatar_blur_url),
|
||||
}
|
||||
for row in rows
|
||||
]
|
||||
|
||||
@ -11,6 +11,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from app.models.match import Match, MatchLike
|
||||
from app.models.user import User
|
||||
from app.services.admin_service import get_system_config_value
|
||||
from app.utils.media import sanitize_user_media_payload
|
||||
|
||||
|
||||
WEIGHTS = {
|
||||
@ -34,7 +35,7 @@ def _birth_year_range(birth_year: int | None) -> str | None:
|
||||
|
||||
def _level1_info(user: User, *, score: float | None = None, reasons: list[str] | None = None) -> dict:
|
||||
personality_tags = _safe_personality_tags(user.personality_tags)
|
||||
return {
|
||||
return sanitize_user_media_payload({
|
||||
"user_id": user.id,
|
||||
"nickname": user.nickname,
|
||||
"birth_year_range": _birth_year_range(user.birth_year),
|
||||
@ -43,21 +44,21 @@ def _level1_info(user: User, *, score: float | None = None, reasons: list[str] |
|
||||
"avatar_blur_url": user.avatar_blur_url,
|
||||
"match_score": score,
|
||||
"match_reasons": reasons or [],
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
def _level2_info(user: User) -> dict:
|
||||
return {
|
||||
return sanitize_user_media_payload({
|
||||
"nickname": user.nickname,
|
||||
"avatar_url": user.avatar_url,
|
||||
"profile_images": user.profile_images if isinstance(user.profile_images, list) else [],
|
||||
"education": user.education,
|
||||
"hobbies": list(_safe_hobbies(user.hobbies)),
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
def _level3_info(user: User) -> dict:
|
||||
return {
|
||||
return sanitize_user_media_payload({
|
||||
"user_id": user.id,
|
||||
"nickname": user.nickname,
|
||||
"avatar_url": user.avatar_url,
|
||||
@ -69,7 +70,7 @@ def _level3_info(user: User) -> dict:
|
||||
"job_company": user.job_company,
|
||||
"self_intro": user.self_intro,
|
||||
"income_range": user.income_range,
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
def _age_score(user_a: User, user_b: User) -> float:
|
||||
@ -397,14 +398,14 @@ async def list_my_matches(session: AsyncSession, current_user: User) -> list[dic
|
||||
"match_status": "success",
|
||||
"match_status_text": "匹配成功",
|
||||
"fail_reason": None,
|
||||
"other_user": {
|
||||
"other_user": sanitize_user_media_payload({
|
||||
"user_id": joined_user.id,
|
||||
"nickname": joined_user.nickname,
|
||||
"avatar_url": joined_user.avatar_url,
|
||||
"education": joined_user.education,
|
||||
"city": joined_user.city,
|
||||
"hobbies": joined_user.hobbies,
|
||||
},
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
||||
@ -419,14 +420,14 @@ async def list_my_matches(session: AsyncSession, current_user: User) -> list[dic
|
||||
"match_status": "success" if reverse_like else "pending",
|
||||
"match_status_text": "匹配成功" if reverse_like else "待回应",
|
||||
"fail_reason": None if reverse_like else "等待对方回应",
|
||||
"other_user": {
|
||||
"other_user": sanitize_user_media_payload({
|
||||
"user_id": joined_user.id,
|
||||
"nickname": joined_user.nickname,
|
||||
"avatar_url": joined_user.avatar_url,
|
||||
"education": joined_user.education,
|
||||
"city": joined_user.city,
|
||||
"hobbies": joined_user.hobbies,
|
||||
},
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
55
backend/app/utils/media.py
Normal file
55
backend/app/utils/media.py
Normal file
@ -0,0 +1,55 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
STATIC_ROOT = Path(__file__).resolve().parent.parent / "static"
|
||||
|
||||
|
||||
def _local_static_file_exists(url: str) -> bool:
|
||||
if not url.startswith("/static/"):
|
||||
return True
|
||||
relative_path = url.removeprefix("/static/")
|
||||
file_path = STATIC_ROOT / Path(relative_path)
|
||||
return file_path.is_file()
|
||||
|
||||
|
||||
def sanitize_media_url(url: str | None) -> str | None:
|
||||
if not url:
|
||||
return None
|
||||
if url.startswith("/static/") and not _local_static_file_exists(url):
|
||||
return None
|
||||
return url
|
||||
|
||||
|
||||
def sanitize_media_list(urls: list[str] | None) -> list[str]:
|
||||
if not isinstance(urls, list):
|
||||
return []
|
||||
items: list[str] = []
|
||||
for item in urls:
|
||||
if not isinstance(item, str):
|
||||
continue
|
||||
safe_url = sanitize_media_url(item)
|
||||
if safe_url:
|
||||
items.append(safe_url)
|
||||
return items
|
||||
|
||||
|
||||
def sanitize_user_media_payload(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
data = dict(payload)
|
||||
avatar_url = sanitize_media_url(data.get("avatar_url"))
|
||||
avatar_blur_url = sanitize_media_url(data.get("avatar_blur_url"))
|
||||
profile_images = sanitize_media_list(data.get("profile_images"))
|
||||
|
||||
if not avatar_url and profile_images:
|
||||
avatar_url = profile_images[0]
|
||||
if not avatar_blur_url:
|
||||
avatar_blur_url = avatar_url
|
||||
if avatar_url and not profile_images:
|
||||
profile_images = [avatar_url]
|
||||
|
||||
data["avatar_url"] = avatar_url
|
||||
data["avatar_blur_url"] = avatar_blur_url
|
||||
data["profile_images"] = profile_images
|
||||
return data
|
||||
@ -44,7 +44,7 @@ async def generate_unlimited_qrcode(*, page: str, scene: str) -> bytes | None:
|
||||
"page": page,
|
||||
"scene": scene,
|
||||
"check_path": False,
|
||||
"env_version": "develop",
|
||||
"env_version": settings.wx_miniprogram_env_version or "release",
|
||||
}
|
||||
async with httpx.AsyncClient(timeout=20.0) as client:
|
||||
response = await client.post(url, json=payload)
|
||||
@ -54,3 +54,28 @@ async def generate_unlimited_qrcode(*, page: str, scene: str) -> bytes | None:
|
||||
if data.get("errcode"):
|
||||
return None
|
||||
return response.content
|
||||
|
||||
|
||||
async def generate_url_link(*, path: str, query: str = "", env_version: str | None = None) -> tuple[str | None, str | None]:
|
||||
if not settings.wx_appid or not settings.wx_secret:
|
||||
return None, "微信小程序 AppID / Secret 未配置"
|
||||
if settings.wx_appid.startswith("your_") or settings.wx_secret.startswith("your_"):
|
||||
return None, "微信小程序 AppID / Secret 仍为占位配置"
|
||||
|
||||
access_token = await get_access_token()
|
||||
if not access_token:
|
||||
return None, "微信 access_token 获取失败"
|
||||
|
||||
url = f"https://api.weixin.qq.com/wxa/generate_urllink?access_token={access_token}"
|
||||
payload = {
|
||||
"path": path,
|
||||
"query": query,
|
||||
"is_expire": False,
|
||||
"env_version": env_version or settings.wx_miniprogram_env_version or "release",
|
||||
}
|
||||
async with httpx.AsyncClient(timeout=20.0) as client:
|
||||
response = await client.post(url, json=payload)
|
||||
data = response.json()
|
||||
if data.get("errcode"):
|
||||
return None, data.get("errmsg") or f"微信接口错误:{data.get('errcode')}"
|
||||
return data.get("url_link"), None
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
const request = require('../../utils/request')
|
||||
const { resolveImageUrl } = require('../../utils/media')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
@ -64,6 +65,7 @@ Page({
|
||||
registered_preview: Array.isArray(activity.registered_preview)
|
||||
? activity.registered_preview.map((item) => ({
|
||||
...item,
|
||||
avatar_blur_url: resolveImageUrl(item.avatar_blur_url || ''),
|
||||
nicknameText: item.nickname || '用户'
|
||||
}))
|
||||
: []
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
const request = require('../../utils/request')
|
||||
const { resolveImageUrl, resolveImageList } = require('../../utils/media')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
@ -26,12 +27,14 @@ Page({
|
||||
url: `/activities/${this.data.activityId}/matches/${this.data.targetUserId}/detail`
|
||||
})
|
||||
const otherUser = detail.other_user || {}
|
||||
const profileImages = Array.isArray(otherUser.profile_images) ? otherUser.profile_images : []
|
||||
const profileImages = resolveImageList(otherUser.profile_images)
|
||||
this.setData({
|
||||
userInfo: {
|
||||
...otherUser,
|
||||
avatar_url: resolveImageUrl(otherUser.avatar_url || ''),
|
||||
avatar_blur_url: resolveImageUrl(otherUser.avatar_blur_url || ''),
|
||||
profileImages,
|
||||
previewImage: profileImages.length ? profileImages[0] : (otherUser.avatar_url || ''),
|
||||
previewImage: profileImages.length ? profileImages[0] : resolveImageUrl(otherUser.avatar_url || ''),
|
||||
nicknameText: otherUser.nickname || '匿名用户',
|
||||
cityText: otherUser.city || '城市未知',
|
||||
educationText: otherUser.education || '学历未知',
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
const request = require('../../utils/request')
|
||||
const { resolveImageUrl } = require('../../utils/media')
|
||||
|
||||
const GENDER_MAP = {
|
||||
1: 'male',
|
||||
@ -230,6 +231,7 @@ Page({
|
||||
const matchScoreLevel = hasMatchScore ? (safeMatchScore >= 80 ? 'high' : safeMatchScore >= 60 ? 'mid' : 'low') : 'none'
|
||||
return {
|
||||
...item,
|
||||
avatar_blur_url: resolveImageUrl(item.avatar_blur_url || ''),
|
||||
nicknameText: item.nickname || '匿名用户',
|
||||
birthYearRangeText: item.birth_year_range || '年龄未知',
|
||||
cityText: item.city || '城市未知',
|
||||
|
||||
@ -101,4 +101,4 @@
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@ -1,4 +1,5 @@
|
||||
const request = require('../../utils/request')
|
||||
const { resolveImageUrl } = require('../../utils/media')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
@ -36,7 +37,7 @@ Page({
|
||||
normalizeMatch(item) {
|
||||
const hobbies = Array.isArray(item.hobbies) ? item.hobbies : Array.isArray(item.other_user && item.other_user.hobbies) ? item.other_user.hobbies : []
|
||||
const nickname = item.nickname || (item.other_user && item.other_user.nickname) || ''
|
||||
const avatarUrl = item.avatar_url || (item.other_user && item.other_user.avatar_url) || ''
|
||||
const avatarUrl = resolveImageUrl(item.avatar_url || (item.other_user && item.other_user.avatar_url) || '')
|
||||
const city = item.city || (item.other_user && item.other_user.city) || ''
|
||||
const education = item.education || (item.other_user && item.other_user.education) || ''
|
||||
const statusMap = {
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
const request = require('../../utils/request')
|
||||
const { getApiBaseUrl } = require('../../utils/constants')
|
||||
const { resolveImageUrl, resolveImageList } = require('../../utils/media')
|
||||
|
||||
function addCacheVersion(url, version) {
|
||||
if (!url) {
|
||||
@ -9,36 +10,10 @@ function addCacheVersion(url, version) {
|
||||
return `${url}${joiner}v=${encodeURIComponent(version)}`
|
||||
}
|
||||
|
||||
function isLocalDevUrl(url) {
|
||||
return /^https?:\/\/(127\.0\.0\.1|localhost|0\.0\.0\.0)(:\d+)?\//i.test(url)
|
||||
}
|
||||
|
||||
function upgradeToHttpsIfNeeded(url) {
|
||||
if (/^http:\/\//i.test(url) && !isLocalDevUrl(url)) {
|
||||
return url.replace(/^http:\/\//i, 'https://')
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
function resolveImageUrl(url) {
|
||||
if (!url) {
|
||||
return ''
|
||||
}
|
||||
if (/^https?:\/\//i.test(url)) {
|
||||
return upgradeToHttpsIfNeeded(url)
|
||||
}
|
||||
|
||||
const baseUrl = getApiBaseUrl().replace(/\/api\/v1\/?$/, '')
|
||||
const resolved = url.startsWith('/') ? `${baseUrl}${url}` : `${baseUrl}/${url}`
|
||||
return upgradeToHttpsIfNeeded(resolved)
|
||||
}
|
||||
|
||||
function normalizeAvatarState(userInfo) {
|
||||
const avatarUrl = resolveImageUrl(userInfo.avatar_url || '')
|
||||
const avatarBlurUrl = resolveImageUrl(userInfo.avatar_blur_url || '')
|
||||
const profileImages = Array.isArray(userInfo.profile_images)
|
||||
? userInfo.profile_images.map((item) => resolveImageUrl(item)).filter(Boolean)
|
||||
: []
|
||||
const profileImages = resolveImageList(userInfo.profile_images)
|
||||
|
||||
return {
|
||||
...userInfo,
|
||||
@ -342,7 +317,7 @@ Page({
|
||||
},
|
||||
|
||||
uploadAvatar(tempFilePath) {
|
||||
wx.getFileInfo({
|
||||
wx.getFileSystemManager().getFileInfo({
|
||||
filePath: tempFilePath,
|
||||
success: (info) => {
|
||||
if (info.size > MAX_AVATAR_SIZE) {
|
||||
@ -402,7 +377,7 @@ Page({
|
||||
|
||||
uploadProfileImage(tempFilePath) {
|
||||
return new Promise((resolve) => {
|
||||
wx.getFileInfo({
|
||||
wx.getFileSystemManager().getFileInfo({
|
||||
filePath: tempFilePath,
|
||||
success: (info) => {
|
||||
if (info.size > MAX_AVATAR_SIZE) {
|
||||
@ -423,8 +398,7 @@ Page({
|
||||
if (body.code !== 0) {
|
||||
throw new Error(body.message || '上传失败')
|
||||
}
|
||||
const rawImages = Array.isArray(body.data.profile_images) ? body.data.profile_images : []
|
||||
const profileImages = rawImages.map((item) => resolveImageUrl(item))
|
||||
const profileImages = resolveImageList(body.data.profile_images)
|
||||
const form = { ...this.data.form, profile_images: profileImages }
|
||||
this.setData({ form, profileImages })
|
||||
resolve(true)
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
const request = require('../../utils/request')
|
||||
const { getApiBaseUrl } = require('../../utils/constants')
|
||||
const { resolveImageUrl, resolveImageList } = require('../../utils/media')
|
||||
|
||||
function addCacheVersion(url, version) {
|
||||
if (!url) {
|
||||
@ -9,34 +9,8 @@ function addCacheVersion(url, version) {
|
||||
return `${url}${joiner}v=${encodeURIComponent(version)}`
|
||||
}
|
||||
|
||||
function isLocalDevUrl(url) {
|
||||
return /^https?:\/\/(127\.0\.0\.1|localhost|0\.0\.0\.0)(:\d+)?\//i.test(url)
|
||||
}
|
||||
|
||||
function upgradeToHttpsIfNeeded(url) {
|
||||
if (/^http:\/\//i.test(url) && !isLocalDevUrl(url)) {
|
||||
return url.replace(/^http:\/\//i, 'https://')
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
function resolveImageUrl(url) {
|
||||
if (!url) {
|
||||
return ''
|
||||
}
|
||||
if (/^https?:\/\//i.test(url)) {
|
||||
return upgradeToHttpsIfNeeded(url)
|
||||
}
|
||||
|
||||
const baseUrl = getApiBaseUrl().replace(/\/api\/v1\/?$/, '')
|
||||
const resolved = url.startsWith('/') ? `${baseUrl}${url}` : `${baseUrl}/${url}`
|
||||
return upgradeToHttpsIfNeeded(resolved)
|
||||
}
|
||||
|
||||
function normalizeAvatarState(userInfo) {
|
||||
const profileImages = Array.isArray(userInfo.profile_images)
|
||||
? userInfo.profile_images.map((item) => resolveImageUrl(item)).filter(Boolean)
|
||||
: []
|
||||
const profileImages = resolveImageList(userInfo.profile_images)
|
||||
return {
|
||||
...userInfo,
|
||||
avatar_url: resolveImageUrl(userInfo.avatar_url || ''),
|
||||
|
||||
39
miniprogram/utils/media.js
Normal file
39
miniprogram/utils/media.js
Normal file
@ -0,0 +1,39 @@
|
||||
const { getApiBaseUrl } = require('./constants')
|
||||
|
||||
function isLocalDevUrl(url) {
|
||||
return /^https?:\/\/(127\.0\.0\.1|localhost|0\.0\.0\.0)(:\d+)?\//i.test(url)
|
||||
}
|
||||
|
||||
function upgradeToHttpsIfNeeded(url) {
|
||||
if (/^http:\/\//i.test(url) && !isLocalDevUrl(url)) {
|
||||
return url.replace(/^http:\/\//i, 'https://')
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
function resolveImageUrl(url) {
|
||||
if (!url) {
|
||||
return ''
|
||||
}
|
||||
if (/^https?:\/\//i.test(url)) {
|
||||
return upgradeToHttpsIfNeeded(url)
|
||||
}
|
||||
|
||||
const baseUrl = getApiBaseUrl().replace(/\/api\/v1\/?$/, '')
|
||||
const resolved = url.startsWith('/') ? `${baseUrl}${url}` : `${baseUrl}/${url}`
|
||||
return upgradeToHttpsIfNeeded(resolved)
|
||||
}
|
||||
|
||||
function resolveImageList(urls) {
|
||||
if (!Array.isArray(urls)) {
|
||||
return []
|
||||
}
|
||||
return urls.map((item) => resolveImageUrl(item)).filter(Boolean)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
resolveImageUrl,
|
||||
resolveImageList,
|
||||
upgradeToHttpsIfNeeded,
|
||||
isLocalDevUrl
|
||||
}
|
||||
@ -1,7 +1,9 @@
|
||||
const { getApiBaseUrl } = require('./constants')
|
||||
|
||||
function isPublicRequest(url) {
|
||||
return url === '/auth/wx-login' || url === '/auth/public-config'
|
||||
return url === '/auth/wx-login'
|
||||
|| url === '/auth/public-config'
|
||||
|| /^\/activities\/share\/[^/]+$/i.test(url || '')
|
||||
}
|
||||
|
||||
function normalizeError(message, detail) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user