Initial commit: TradeMate 外贸小助手 MVP

项目结构:
- backend/     Python FastAPI 后端
- uni-app/     uni-app跨端前端
- docs/        设计文档
- docker-compose.yml  Docker编排
- nginx/scripts/systemd 运维配置

已完成功能:
- 用户认证 (JWT)
- 智能翻译 + 回复建议
- 营销素材生成
- 客户管理 + 沉默检测
- 报价单管理
- 产品库管理
- 汇率换算
- 推送通知 (uni-push)
- WhatsApp Webhook框架
- Celery定时任务
This commit is contained in:
TradeMate Dev
2026-05-08 18:17:12 +08:00
commit c6206787da
121 changed files with 11743 additions and 0 deletions
+88
View File
@@ -0,0 +1,88 @@
const BASE_URL = 'http://localhost:8000/api/v1'
const getAuthHeader = () => {
const token = uni.getStorageSync('token')
return token ? { Authorization: `Bearer ${token}` } : {}
}
const request = (url, method = 'GET', data = {}) => {
return new Promise((resolve, reject) => {
uni.request({
url: `${BASE_URL}${url}`,
method,
data,
header: {
'Content-Type': 'application/json',
...getAuthHeader(),
},
success: (res) => {
if (res.statusCode === 200) {
resolve(res.data)
} else if (res.statusCode === 401) {
uni.removeStorageSync('token')
uni.reLaunch({ url: '/pages/login/login' })
reject(new Error('Unauthorized'))
} else {
reject(new Error(res.data?.detail || 'Request failed'))
}
},
fail: (err) => {
reject(err)
},
})
})
}
export const authApi = {
login: (phone, password) => request('/auth/login', 'POST', { username: phone, password }),
register: (phone, password, username) => request('/auth/register', 'POST', { phone, password, username }),
getUserInfo: () => request('/auth/me'),
}
export const translateApi = {
translate: (text, targetLang, sourceLang = 'auto') =>
request('/translate', 'POST', { text, target_lang: targetLang, source_lang: sourceLang }),
getReply: (inquiry, tone = 'professional', count = 3) =>
request('/translate/reply', 'POST', { inquiry, tone, count }),
}
export const customerApi = {
list: (page = 1, size = 20, status) => {
let params = `page=${page}&size=${size}`
if (status) params += `&status=${status}`
return request(`/customers?${params}`)
},
get: (id) => request(`/customers/${id}`),
create: (data) => request('/customers', 'POST', data),
update: (id, data) => request(`/customers/${id}`, 'PATCH', data),
delete: (id) => request(`/customers/${id}`, 'DELETE'),
getSilent: (days = 3) => request(`/customers/silent?days=${days}`),
getConversation: (id, page = 1, size = 50) =>
request(`/customers/${id}/conversation?page=${page}&size=${size}`),
}
export const marketingApi = {
generate: (productName, description, category, target = 'US importers', style = 'professional') =>
request('/marketing/generate', 'POST', {
product_name: productName,
description,
category,
target,
style,
}),
}
export const quotationApi = {
list: (page = 1, size = 20) => request(`/quotations?page=${page}&size=${size}`),
get: (id) => request(`/quotations/${id}`),
create: (data) => request('/quotations', 'POST', data),
updateStatus: (id, status) => request(`/quotations/${id}/status`, 'PATCH', { status }),
}
export const productApi = {
list: (page = 1, size = 20) => request(`/products?page=${page}&size=${size}`),
get: (id) => request(`/products/${id}`),
create: (data) => request('/products', 'POST', data),
update: (id, data) => request(`/products/${id}`, 'PATCH', data),
delete: (id) => request(`/products/${id}`, 'DELETE'),
}
+183
View File
@@ -0,0 +1,183 @@
import { ref } from 'vue'
let pushClientId = ''
let isInitialized = ref(false)
export const pushService = {
/**
* 初始化推送服务
*/
init() {
return new Promise((resolve, reject) => {
// #ifdef APP-PLUS
const plus = window.plus
plus.push.init({}, {
cover: true,
sound: 'system'
}, () => {
console.log('Push init success')
this.getClientId()
resolve(true)
}, (err) => {
console.error('Push init failed:', err)
reject(err)
})
// #endif
// #ifndef APP-PLUS
console.log('非App环境下跳过推送初始化')
resolve(false)
// #endif
})
},
/**
* 获取客户端推送ID
*/
getClientId() {
// #ifdef APP-PLUS
const push = window.plus.push
push.getClientInfo((info) => {
pushClientId = info.clientid
console.log('Push ClientID:', pushClientId)
this.registerDevice(pushClientId)
}, (err) => {
console.error('Get client ID failed:', err)
})
// #endif
},
/**
* 注册设备到服务器
*/
async registerDevice(clientId) {
if (!clientId) return
try {
const { request } = require('./api.js')
await request('/push/register', 'POST', {
client_id: clientId,
platform: uni.getSystemInfoSync().platform,
device_info: uni.getSystemInfoSync(),
})
console.log('Device registered successfully')
} catch (err) {
console.error('Register device failed:', err)
}
},
/**
* 监听接收推送消息
*/
onMessage(callback) {
// #ifdef APP-PLUS
const push = window.plus.push
push.addEventListener('receive', (msg) => {
console.log('Push received:', msg)
if (msg.payload) {
let payload
try {
payload = JSON.parse(msg.payload)
} catch (e) {
payload = { content: msg.payload }
}
callback({
title: msg.title || '外贸小助手',
content: msg.content,
payload,
timestamp: Date.now()
})
}
})
// #endif
},
/**
* 监听点击推送消息
*/
onClick(callback) {
// #ifdef APP-PLUS
const push = window.plus.push
push.addEventListener('click', (msg) => {
console.log('Push clicked:', msg)
if (msg.payload) {
let payload
try {
payload = JSON.parse(msg.payload)
} catch (e) {
payload = { content: msg.payload }
}
callback(payload)
}
})
// #endif
},
/**
* 创建本地推送通知
*/
createLocalNotification(options) {
return new Promise((resolve, reject) => {
// #ifdef APP-PLUS
const push = window.plus.push
const msg = {
title: options.title || '外贸小助手',
content: options.content,
payload: options.payload ? JSON.stringify(options.payload) : '',
delay: options.delay || 0,
icon: 'static/icons/logo.png'
}
push.createMessage(msg, (res) => {
console.log('Local notification created:', res)
resolve(res)
}, (err) => {
console.error('Create notification failed:', err)
reject(err)
})
// #endif
// #ifndef APP-PLUS
resolve(false)
// #endif
})
},
/**
* 清除所有推送消息
*/
clearNotifications() {
// #ifdef APP-PLUS
const push = window.plus.push
push.clear()
// #endif
},
/**
* 获取未读消息数量
*/
getBadgeCount() {
// #ifdef APP-PLUS
const main = window.plus.android.runtimeMainActivity()
const count = plus.android.invoke(main, 'getIntent', 'getIntExtra', '/badge', 0)
return count
// #endif
return 0
},
/**
* 设置角标
*/
setBadge(count) {
// #ifdef APP-PLUS
if (uni.setStorageSync) {
// 小程序设置角标
// #ifdef MP-WEIXIN
uni.setStorageSync('badgeCount', count)
// #endif
}
// #endif
}
}
export default pushService