This commit is contained in:
@@ -147,6 +147,12 @@ class ServiceOrderSerializer(serializers.ModelSerializer):
|
||||
validated_data['salesperson'] = salesperson
|
||||
except Salesperson.DoesNotExist:
|
||||
pass
|
||||
|
||||
try:
|
||||
distributor = Distributor.objects.get(invite_code=ref_code)
|
||||
validated_data['distributor'] = distributor
|
||||
except Distributor.DoesNotExist:
|
||||
pass
|
||||
|
||||
return super().create(validated_data)
|
||||
|
||||
|
||||
@@ -1463,10 +1463,36 @@ class DistributorViewSet(viewsets.GenericViewSet):
|
||||
if distributor.qr_code_url:
|
||||
return Response({'qr_code_url': distributor.qr_code_url})
|
||||
|
||||
# 调用微信接口生成小程序码 (wxacode.getUnlimited)
|
||||
# 这里简化处理,返回模拟URL或需要实现具体逻辑
|
||||
# 实际逻辑需要获取 AccessToken 然后调用 API
|
||||
return Response({'qr_code_url': 'https://placeholder.com/qrcode.png'})
|
||||
access_token = get_access_token()
|
||||
if not access_token:
|
||||
return Response({'error': 'Failed to get access token'}, status=500)
|
||||
|
||||
# 微信小程序码接口 B:适用于需要的码数量极多的业务场景
|
||||
url = f"https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token={access_token}"
|
||||
data = {
|
||||
"scene": distributor.invite_code,
|
||||
"page": "pages/index/index", # 扫码落地页
|
||||
"width": 430
|
||||
}
|
||||
|
||||
try:
|
||||
res = requests.post(url, json=data)
|
||||
# 微信返回图片时 Content-Type 包含 image/jpeg 或 image/png
|
||||
if res.status_code == 200 and 'image' in res.headers.get('Content-Type', ''):
|
||||
file_name = f"distributor_qr_{distributor.invite_code}_{uuid.uuid4().hex[:6]}.png"
|
||||
# 保存到 media/qr_codes 目录
|
||||
path = default_storage.save(f"qr_codes/{file_name}", ContentFile(res.content))
|
||||
qr_url = default_storage.url(path)
|
||||
|
||||
distributor.qr_code_url = qr_url
|
||||
distributor.save()
|
||||
|
||||
return Response({'qr_code_url': qr_url})
|
||||
else:
|
||||
# 如果是 JSON 错误信息
|
||||
return Response({'error': 'WeChat API error', 'detail': res.json()}, status=500)
|
||||
except Exception as e:
|
||||
return Response({'error': str(e)}, status=500)
|
||||
|
||||
@action(detail=False, methods=['post'])
|
||||
def withdraw(self, request):
|
||||
|
||||
@@ -4,8 +4,14 @@ import { request } from '../utils/request'
|
||||
export const getConfigs = () => request({ url: '/configs/' })
|
||||
export const getConfigDetail = (id: number) => request({ url: `/configs/${id}/` })
|
||||
|
||||
const getInviteCode = () => Taro.getStorageSync('invite_code') || ''
|
||||
|
||||
// Orders
|
||||
export const createOrder = (data: any) => request({ url: '/orders/', method: 'POST', data })
|
||||
export const createOrder = (data: any) => {
|
||||
const code = getInviteCode()
|
||||
if (code) data.ref_code = code
|
||||
return request({ url: '/orders/', method: 'POST', data })
|
||||
}
|
||||
export const getOrder = (id: number) => request({ url: `/orders/${id}/` })
|
||||
export const getMyOrders = () => request({ url: '/orders/' })
|
||||
export const prepayMiniprogram = (orderId: number) => request({ url: `/orders/${orderId}/prepay_miniprogram/`, method: 'POST' })
|
||||
@@ -14,20 +20,28 @@ export const queryOrderStatus = (orderId: number) => request({ url: `/orders/${o
|
||||
// AI Services
|
||||
export const getServices = () => request({ url: '/services/' })
|
||||
export const getServiceDetail = (id: number) => request({ url: `/services/${id}/` })
|
||||
export const createServiceOrder = (data: any) => request({ url: '/service-orders/', method: 'POST', data })
|
||||
export const createServiceOrder = (data: any) => {
|
||||
const code = getInviteCode()
|
||||
if (code) data.ref_code = code
|
||||
return request({ url: '/service-orders/', method: 'POST', data })
|
||||
}
|
||||
|
||||
// VB Courses
|
||||
export const getVBCourses = () => request({ url: '/courses/' })
|
||||
export const getVBCourseDetail = (id: number) => request({ url: `/courses/${id}/` })
|
||||
|
||||
// Distributor
|
||||
export const distributorRegister = (data: any) => request({ url: '/distributor/register/', method: 'POST', data })
|
||||
export const distributorRegister = (data: any) => {
|
||||
const code = getInviteCode()
|
||||
if (code && !data.invite_code) data.invite_code = code
|
||||
return request({ url: '/distributor/register/', method: 'POST', data })
|
||||
}
|
||||
export const distributorInfo = () => request({ url: '/distributor/info/' })
|
||||
export const distributorInvite = () => request({ url: '/distributor/invite/', method: 'POST' })
|
||||
export const distributorWithdraw = (amount: number) => request({ url: '/distributor/withdraw/', method: 'POST', data: { amount } })
|
||||
// TODO: Verify if these exist in the API docs
|
||||
// export const distributorTeam = () => request({ url: '/distributor/team/' })
|
||||
// export const distributorHistory = () => request({ url: '/distributor/history/' })
|
||||
export const distributorTeam = () => request({ url: '/distributor/team/' })
|
||||
export const distributorEarnings = (params?: any) => request({ url: '/distributor/earnings/', data: params })
|
||||
export const distributorOrders = (params?: any) => request({ url: '/distributor/orders/', data: params })
|
||||
|
||||
// User
|
||||
export const updateUserInfo = (data: any) => request({ url: '/wechat/update/', method: 'POST', data })
|
||||
|
||||
@@ -21,7 +21,10 @@ export default defineAppConfig({
|
||||
'index',
|
||||
'register',
|
||||
'invite',
|
||||
'withdraw'
|
||||
'withdraw',
|
||||
'team',
|
||||
'earnings',
|
||||
'orders'
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,12 +1,31 @@
|
||||
import { PropsWithChildren } from 'react'
|
||||
import { useLaunch } from '@tarojs/taro'
|
||||
import Taro, { useLaunch } from '@tarojs/taro'
|
||||
import { login } from './utils/request'
|
||||
import './app.scss'
|
||||
|
||||
function App({ children }: PropsWithChildren<any>) {
|
||||
|
||||
useLaunch(() => {
|
||||
console.log('App launched.')
|
||||
useLaunch((options) => {
|
||||
console.log('App launched.', options)
|
||||
|
||||
// 捕获邀请码 (场景值或直接参数)
|
||||
const { query } = options
|
||||
if (query) {
|
||||
let inviteCode = ''
|
||||
if (query.scene) {
|
||||
// 扫码进入,scene 需要解码
|
||||
inviteCode = decodeURIComponent(query.scene)
|
||||
} else if (query.invite_code) {
|
||||
// 链接分享进入
|
||||
inviteCode = query.invite_code
|
||||
}
|
||||
|
||||
if (inviteCode && inviteCode !== 'undefined') {
|
||||
console.log('Captured invite code:', inviteCode)
|
||||
Taro.setStorageSync('invite_code', inviteCode)
|
||||
}
|
||||
}
|
||||
|
||||
// Auto login
|
||||
login().then(res => {
|
||||
console.log('Logged in as:', res?.nickname)
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '收益明细'
|
||||
})
|
||||
48
miniprogram/src/subpackages/distributor/earnings.scss
Normal file
48
miniprogram/src/subpackages/distributor/earnings.scss
Normal file
@@ -0,0 +1,48 @@
|
||||
.page-container {
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
padding: 20px;
|
||||
}
|
||||
.item {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 30px;
|
||||
margin-bottom: 20px;
|
||||
.row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
.type {
|
||||
font-size: 30px;
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
}
|
||||
.amount {
|
||||
font-size: 32px;
|
||||
color: #ff9800;
|
||||
font-weight: bold;
|
||||
}
|
||||
.source {
|
||||
font-size: 24px;
|
||||
color: #666;
|
||||
}
|
||||
.status {
|
||||
font-size: 24px;
|
||||
color: #00b96b;
|
||||
}
|
||||
}
|
||||
.time {
|
||||
font-size: 22px;
|
||||
color: #999;
|
||||
display: block;
|
||||
margin-top: 10px;
|
||||
border-top: 1px solid #f5f5f5;
|
||||
padding-top: 10px;
|
||||
}
|
||||
}
|
||||
.empty {
|
||||
padding: 100px 0;
|
||||
text-align: center;
|
||||
color: #999;
|
||||
font-size: 28px;
|
||||
}
|
||||
55
miniprogram/src/subpackages/distributor/earnings.tsx
Normal file
55
miniprogram/src/subpackages/distributor/earnings.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import Taro, { useLoad } from '@tarojs/taro'
|
||||
import { useState } from 'react'
|
||||
import { distributorEarnings } from '../../api'
|
||||
import './earnings.scss'
|
||||
|
||||
export default function Earnings() {
|
||||
const [list, setList] = useState<any[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useLoad(() => {
|
||||
fetchData()
|
||||
})
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const res: any = await distributorEarnings()
|
||||
// Pagination support check? The backend returns { count, next, previous, results } or just list if no pagination
|
||||
if (res.results) {
|
||||
setList(res.results)
|
||||
} else if (Array.isArray(res)) {
|
||||
setList(res)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
Taro.showToast({ title: '加载失败', icon: 'none' })
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='page-container'>
|
||||
{list.length > 0 ? (
|
||||
list.map((item: any) => (
|
||||
<View className='item' key={item.id}>
|
||||
<View className='row'>
|
||||
<Text className='type'>{item.level === 1 ? '直接推广' : '团队奖励'}</Text>
|
||||
<Text className='amount'>+{item.amount}</Text>
|
||||
</View>
|
||||
<View className='row'>
|
||||
<Text className='source'>
|
||||
{item.order_info?.customer_name} - 订单金额 ¥{item.order_info?.total_price}
|
||||
</Text>
|
||||
<Text className='status'>{item.status === 'settled' ? '已结算' : '待结算'}</Text>
|
||||
</View>
|
||||
<Text className='time'>{item.created_at?.replace('T', ' ').substring(0, 19)}</Text>
|
||||
</View>
|
||||
))
|
||||
) : (
|
||||
<View className='empty'>暂无收益记录</View>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -30,7 +30,9 @@ export default function DistributorIndex() {
|
||||
|
||||
const goInvite = () => Taro.navigateTo({ url: '/subpackages/distributor/invite' })
|
||||
const goWithdraw = () => Taro.navigateTo({ url: '/subpackages/distributor/withdraw' })
|
||||
const showComingSoon = () => Taro.showToast({ title: '功能开发中', icon: 'none' })
|
||||
const goTeam = () => Taro.navigateTo({ url: '/subpackages/distributor/team' })
|
||||
const goEarnings = () => Taro.navigateTo({ url: '/subpackages/distributor/earnings' })
|
||||
const goOrders = () => Taro.navigateTo({ url: '/subpackages/distributor/orders' })
|
||||
|
||||
if (loading) return <View>Loading...</View>
|
||||
if (!info) return <View>Error</View>
|
||||
@@ -61,15 +63,19 @@ export default function DistributorIndex() {
|
||||
<View className='menu-list'>
|
||||
<View className='menu-item' onClick={goInvite}>
|
||||
<Text>推广二维码</Text>
|
||||
<Text className='arrow'>></Text>
|
||||
<Text className='arrow'>{'>'}</Text>
|
||||
</View>
|
||||
<View className='menu-item' onClick={showComingSoon}>
|
||||
<View className='menu-item' onClick={goTeam}>
|
||||
<Text>我的团队</Text>
|
||||
<Text className='arrow'>></Text>
|
||||
<Text className='arrow'>{'>'}</Text>
|
||||
</View>
|
||||
<View className='menu-item' onClick={showComingSoon}>
|
||||
<Text>提现记录</Text>
|
||||
<Text className='arrow'>></Text>
|
||||
<View className='menu-item' onClick={goEarnings}>
|
||||
<Text>收益明细</Text>
|
||||
<Text className='arrow'>{'>'}</Text>
|
||||
</View>
|
||||
<View className='menu-item' onClick={goOrders}>
|
||||
<Text>分销订单</Text>
|
||||
<Text className='arrow'>{'>'}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
3
miniprogram/src/subpackages/distributor/orders.config.ts
Normal file
3
miniprogram/src/subpackages/distributor/orders.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '分销订单'
|
||||
})
|
||||
65
miniprogram/src/subpackages/distributor/orders.scss
Normal file
65
miniprogram/src/subpackages/distributor/orders.scss
Normal file
@@ -0,0 +1,65 @@
|
||||
.page-container {
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
padding: 20px;
|
||||
}
|
||||
.item {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
margin-bottom: 20px;
|
||||
.row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
padding-bottom: 16px;
|
||||
margin-bottom: 16px;
|
||||
.order-no {
|
||||
font-size: 24px;
|
||||
color: #666;
|
||||
}
|
||||
.status {
|
||||
font-size: 24px;
|
||||
color: #00b96b;
|
||||
}
|
||||
}
|
||||
.content {
|
||||
display: flex;
|
||||
margin-bottom: 16px;
|
||||
.img {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border-radius: 8px;
|
||||
background: #eee;
|
||||
margin-right: 20px;
|
||||
}
|
||||
.info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
.title {
|
||||
font-size: 28px;
|
||||
color: #333;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.price {
|
||||
font-size: 32px;
|
||||
color: #ff9800;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
.footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 24px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
.empty {
|
||||
padding: 100px 0;
|
||||
text-align: center;
|
||||
color: #999;
|
||||
font-size: 28px;
|
||||
}
|
||||
58
miniprogram/src/subpackages/distributor/orders.tsx
Normal file
58
miniprogram/src/subpackages/distributor/orders.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import { View, Text, Image } from '@tarojs/components'
|
||||
import Taro, { useLoad } from '@tarojs/taro'
|
||||
import { useState } from 'react'
|
||||
import { distributorOrders } from '../../api'
|
||||
import './orders.scss'
|
||||
|
||||
export default function Orders() {
|
||||
const [list, setList] = useState<any[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useLoad(() => {
|
||||
fetchData()
|
||||
})
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const res: any = await distributorOrders()
|
||||
if (res.results) {
|
||||
setList(res.results)
|
||||
} else if (Array.isArray(res)) {
|
||||
setList(res)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
Taro.showToast({ title: '加载失败', icon: 'none' })
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='page-container'>
|
||||
{list.length > 0 ? (
|
||||
list.map((item: any) => (
|
||||
<View className='item' key={item.id}>
|
||||
<View className='row'>
|
||||
<Text className='order-no'>订单号: {item.wechat_trade_no || item.id}</Text>
|
||||
<Text className='status'>{item.status === 'paid' ? '已支付' : item.status}</Text>
|
||||
</View>
|
||||
<View className='content'>
|
||||
<Image className='img' src={item.config_image || ''} mode='aspectFill' />
|
||||
<View className='info'>
|
||||
<Text className='title'>{item.config_name || item.course_title || '商品'}</Text>
|
||||
<Text className='price'>¥{item.total_price}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View className='footer'>
|
||||
<Text className='customer'>买家: {item.customer_name}</Text>
|
||||
<Text className='time'>{item.created_at?.split('T')[0]}</Text>
|
||||
</View>
|
||||
</View>
|
||||
))
|
||||
) : (
|
||||
<View className='empty'>暂无分销订单</View>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
3
miniprogram/src/subpackages/distributor/team.config.ts
Normal file
3
miniprogram/src/subpackages/distributor/team.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '我的团队'
|
||||
})
|
||||
72
miniprogram/src/subpackages/distributor/team.scss
Normal file
72
miniprogram/src/subpackages/distributor/team.scss
Normal file
@@ -0,0 +1,72 @@
|
||||
.page-container {
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
.header {
|
||||
background: #fff;
|
||||
padding: 30px;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
margin-bottom: 20px;
|
||||
.stat {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
.val {
|
||||
font-size: 36px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
.lbl {
|
||||
font-size: 24px;
|
||||
color: #999;
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.list {
|
||||
background: #fff;
|
||||
.list-header {
|
||||
padding: 20px 30px;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
.item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20px 30px;
|
||||
border-bottom: 1px solid #eee;
|
||||
.avatar {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
margin-right: 20px;
|
||||
background: #eee;
|
||||
}
|
||||
.info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.name {
|
||||
font-size: 28px;
|
||||
color: #333;
|
||||
}
|
||||
.time {
|
||||
font-size: 22px;
|
||||
color: #999;
|
||||
margin-top: 6px;
|
||||
}
|
||||
}
|
||||
.level {
|
||||
font-size: 24px;
|
||||
color: #ff9800;
|
||||
}
|
||||
}
|
||||
.empty {
|
||||
padding: 50px;
|
||||
text-align: center;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
62
miniprogram/src/subpackages/distributor/team.tsx
Normal file
62
miniprogram/src/subpackages/distributor/team.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import { View, Text, Image } from '@tarojs/components'
|
||||
import Taro, { useLoad } from '@tarojs/taro'
|
||||
import { useState } from 'react'
|
||||
import { distributorTeam } from '../../api'
|
||||
import './team.scss'
|
||||
|
||||
export default function Team() {
|
||||
const [data, setData] = useState<any>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useLoad(() => {
|
||||
fetchData()
|
||||
})
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const res = await distributorTeam()
|
||||
setData(res)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
Taro.showToast({ title: '加载失败', icon: 'none' })
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) return <View className='page-container'>Loading...</View>
|
||||
if (!data) return <View className='page-container'>Error</View>
|
||||
|
||||
return (
|
||||
<View className='page-container'>
|
||||
<View className='header'>
|
||||
<View className='stat'>
|
||||
<Text className='val'>{data.children_count}</Text>
|
||||
<Text className='lbl'>直推人数</Text>
|
||||
</View>
|
||||
<View className='stat'>
|
||||
<Text className='val'>¥{data.second_level_earnings}</Text>
|
||||
<Text className='lbl'>团队贡献收益</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className='list'>
|
||||
<View className='list-header'>我的团队成员</View>
|
||||
{data.children?.length > 0 ? (
|
||||
data.children.map((item: any) => (
|
||||
<View className='item' key={item.id}>
|
||||
<Image className='avatar' src={item.user_info?.avatar_url || ''} />
|
||||
<View className='info'>
|
||||
<Text className='name'>{item.user_info?.nickname || '用户'}</Text>
|
||||
<Text className='time'>加入时间: {item.created_at?.split('T')[0]}</Text>
|
||||
</View>
|
||||
<Text className='level'>Lv.{item.level}</Text>
|
||||
</View>
|
||||
))
|
||||
) : (
|
||||
<View className='empty'>暂无成员</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user