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
这个提交包含在:
@@ -44,7 +44,7 @@
|
||||
<text class="t-td flex2">{{ formatDate(item.created_at) }}</text>
|
||||
<text class="t-td flex1">
|
||||
<text class="action-link" @click="onDetail(item.user_id)">详情</text>
|
||||
<text class="action-link" @click="onDetail(item.user_id)">记录</text>
|
||||
<text class="action-link" @click="onViewRecords(item.user_id)">记录</text>
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -63,6 +63,7 @@
|
||||
<script>
|
||||
import { get } from '../../utils/request'
|
||||
import { exportCSV } from '../../utils/export'
|
||||
import { formatDateShort } from '../../utils/format'
|
||||
import AdminLayout from '../../components/AdminLayout.vue'
|
||||
|
||||
export default {
|
||||
@@ -90,7 +91,9 @@ export default {
|
||||
const data = await get('/api/v1/admin/users', { page: this.page, page_size: this.pageSize, keyword: this.keyword })
|
||||
this.users = data.records || []
|
||||
this.total = data.total || 0
|
||||
} catch (e) {}
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
onSearch() {
|
||||
this.page = 1
|
||||
@@ -103,8 +106,11 @@ export default {
|
||||
onDetail(userId) {
|
||||
uni.navigateTo({ url: '/pages/user-detail/index?user_id=' + userId })
|
||||
},
|
||||
onViewRecords(userId) {
|
||||
uni.navigateTo({ url: '/pages/record/index?user_id=' + userId })
|
||||
},
|
||||
subStatusText(status) {
|
||||
const map = { yearly: '年卡', monthly: '月卡', trial: '试用中', none: '未订阅' }
|
||||
const map = { yearly: '年卡会员', monthly: '月卡会员', trial: '试用中', none: '未订阅' }
|
||||
return map[status] || '未订阅'
|
||||
},
|
||||
subBadge(status) {
|
||||
@@ -116,9 +122,7 @@ export default {
|
||||
}
|
||||
return map[status] || 'badge badge-default'
|
||||
},
|
||||
formatDate(d) {
|
||||
return d ? d.slice(0, 10) : '-'
|
||||
},
|
||||
formatDate: formatDateShort,
|
||||
async onExport() {
|
||||
try {
|
||||
const data = await get('/api/v1/admin/users', { page: 1, page_size: 9999, keyword: this.keyword })
|
||||
@@ -139,97 +143,7 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.toolbar {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 12px 20px;
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.page-card {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 220px;
|
||||
height: 32px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 6px;
|
||||
padding: 0 12px;
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.input-placeholder {
|
||||
color: #bfbfbf;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #E6508C;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-default {
|
||||
background: #fff;
|
||||
color: #333;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
height: 32px;
|
||||
padding: 0 16px;
|
||||
font-size: 14px;
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
.data-table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.t-header {
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.t-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.t-th {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.t-td {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.flex1 {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.flex2 {
|
||||
flex: 2;
|
||||
}
|
||||
@import '../../styles/common.css';
|
||||
|
||||
.user-cell {
|
||||
display: flex;
|
||||
@@ -249,75 +163,7 @@ export default {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.badge-success {
|
||||
background: #f6ffed;
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.badge-warning {
|
||||
background: #fffbe6;
|
||||
color: #faad14;
|
||||
}
|
||||
|
||||
.badge-blue {
|
||||
background: #e6f7ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.badge-default {
|
||||
background: #f5f5f5;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.action-link {
|
||||
color: #E6508C;
|
||||
font-size: 14px;
|
||||
margin-right: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.btn-page {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 6px;
|
||||
background: #fff;
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-page.active {
|
||||
background: #E6508C;
|
||||
color: #fff;
|
||||
border-color: #E6508C;
|
||||
}
|
||||
|
||||
.btn-page[disabled] {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.page-info {
|
||||
font-size: 13px;
|
||||
color: #999;
|
||||
margin-left: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
在新工单中引用
屏蔽一个用户