fix: comprehensive security, quality and consistency fixes
Server: - Block startup with default JWT secrets in production - Make subscription verify admin-only (no payment integration yet) - Add device ownership validation on command/result, event, treatment/sync - Remove admin token from request body fallback - Add pageParams boundary protection (pageSize capped at 100) - Fix COS getObjectUrl to use callback-based Promise - Add settings key whitelist matching frontend fields - Add user existence check before subscription creation - Fix firmware always returning has_update:true - Replace hardcoded trial subscription with actual DB query - Extract shared utilities (limitClause, toMysqlDate, formatDate) Miniprogram: - Replace fake PD random data with placeholder - Mark client-timer treatment completions with source field - Disable mock.js - Fix BLE listener leaks (save refs, cleanup in onUnload) - Fix ble.off clearing all listeners (pass specific callback) - Add BLE disconnect detection via onBLEConnectionStateChange - Fix subscription status type consistency (number not string) - Fix scan callback accumulation in ble.js - Fix history stats accumulation across pages - Fix subscribe-success/treatment-done hardcoded values - Fix profile subscription view logic - Replace purchase flow with admin-contact modal - Add error logging in command-sync report Admin console: - Fix AdminLayout logout (require->import, logout->clearToken) - Remove all mock data from production request.js - Replace dashboard fake data with real API calls - Replace monthly_revenue with subscription_count - Fix subscription stats fallback (|| -> ??) - Add token expiry tracking (7 days) - Unify device status map and subscription status text - Fix user page record link navigation - Fix subscription createForm.user_id type handling - Add error feedback in all empty catch blocks - Remove unused remember checkbox and uview-plus dependency - Extract common CSS to shared stylesheet (-900 lines) - Extract formatDate to shared utils/format.js - Show real admin name in layout header
这个提交包含在:
@@ -1,97 +1,8 @@
|
||||
import env from '../config/env'
|
||||
|
||||
const BASE_URL = env.API_BASE
|
||||
const USE_MOCK = false
|
||||
|
||||
const MOCK_DATA = {
|
||||
'/api/v1/admin/devices/AABBCCDDEEFF0011': {
|
||||
device_id: 'AABBCCDDEEFF0011', bound_user: 'user_001', battery: 85, fw_version: '1.0.0',
|
||||
activated_at: '2025-03-15T10:00:00Z', status: 2, total_usage: '45h', last_online: '2025-04-22T14:30:00Z',
|
||||
binding_history: [
|
||||
{ nickname: '张小姐', bound_at: '2025-03-15T10:00:00Z', unbound_at: null, status: 1 }
|
||||
]
|
||||
},
|
||||
'/api/v1/admin/dashboard': {
|
||||
device_count: 12,
|
||||
user_count: 86,
|
||||
treatment_count: 1234,
|
||||
subscription_count: 52
|
||||
},
|
||||
'/api/v1/admin/devices': {
|
||||
records: [
|
||||
{ device_id: 'AABBCCDDEEFF0011', bound_user: 'user_001', battery: 85, fw_version: '1.0.0', activated_at: '2025-03-15T10:00:00Z', status: 2 },
|
||||
{ device_id: '1122334455667788', bound_user: 'user_002', battery: 60, fw_version: '1.0.0', activated_at: '2025-03-20T14:00:00Z', status: 2 },
|
||||
{ device_id: 'A1B2C3D4E5F60011', bound_user: null, battery: null, fw_version: '1.0.0', activated_at: '2025-03-10T08:00:00Z', status: 1 }
|
||||
],
|
||||
total: 3
|
||||
},
|
||||
'/api/v1/admin/users': {
|
||||
records: [
|
||||
{ _id: 'u1', openid: 'oXXXX1', nickname: '张小姐', phone: '138****1234', created_at: '2025-03-01T08:00:00Z', subscription_status: 'yearly' },
|
||||
{ _id: 'u2', openid: 'oXXXX2', nickname: '李女士', phone: '139****5678', created_at: '2025-03-05T10:00:00Z', subscription_status: 'monthly' },
|
||||
{ _id: 'u3', openid: 'oXXXX3', nickname: '王先生', phone: '137****9012', created_at: '2025-03-10T12:00:00Z', subscription_status: 'trial' }
|
||||
],
|
||||
total: 3
|
||||
},
|
||||
'/api/v1/admin/users/u1': {
|
||||
_id: 'u1', openid: 'oXXXX1', nickname: '张小姐', phone: '138****1234',
|
||||
created_at: '2025-03-01T08:00:00Z', subscription_type: 'yearly', subscription_status: 'active',
|
||||
subscription_expire: '2026-03-01T08:00:00Z', treatment_count: 28, total_duration: '14h',
|
||||
total_spent: 899, devices: [{ device_id: 'AABBCCDDEEFF0011', device_name: '我的光面膜' }],
|
||||
recent_treatments: [
|
||||
{ started_at: '2025-04-20T10:00:00Z', total_duration_ms: 1200000, device_id: 'AABBCCDDEEFF0011' },
|
||||
{ started_at: '2025-04-18T09:00:00Z', total_duration_ms: 900000, device_id: 'AABBCCDDEEFF0011' }
|
||||
]
|
||||
},
|
||||
'/api/v1/admin/subscriptions': {
|
||||
records: [
|
||||
{ id: 's1', user_id: 'user_001', plan: 'yearly', amount: 899, started_at: '2025-03-01T00:00:00Z', expired_at: '2026-03-01T00:00:00Z', status: 1 },
|
||||
{ id: 's2', user_id: 'user_002', plan: 'monthly', amount: 99, started_at: '2025-04-01T00:00:00Z', expired_at: '2025-05-01T00:00:00Z', status: 1 },
|
||||
{ id: 's3', user_id: 'user_003', plan: 'trial', amount: '-', started_at: '2025-03-10T00:00:00Z', expired_at: '2025-03-17T00:00:00Z', status: 3 }
|
||||
],
|
||||
total: 3,
|
||||
stats: { monthly_count: 15, yearly_count: 30, trial_count: 7, monthly_revenue: 45890 }
|
||||
},
|
||||
'/api/v1/admin/records': {
|
||||
records: [
|
||||
{ started_at: '2025-04-20T10:00:00Z', total_duration_ms: 1200000, device_id: 'AABBCCDDEEFF0011', openid: 'oXXXX1' },
|
||||
{ started_at: '2025-04-19T15:00:00Z', total_duration_ms: 900000, device_id: '1122334455667788', openid: 'oXXXX2' }
|
||||
],
|
||||
total: 2
|
||||
},
|
||||
'/api/v1/admin/logs': {
|
||||
records: [
|
||||
{ created_at: '2025-04-20T10:05:00Z', action: 'treatment_complete', detail: '用户张小姐完成护理', openid: 'oXXXX1' },
|
||||
{ created_at: '2025-04-20T09:00:00Z', action: 'device_bind', detail: '设备AABBCCDDEEFF0011绑定', openid: 'oXXXX1' }
|
||||
],
|
||||
total: 2
|
||||
}
|
||||
}
|
||||
|
||||
function getMockData(url, data) {
|
||||
if (url.includes('/api/v1/admin/login')) {
|
||||
if (data && data.username === 'admin' && data.password === 'admin123') {
|
||||
return { token: 'mock_token_admin', admin_id: 'admin_001', username: 'admin', real_name: '管理员', role: 'admin' }
|
||||
}
|
||||
throw { code: 1001, message: '用户名或密码错误' }
|
||||
}
|
||||
if (url.startsWith('/api/v1/admin/users/') && !url.includes('users?')) {
|
||||
const id = url.split('/').pop()
|
||||
return MOCK_DATA['/api/v1/admin/users/u1'] || { _id: id, nickname: '未知用户' }
|
||||
}
|
||||
for (const key of Object.keys(MOCK_DATA)) {
|
||||
if (url.includes(key)) return MOCK_DATA[key]
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
function request(options) {
|
||||
if (USE_MOCK) {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => resolve(getMockData(options.url, options.data)), 200)
|
||||
})
|
||||
}
|
||||
|
||||
const token = uni.getStorageSync('admin_token')
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
在新工单中引用
屏蔽一个用户