From 1919ab2227d4dce2d8bdf4a4563c9bc766fe1c85 Mon Sep 17 00:00:00 2001 From: jeremygan2021 Date: Thu, 12 Feb 2026 16:31:05 +0800 Subject: [PATCH] forum --- backend/community/utils.py | 19 +- miniprogram/package.json | 1 + miniprogram/src/api/index.ts | 34 +++ miniprogram/src/app.config.ts | 12 +- miniprogram/src/app.scss | 2 + miniprogram/src/pages/forum/index.config.ts | 7 + miniprogram/src/pages/forum/index.scss | 219 ++++++++++++++++++ miniprogram/src/pages/forum/index.tsx | 207 +++++++++++++++++ .../subpackages/forum/create/create.config.ts | 6 + .../src/subpackages/forum/create/create.scss | 69 ++++++ .../src/subpackages/forum/create/index.tsx | 136 +++++++++++ .../subpackages/forum/detail/detail.config.ts | 6 + .../src/subpackages/forum/detail/detail.scss | 174 ++++++++++++++ .../src/subpackages/forum/detail/index.tsx | 202 ++++++++++++++++ 14 files changed, 1090 insertions(+), 4 deletions(-) create mode 100644 miniprogram/src/pages/forum/index.config.ts create mode 100644 miniprogram/src/pages/forum/index.scss create mode 100644 miniprogram/src/pages/forum/index.tsx create mode 100644 miniprogram/src/subpackages/forum/create/create.config.ts create mode 100644 miniprogram/src/subpackages/forum/create/create.scss create mode 100644 miniprogram/src/subpackages/forum/create/index.tsx create mode 100644 miniprogram/src/subpackages/forum/detail/detail.config.ts create mode 100644 miniprogram/src/subpackages/forum/detail/detail.scss create mode 100644 miniprogram/src/subpackages/forum/detail/index.tsx diff --git a/backend/community/utils.py b/backend/community/utils.py index ee4653a..516961a 100644 --- a/backend/community/utils.py +++ b/backend/community/utils.py @@ -1,5 +1,8 @@ from django.core.signing import TimestampSigner, BadSignature, SignatureExpired from shop.models import WeChatUser +import logging + +logger = logging.getLogger(__name__) def get_current_wechat_user(request): """ @@ -9,6 +12,7 @@ def get_current_wechat_user(request): """ auth_header = request.headers.get('Authorization') if not auth_header or not auth_header.startswith('Bearer '): + logger.warning(f"Authentication failed: Missing or invalid Authorization header. Header: {auth_header}") return None token = auth_header.split(' ')[1] signer = TimestampSigner() @@ -22,6 +26,7 @@ def get_current_wechat_user(request): # 如果没找到用户,检查是否是 Web 虚拟 OpenID # 场景:Web 用户已被合并到小程序账号,旧 Web Token 依然有效,指向合并后的账号 + logger.info(f"User not found for openid: {openid}, checking for merged account...") if openid.startswith('web_'): try: # 格式: web_13800138000 @@ -31,10 +36,20 @@ def get_current_wechat_user(request): # 尝试通过手机号查找(查找合并后的主账号) user = WeChatUser.objects.filter(phone_number=phone).first() if user: + logger.info(f"Found merged user {user.id} for phone {phone}") return user - except Exception: + except Exception as e: + logger.error(f"Error checking merged account: {e}") pass + logger.warning(f"Authentication failed: User not found for openid {openid}") return None - except (BadSignature, SignatureExpired): + except SignatureExpired: + logger.warning("Authentication failed: Signature expired") + return None + except BadSignature: + logger.warning("Authentication failed: Bad signature") + return None + except Exception as e: + logger.error(f"Authentication unexpected error: {e}") return None diff --git a/miniprogram/package.json b/miniprogram/package.json index 0dbeb30..ff83f4a 100644 --- a/miniprogram/package.json +++ b/miniprogram/package.json @@ -42,6 +42,7 @@ "@tarojs/runtime": "3.6.20", "@tarojs/shared": "3.6.20", "@tarojs/taro": "3.6.20", + "marked": "^17.0.2", "react": "^18.0.0", "react-dom": "^18.0.0", "taro-ui": "^3.0.0-alpha.10" diff --git a/miniprogram/src/api/index.ts b/miniprogram/src/api/index.ts index 6bdc308..02fb530 100644 --- a/miniprogram/src/api/index.ts +++ b/miniprogram/src/api/index.ts @@ -31,3 +31,37 @@ export const distributorWithdraw = (amount: number) => request({ url: '/distribu // User export const updateUserInfo = (data: any) => request({ url: '/wechat/update/', method: 'POST', data }) export const wechatLogin = (code: string) => request({ url: '/wechat/login/', method: 'POST', data: { code } }) + +// Forum / Community +export const getTopics = (params: any) => request({ url: '/community/topics/', data: params }) +export const getTopicDetail = (id: number) => request({ url: `/community/topics/${id}/` }) +export const createTopic = (data: any) => request({ url: '/community/topics/', method: 'POST', data }) +export const updateTopic = (id: number, data: any) => request({ url: `/community/topics/${id}/`, method: 'PATCH', data }) +export const getReplies = (params: any) => request({ url: '/community/replies/', data: params }) +export const createReply = (data: any) => request({ url: '/community/replies/', method: 'POST', data }) +export const getStarUsers = () => request({ url: '/users/stars/' }) +export const getAnnouncements = () => request({ url: '/community/announcements/' }) + +// Upload Media for Forum +export const uploadMedia = (filePath: string, type: 'image' | 'video') => { + const BASE_URL = process.env.TARO_APP_API_URL || 'https://market.quant-speed.com/api' + return Taro.uploadFile({ + url: `${BASE_URL}/community/media/`, + filePath, + name: 'file', + formData: { + media_type: type + }, + header: { + 'Authorization': `Bearer ${Taro.getStorageSync('token')}` + } + }).then(res => { + if (res.statusCode >= 200 && res.statusCode < 300) { + return JSON.parse(res.data) + } + throw new Error('Upload failed') + }) +} + +import Taro from '@tarojs/taro' + diff --git a/miniprogram/src/app.config.ts b/miniprogram/src/app.config.ts index 187cb73..7701efd 100644 --- a/miniprogram/src/app.config.ts +++ b/miniprogram/src/app.config.ts @@ -5,6 +5,7 @@ export default defineAppConfig({ 'pages/services/detail', 'pages/courses/index', 'pages/courses/detail', + 'pages/forum/index', 'pages/goods/detail', 'pages/cart/cart', 'pages/order/checkout', @@ -21,6 +22,13 @@ export default defineAppConfig({ 'invite', 'withdraw' ] + }, + { + root: 'subpackages/forum', + pages: [ + 'detail/index', + 'create/index' + ] } ], window: { @@ -48,8 +56,8 @@ export default defineAppConfig({ selectedIconPath: "./assets/AI_service_active.png" }, { - pagePath: "pages/courses/index", - text: "VB课程", + pagePath: "pages/forum/index", + text: "社区", iconPath: "./assets/VR.png", selectedIconPath: "./assets/VR_active.png" }, diff --git a/miniprogram/src/app.scss b/miniprogram/src/app.scss index 55d4031..e1d920e 100644 --- a/miniprogram/src/app.scss +++ b/miniprogram/src/app.scss @@ -1,3 +1,5 @@ +@import 'taro-ui/dist/style/index.scss'; + page { --primary-cyan: #00f0ff; --primary-green: #00b96b; diff --git a/miniprogram/src/pages/forum/index.config.ts b/miniprogram/src/pages/forum/index.config.ts new file mode 100644 index 0000000..675a20a --- /dev/null +++ b/miniprogram/src/pages/forum/index.config.ts @@ -0,0 +1,7 @@ +export default definePageConfig({ + navigationBarTitleText: '开发者社区', + enablePullDownRefresh: true, + backgroundColor: '#000000', + navigationBarBackgroundColor: '#000000', + navigationBarTextStyle: 'white' +}) diff --git a/miniprogram/src/pages/forum/index.scss b/miniprogram/src/pages/forum/index.scss new file mode 100644 index 0000000..8465aaa --- /dev/null +++ b/miniprogram/src/pages/forum/index.scss @@ -0,0 +1,219 @@ +.forum-page { + min-height: 100vh; + background-color: #000; + padding-bottom: 40px; + color: #fff; + + .hero-section { + padding: 40px 20px 20px; + text-align: center; + background: linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(0,185,107,0.1) 100%); + + .title { + font-size: 24px; + font-weight: bold; + margin-bottom: 10px; + color: #fff; + + .highlight { + color: #00b96b; + } + } + + .subtitle { + color: #888; + font-size: 14px; + margin-bottom: 20px; + } + + .search-box { + display: flex; + gap: 10px; + margin-bottom: 20px; + + .at-search-bar { + flex: 1; + background-color: transparent; + padding: 0; + + &::after { + border-bottom: none; + } + + .at-search-bar__input-cnt { + background-color: rgba(255, 255, 255, 0.1); + border: 1px solid #333; + border-radius: 8px; + } + + .at-search-bar__input { + color: #fff; + } + } + + .create-btn { + background-color: #00b96b; + color: #fff; + border: none; + border-radius: 8px; + padding: 0 16px; + font-size: 14px; + display: flex; + align-items: center; + justify-content: center; + } + } + } + + .tabs-wrapper { + background-color: #000; + + .at-tabs__item { + color: #888; + + &--active { + color: #00b96b; + font-weight: bold; + } + } + + .at-tabs__item-underline { + background-color: #00b96b; + } + } + + .topic-list { + padding: 10px; + + .topic-card { + background: rgba(20,20,20,0.6); + border: 1px solid rgba(255,255,255,0.1); + border-radius: 8px; + padding: 16px; + margin-bottom: 12px; + position: relative; + + &.pinned { + border-color: rgba(0, 185, 107, 0.4); + box-shadow: 0 0 10px rgba(0, 185, 107, 0.1); + } + + .card-header { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 6px; + margin-bottom: 8px; + + .tag { + font-size: 10px; + padding: 2px 6px; + border-radius: 4px; + border: 1px solid #444; + + &.pinned-tag { + border-color: #ff4d4f; + color: #ff4d4f; + } + + &.verified-tag { + border-color: #00b96b; + color: #00b96b; + } + } + + .card-title { + font-size: 16px; + font-weight: bold; + color: #fff; + flex: 1; + } + } + + .card-content { + font-size: 14px; + color: #aaa; + margin-bottom: 12px; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + overflow: hidden; + } + + .card-image { + margin-bottom: 12px; + + image { + width: 100%; + max-height: 150px; + border-radius: 8px; + object-fit: cover; + } + } + + .card-footer { + display: flex; + justify-content: space-between; + align-items: center; + font-size: 12px; + color: #666; + + .author-info { + display: flex; + align-items: center; + gap: 6px; + + .avatar { + width: 20px; + height: 20px; + border-radius: 50%; + } + + .nickname { + &.star { + color: #ffd700; + font-weight: bold; + } + } + } + + .stats { + display: flex; + gap: 12px; + + .stat-item { + display: flex; + align-items: center; + gap: 4px; + } + } + } + } + } + + .empty-state { + text-align: center; + padding: 40px; + color: #666; + } + + .fab { + position: fixed; + right: 20px; + bottom: 40px; + width: 50px; + height: 50px; + background-color: #00b96b; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 4px 12px rgba(0, 185, 107, 0.4); + z-index: 100; + + .at-icon { + font-size: 24px; + color: #fff; + } + } +} diff --git a/miniprogram/src/pages/forum/index.tsx b/miniprogram/src/pages/forum/index.tsx new file mode 100644 index 0000000..e529459 --- /dev/null +++ b/miniprogram/src/pages/forum/index.tsx @@ -0,0 +1,207 @@ +import React, { useState, useEffect } from 'react' +import Taro, { usePullDownRefresh, useReachBottom } from '@tarojs/taro' +import { View, Text, Image, Button } from '@tarojs/components' +import { AtSearchBar, AtTabs, AtIcon, AtActivityIndicator, AtFab } from 'taro-ui' +import { getTopics } from '../../api' +import { useLogin } from '../../utils/hooks' // Assuming a hook or just use Taro.getStorageSync +import './index.scss' + +const ForumList = () => { + const [topics, setTopics] = useState([]) + const [loading, setLoading] = useState(false) + const [hasMore, setHasMore] = useState(true) + const [page, setPage] = useState(1) + const [searchText, setSearchText] = useState('') + const [currentTab, setCurrentTab] = useState(0) + + const categories = [ + { title: '全部', key: 'all' }, + { title: '讨论', key: 'discussion' }, + { title: '求助', key: 'help' }, + { title: '分享', key: 'share' }, + { title: '公告', key: 'notice' }, + ] + + const fetchList = async (reset = false) => { + if (loading) return + if (!reset && !hasMore) return + + setLoading(true) + try { + const currentPage = reset ? 1 : page + const params: any = { + page: currentPage, + search: searchText, + category: categories[currentTab].key !== 'all' ? categories[currentTab].key : undefined + } + + const res = await getTopics(params) + const newTopics = res.results || res.data || [] // Adjust based on API response structure + + if (reset) { + setTopics(newTopics) + } else { + setTopics(prev => [...prev, ...newTopics]) + } + + // Check if more data exists (assuming standard pagination) + if (res.next || newTopics.length === 10) { // 10 is default page size usually + setHasMore(true) + setPage(currentPage + 1) + } else { + setHasMore(false) + } + } catch (error) { + console.error(error) + Taro.showToast({ title: '加载失败', icon: 'none' }) + } finally { + setLoading(false) + Taro.stopPullDownRefresh() + } + } + + useEffect(() => { + fetchList(true) + }, [currentTab]) + + usePullDownRefresh(() => { + fetchList(true) + }) + + useReachBottom(() => { + fetchList(false) + }) + + const handleSearch = (value) => { + setSearchText(value) + } + + const onSearchConfirm = () => { + fetchList(true) + } + + const handleTabClick = (value) => { + setCurrentTab(value) + // useEffect will trigger fetch + } + + const navigateToDetail = (id) => { + Taro.navigateTo({ + url: `/subpackages/forum/detail/index?id=${id}` + }) + } + + const navigateToCreate = () => { + const token = Taro.getStorageSync('token') + if (!token) { + Taro.showToast({ title: '请先登录', icon: 'none' }) + // Optional: Trigger login flow + return + } + Taro.navigateTo({ + url: '/subpackages/forum/create/index' + }) + } + + const getCategoryLabel = (cat) => { + const map = { + 'help': '求助', + 'share': '分享', + 'notice': '公告', + 'discussion': '讨论' + } + return map[cat] || '讨论' + } + + // Helper to extract first image from markdown + const getCoverImage = (content) => { + const match = content.match(/!\[.*?\]\((.*?)\)/) + return match ? match[1] : null + } + + const stripMarkdown = (content) => { + return content.replace(/!\[.*?\]\(.*?\)/g, '[图片]').replace(/[#*`]/g, '') + } + + return ( + + + + Quant Speed Community + + 技术交流 · 硬件开发 · 官方支持 + + + + + + + + + + + + {topics.map(item => ( + navigateToDetail(item.id)} + > + + {item.is_pinned && 置顶} + {getCategoryLabel(item.category)} + {item.is_verified_owner && 认证} + {item.title} + + + + {stripMarkdown(item.content)} + + + {getCoverImage(item.content) && ( + + + + )} + + + + + + {item.author_info?.nickname || '匿名'} + + + + + 👁 {item.view_count || 0} + + + 💬 {item.replies?.length || 0} + + + + + ))} + + {loading && } + {!loading && topics.length === 0 && 暂无内容} + + + + + + + ) +} + +export default ForumList diff --git a/miniprogram/src/subpackages/forum/create/create.config.ts b/miniprogram/src/subpackages/forum/create/create.config.ts new file mode 100644 index 0000000..c8cead1 --- /dev/null +++ b/miniprogram/src/subpackages/forum/create/create.config.ts @@ -0,0 +1,6 @@ +export default definePageConfig({ + navigationBarTitleText: '发布话题', + backgroundColor: '#000000', + navigationBarBackgroundColor: '#000000', + navigationBarTextStyle: 'white' +}) diff --git a/miniprogram/src/subpackages/forum/create/create.scss b/miniprogram/src/subpackages/forum/create/create.scss new file mode 100644 index 0000000..d69ef59 --- /dev/null +++ b/miniprogram/src/subpackages/forum/create/create.scss @@ -0,0 +1,69 @@ +.create-topic-page { + min-height: 100vh; + background-color: #000; + padding: 20px; + color: #fff; + + .form-item { + margin-bottom: 20px; + + .label { + display: block; + margin-bottom: 8px; + color: #888; + font-size: 14px; + } + + input { + background: rgba(255,255,255,0.1); + border: 1px solid #333; + padding: 10px; + border-radius: 8px; + color: #fff; + } + + textarea { + background: rgba(255,255,255,0.1); + border: 1px solid #333; + padding: 10px; + border-radius: 8px; + color: #fff; + width: 100%; + min-height: 200px; + } + + .picker { + background: rgba(255,255,255,0.1); + border: 1px solid #333; + padding: 10px; + border-radius: 8px; + color: #fff; + } + } + + .media-upload { + margin-bottom: 30px; + + .upload-btn { + display: inline-block; + padding: 8px 16px; + background: rgba(255,255,255,0.1); + border-radius: 4px; + font-size: 14px; + color: #ccc; + } + } + + .submit-btn { + background-color: #00b96b; + color: #fff; + border: none; + border-radius: 8px; + font-weight: bold; + + &.disabled { + background-color: #333; + color: #666; + } + } +} diff --git a/miniprogram/src/subpackages/forum/create/index.tsx b/miniprogram/src/subpackages/forum/create/index.tsx new file mode 100644 index 0000000..ab83383 --- /dev/null +++ b/miniprogram/src/subpackages/forum/create/index.tsx @@ -0,0 +1,136 @@ +import React, { useState } from 'react' +import Taro from '@tarojs/taro' +import { View, Text, Input, Textarea, Button, Picker } from '@tarojs/components' +import { createTopic, uploadMedia } from '../../../api' +import './create.scss' + +const CreateTopic = () => { + const [title, setTitle] = useState('') + const [content, setContent] = useState('') + const [categoryIndex, setCategoryIndex] = useState(0) + const [loading, setLoading] = useState(false) + + const categories = [ + { key: 'discussion', label: '技术讨论' }, + { key: 'help', label: '求助问答' }, + { key: 'share', label: '经验分享' }, + ] + + const handleCategoryChange = (e) => { + setCategoryIndex(e.detail.value) + } + + const handleUpload = async () => { + try { + const res = await Taro.chooseMedia({ + count: 1, + mediaType: ['image', 'video'], + sourceType: ['album', 'camera'] + }) + + const file = res.tempFiles[0] + const type = file.fileType === 'video' ? 'video' : 'image' + + Taro.showLoading({ title: '上传中...' }) + + const uploadRes = await uploadMedia(file.tempFilePath, type) + + let url = uploadRes.file + if (url && !url.startsWith('http')) { + const BASE_URL = process.env.TARO_APP_API_URL || 'https://market.quant-speed.com/api' + const host = BASE_URL.replace(/\/api\/?$/, '') + if (!url.startsWith('/')) url = '/' + url + url = `${host}${url}` + } + + const insertText = type === 'video' + ? `\n\n` + : `\n![image](${url})\n` + + setContent(prev => prev + insertText) + + Taro.hideLoading() + } catch (error) { + console.error(error) + Taro.hideLoading() + // Only toast if it's an error, not cancel + if (error.errMsg && error.errMsg.indexOf('cancel') === -1) { + Taro.showToast({ title: '上传失败', icon: 'none' }) + } + } + } + + const handleSubmit = async () => { + if (!title.trim() || !content.trim()) { + Taro.showToast({ title: '请填写完整', icon: 'none' }) + return + } + + setLoading(true) + try { + const res = await createTopic({ + title, + content, + category: categories[categoryIndex].key + }) + + Taro.showToast({ title: '发布成功', icon: 'success' }) + setTimeout(() => { + Taro.navigateBack() + }, 1500) + } catch (error) { + console.error(error) + Taro.showToast({ title: '发布失败', icon: 'none' }) + } finally { + setLoading(false) + } + } + + return ( + + + 板块 + + + {categories[categoryIndex].label} + + + + + + 标题 + setTitle(e.detail.value)} + placeholder='请输入标题' + /> + + + + 内容 (支持 Markdown) +