diff --git a/backend/app/api/v1/auth.py b/backend/app/api/v1/auth.py
index ee44390..7dbb74a 100644
--- a/backend/app/api/v1/auth.py
+++ b/backend/app/api/v1/auth.py
@@ -189,6 +189,16 @@ async def get_me(
}
+class ProfileUpdate(BaseModel):
+ username: str = None
+ email: str = None
+
+
+class PasswordChange(BaseModel):
+ old_password: str
+ new_password: str
+
+
class SettingsUpdate(BaseModel):
preferred_translate_provider: str = None
reply_tone: str = None
@@ -202,6 +212,72 @@ class WeChatLoginRequest(BaseModel):
iv: str = ""
+@router.put("/me")
+async def update_me(
+ data: ProfileUpdate,
+ authorization: Optional[str] = Header(None, alias="Authorization"),
+ db: AsyncSession = Depends(get_db),
+):
+ if not authorization or not authorization.startswith("Bearer "):
+ raise HTTPException(status_code=401, detail="Missing token")
+
+ payload = decode_token(authorization[7:])
+ if not payload:
+ raise HTTPException(status_code=401, detail="Invalid token")
+
+ if payload.get("is_guest"):
+ raise HTTPException(status_code=403, detail="Guests cannot update profile")
+
+ result = await db.execute(select(User).where(User.id == payload["sub"]))
+ user = result.scalar_one_or_none()
+ if not user:
+ raise HTTPException(status_code=404, detail="User not found")
+
+ if data.username is not None:
+ user.username = data.username
+ if data.email is not None:
+ user.email = data.email
+
+ await db.flush()
+ return {
+ "id": str(user.id),
+ "phone": user.phone,
+ "username": user.username,
+ "email": user.email,
+ "tier": user.tier,
+ "role": user.role,
+ }
+
+
+@router.put("/password")
+async def change_password(
+ data: PasswordChange,
+ authorization: Optional[str] = Header(None, alias="Authorization"),
+ db: AsyncSession = Depends(get_db),
+):
+ if not authorization or not authorization.startswith("Bearer "):
+ raise HTTPException(status_code=401, detail="Missing token")
+
+ payload = decode_token(authorization[7:])
+ if not payload:
+ raise HTTPException(status_code=401, detail="Invalid token")
+
+ if payload.get("is_guest"):
+ raise HTTPException(status_code=403, detail="Guests cannot change password")
+
+ result = await db.execute(select(User).where(User.id == payload["sub"]))
+ user = result.scalar_one_or_none()
+ if not user:
+ raise HTTPException(status_code=404, detail="User not found")
+
+ if not verify_password(data.old_password, user.password_hash):
+ raise HTTPException(status_code=400, detail="旧密码不正确")
+
+ user.password_hash = hash_password(data.new_password)
+ await db.flush()
+ return {"message": "密码修改成功"}
+
+
@router.get("/wechat/config")
async def wechat_config():
from app.config import settings
diff --git a/backend/app/main.py b/backend/app/main.py
index 031da08..5c4c7df 100644
--- a/backend/app/main.py
+++ b/backend/app/main.py
@@ -54,7 +54,7 @@ async def health():
return {"status": "ok", "app": settings.APP_NAME, "version": "1.0.0"}
-from app.api.v1 import auth, marketing, translate, customer, quotation, whatsapp, product, exchange, push, admin, analytics, teams, onboarding, notification, feedback, payment, interaction, silent_pattern, training, followup
+from app.api.v1 import auth, marketing, translate, customer, quotation, whatsapp, product, exchange, push, admin, analytics, teams, onboarding, notification, feedback, payment, interaction, silent_pattern, training, followup, ai_assistant
app.include_router(auth.router, prefix="/api/v1/auth", tags=["auth"])
app.include_router(marketing.router, prefix="/api/v1/marketing", tags=["marketing"])
@@ -77,6 +77,7 @@ app.include_router(interaction.router, prefix="/api/v1/interaction", tags=["inte
app.include_router(silent_pattern.router, prefix="/api/v1/silent-pattern", tags=["silent-pattern"])
app.include_router(training.router, prefix="/api/v1/training", tags=["training"])
app.include_router(followup.router, prefix="/api/v1/followup", tags=["followup"])
+app.include_router(ai_assistant.router, prefix="/api/v1/ai", tags=["ai-assistant"])
if __name__ == "__main__":
diff --git a/backend/app/services/customer.py b/backend/app/services/customer.py
index 02757ce..0b3bb82 100644
--- a/backend/app/services/customer.py
+++ b/backend/app/services/customer.py
@@ -197,8 +197,11 @@ class CustomerService:
"whatsapp_id": c.whatsapp_id,
"source": c.source,
"tags": c.tags,
+ "notes": c.notes,
"status": c.status,
+ "estimated_value": c.estimated_value,
"last_contact_at": c.last_contact_at.isoformat() if c.last_contact_at else None,
"silence_days": (datetime.utcnow() - c.last_contact_at).days if c.last_contact_at else 0,
+ "next_followup_at": c.next_followup_at.isoformat() if c.next_followup_at else None,
"created_at": c.created_at.isoformat() if c.created_at else None,
}
diff --git a/uni-app/src/App.vue b/uni-app/src/App.vue
index 5ec656c..b18e9c6 100644
--- a/uni-app/src/App.vue
+++ b/uni-app/src/App.vue
@@ -1,10 +1,8 @@
-
diff --git a/uni-app/src/utils/api.js b/uni-app/src/utils/api.js
index b2187e1..d8f0988 100644
--- a/uni-app/src/utils/api.js
+++ b/uni-app/src/utils/api.js
@@ -1,7 +1,9 @@
+import { STORAGE_KEYS, PAGES } from '@/config.js'
+
export const BASE_URL = '/api/v1'
const getAuthHeader = () => {
- const token = uni.getStorageSync('token')
+ const token = uni.getStorageSync(STORAGE_KEYS.TOKEN)
return token ? { Authorization: `Bearer ${token}` } : {}
}
@@ -19,8 +21,8 @@ const request = (url, method = 'GET', data = {}) => {
if (res.statusCode === 200) {
resolve(res.data)
} else if (res.statusCode === 401) {
- uni.removeStorageSync('token')
- uni.reLaunch({ url: '/pages/login/login' })
+ uni.removeStorageSync(STORAGE_KEYS.TOKEN)
+ uni.reLaunch({ url: PAGES.LOGIN })
reject(new Error('Unauthorized'))
} else {
reject(new Error(res.data?.detail || 'Request failed'))
@@ -60,6 +62,8 @@ 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'),
+ updateProfile: (data) => request('/auth/me', 'PUT', data),
+ changePassword: (oldPassword, newPassword) => request('/auth/password', 'PUT', { old_password: oldPassword, new_password: newPassword }),
wechatLogin: (code) => request('/auth/wechat-login', 'POST', { code }),
wechatConfig: () => request('/auth/wechat/config'),
guestLogin: () => requestWithoutAuth('/auth/login/guest', 'POST'),
@@ -103,7 +107,7 @@ export const quotationApi = {
request('/quotations/generate-from-inquiry', 'POST', { inquiry_text: inquiryText, customer_id: customerId }),
importQuotations: (file) => {
return new Promise((resolve, reject) => {
- const token = uni.getStorageSync('token')
+ const token = uni.getStorageSync(STORAGE_KEYS.TOKEN)
uni.uploadFile({
url: `${BASE_URL}/quotations/import`,
filePath: file,
@@ -132,7 +136,7 @@ export const productApi = {
exportXlsx: () => `${BASE_URL}/products/export/xlsx`,
importProducts: (file) => {
return new Promise((resolve, reject) => {
- const token = uni.getStorageSync('token')
+ const token = uni.getStorageSync(STORAGE_KEYS.TOKEN)
uni.uploadFile({
url: `${BASE_URL}/products/import`,
filePath: file,
@@ -310,7 +314,7 @@ export const customerApi = {
exportXlsx: () => `${BASE_URL}/customers/export/xlsx`,
importCustomers: (file) => {
return new Promise((resolve, reject) => {
- const token = uni.getStorageSync('token')
+ const token = uni.getStorageSync(STORAGE_KEYS.TOKEN)
uni.uploadFile({
url: `${BASE_URL}/customers/import`,
filePath: file,