feat: init project with miniprogram, cloud functions and admin console
这个提交包含在:
@@ -0,0 +1,308 @@
|
||||
var db = require('../../common/db')
|
||||
var auth = require('../../common/auth')
|
||||
var log = require('../../common/log')
|
||||
var RESPONSE = require('../../common/response').RESPONSE
|
||||
var ERROR_CODES = require('../../common/response').ERROR_CODES
|
||||
var bcrypt = require('bcryptjs')
|
||||
|
||||
exports.main_handler = async function (event) {
|
||||
try {
|
||||
var method = event.httpMethod || 'GET'
|
||||
var path = event.path || ''
|
||||
var pathParams = event.pathParameters || {}
|
||||
var body = parseBody(event)
|
||||
|
||||
if (method === 'POST' && path === '/api/v1/admin/login') {
|
||||
return await adminLogin(body, event)
|
||||
}
|
||||
|
||||
var admin = auth.extractAdmin(event)
|
||||
if (!admin) return RESPONSE.error(ERROR_CODES.INVALID_TOKEN.code, ERROR_CODES.INVALID_TOKEN.message)
|
||||
|
||||
if (method === 'GET' && path === '/api/v1/admin/dashboard') {
|
||||
return await getDashboard()
|
||||
}
|
||||
if (method === 'GET' && path === '/api/v1/admin/devices' && !pathParams.device_id) {
|
||||
return await listDevices(body)
|
||||
}
|
||||
if (method === 'GET' && pathParams.device_id && !path.match(/\/command$/)) {
|
||||
return await getDeviceDetail(pathParams.device_id)
|
||||
}
|
||||
if (method === 'POST' && path.match(/\/command$/)) {
|
||||
return await sendDeviceCommand(pathParams.device_id, body)
|
||||
}
|
||||
if (method === 'GET' && path === '/api/v1/admin/users' && !pathParams.user_id) {
|
||||
return await listUsers(body)
|
||||
}
|
||||
if (method === 'GET' && pathParams.user_id && path.indexOf('/admin/users/') !== -1) {
|
||||
return await getUserDetail(pathParams.user_id)
|
||||
}
|
||||
if (method === 'GET' && path === '/api/v1/admin/subscriptions') {
|
||||
return await listSubscriptions(body)
|
||||
}
|
||||
if (method === 'POST' && path === '/api/v1/admin/subscriptions') {
|
||||
return await createSubscription(event, body)
|
||||
}
|
||||
if (method === 'GET' && path === '/api/v1/admin/logs') {
|
||||
return await getLogs(body)
|
||||
}
|
||||
|
||||
return RESPONSE.error(ERROR_CODES.PARAM_ERROR.code, 'unknown route')
|
||||
} catch (err) {
|
||||
console.error('admin error:', err)
|
||||
return RESPONSE.error(ERROR_CODES.SERVER_ERROR.code, err.message)
|
||||
}
|
||||
}
|
||||
|
||||
async function adminLogin(body, event) {
|
||||
if (!body.username || !body.password) {
|
||||
return RESPONSE.error(ERROR_CODES.PARAM_ERROR.code, 'missing username or password')
|
||||
}
|
||||
|
||||
var admins = await db.query(
|
||||
'SELECT id, username, password_hash, role, real_name FROM admins WHERE username = ? AND status = 1 AND deleted_at IS NULL LIMIT 1',
|
||||
[body.username]
|
||||
)
|
||||
if (admins.length === 0) {
|
||||
return RESPONSE.error(ERROR_CODES.PERMISSION_DENIED.code, 'invalid credentials')
|
||||
}
|
||||
|
||||
var admin = admins[0]
|
||||
var valid = bcrypt.compareSync(body.password, admin.password_hash)
|
||||
if (!valid) {
|
||||
return RESPONSE.error(ERROR_CODES.PERMISSION_DENIED.code, 'invalid credentials')
|
||||
}
|
||||
|
||||
await db.update(
|
||||
'UPDATE admins SET last_login_at = NOW() WHERE id = ?',
|
||||
[admin.id]
|
||||
)
|
||||
|
||||
var token = auth.generateAdminToken(admin.id, admin.username, admin.role)
|
||||
|
||||
await log.writeLog({
|
||||
operator_type: 1,
|
||||
operator_id: admin.id,
|
||||
target_type: 'admin',
|
||||
target_id: String(admin.id),
|
||||
action: 'login',
|
||||
ip_address: getClientIp(event)
|
||||
})
|
||||
|
||||
return RESPONSE.success({
|
||||
token: token,
|
||||
admin_id: String(admin.id),
|
||||
username: admin.username,
|
||||
real_name: admin.real_name,
|
||||
role: admin.role
|
||||
})
|
||||
}
|
||||
|
||||
async function getDashboard() {
|
||||
var userCount = await db.query('SELECT COUNT(*) AS cnt FROM users WHERE deleted_at IS NULL')
|
||||
var deviceCount = await db.query('SELECT COUNT(*) AS cnt FROM devices WHERE deleted_at IS NULL')
|
||||
var activeDevices = await db.query('SELECT COUNT(*) AS cnt FROM devices WHERE status = 2 AND deleted_at IS NULL')
|
||||
var subCount = await db.query('SELECT COUNT(*) AS cnt FROM subscriptions WHERE status = 1 AND expired_at > NOW()')
|
||||
var trialCount = await db.query('SELECT COUNT(*) AS cnt FROM subscriptions WHERE type = 1')
|
||||
var treatmentCount = await db.query('SELECT COUNT(*) AS cnt FROM treatment_records')
|
||||
var todayTreatments = await db.query(
|
||||
"SELECT COUNT(*) AS cnt FROM treatment_records WHERE DATE(created_at) = CURDATE()"
|
||||
)
|
||||
|
||||
return RESPONSE.success({
|
||||
user_count: userCount[0].cnt,
|
||||
device_count: deviceCount[0].cnt,
|
||||
active_device_count: activeDevices[0].cnt,
|
||||
subscription_count: subCount[0].cnt,
|
||||
trial_count: trialCount[0].cnt,
|
||||
treatment_count: treatmentCount[0].cnt,
|
||||
today_treatment_count: todayTreatments[0].cnt
|
||||
})
|
||||
}
|
||||
|
||||
async function listDevices(body) {
|
||||
var page = parseInt(body.page) || 1
|
||||
var pageSize = parseInt(body.page_size) || 20
|
||||
var offset = (page - 1) * pageSize
|
||||
|
||||
var total = await db.query('SELECT COUNT(*) AS cnt FROM devices WHERE deleted_at IS NULL')
|
||||
var devices = await db.query(
|
||||
'SELECT d.id, d.device_id, d.device_name, d.device_type, d.hw_version, d.fw_version, d.status, d.activated_at, d.created_at, ' +
|
||||
'u.nickname AS bound_user, b.bound_at ' +
|
||||
'FROM devices d ' +
|
||||
'LEFT JOIN bindings b ON d.id = b.device_id AND b.status = 1 ' +
|
||||
'LEFT JOIN users u ON b.user_id = u.id AND u.deleted_at IS NULL ' +
|
||||
'WHERE d.deleted_at IS NULL ' +
|
||||
'ORDER BY d.created_at DESC LIMIT ? OFFSET ?',
|
||||
[pageSize, offset]
|
||||
)
|
||||
|
||||
return RESPONSE.paginate(devices, total[0].cnt, page, pageSize)
|
||||
}
|
||||
|
||||
async function getDeviceDetail(deviceId) {
|
||||
var devices = await db.query(
|
||||
'SELECT * FROM devices WHERE device_id = ? AND deleted_at IS NULL LIMIT 1',
|
||||
[deviceId]
|
||||
)
|
||||
if (devices.length === 0) {
|
||||
return RESPONSE.error(ERROR_CODES.DEVICE_NOT_FOUND.code, ERROR_CODES.DEVICE_NOT_FOUND.message)
|
||||
}
|
||||
|
||||
var bindings = await db.query(
|
||||
'SELECT b.*, u.nickname, u.avatar_url FROM bindings b JOIN users u ON b.user_id = u.id WHERE b.device_id = ? ORDER BY b.bound_at DESC',
|
||||
[devices[0].id]
|
||||
)
|
||||
|
||||
return RESPONSE.success(Object.assign({}, devices[0], { binding_history: bindings }))
|
||||
}
|
||||
|
||||
async function sendDeviceCommand(deviceId, body) {
|
||||
var devices = await db.query(
|
||||
'SELECT id, device_id, product_id, device_name FROM devices WHERE device_id = ? AND deleted_at IS NULL LIMIT 1',
|
||||
[deviceId]
|
||||
)
|
||||
if (devices.length === 0) {
|
||||
return RESPONSE.error(ERROR_CODES.DEVICE_NOT_FOUND.code, ERROR_CODES.DEVICE_NOT_FOUND.message)
|
||||
}
|
||||
|
||||
return RESPONSE.success({ message: 'command queued', device_id: deviceId, command: body })
|
||||
}
|
||||
|
||||
async function listUsers(body) {
|
||||
var page = parseInt(body.page) || 1
|
||||
var pageSize = parseInt(body.page_size) || 20
|
||||
var offset = (page - 1) * pageSize
|
||||
|
||||
var total = await db.query('SELECT COUNT(*) AS cnt FROM users WHERE deleted_at IS NULL')
|
||||
var users = await db.query(
|
||||
'SELECT u.id AS user_id, u.nickname, u.avatar_url, u.phone, u.status, u.created_at, ' +
|
||||
'COUNT(DISTINCT b.id) AS device_count, ' +
|
||||
'MAX(s.expired_at) AS subscription_expire ' +
|
||||
'FROM users u ' +
|
||||
'LEFT JOIN bindings b ON u.id = b.user_id AND b.status = 1 ' +
|
||||
'LEFT JOIN subscriptions s ON u.id = s.user_id AND s.status = 1 AND s.expired_at > NOW() ' +
|
||||
'WHERE u.deleted_at IS NULL ' +
|
||||
'GROUP BY u.id ' +
|
||||
'ORDER BY u.created_at DESC LIMIT ? OFFSET ?',
|
||||
[pageSize, offset]
|
||||
)
|
||||
|
||||
return RESPONSE.paginate(users, total[0].cnt, page, pageSize)
|
||||
}
|
||||
|
||||
async function getUserDetail(userId) {
|
||||
var users = await db.query(
|
||||
'SELECT id AS user_id, openid, nickname, avatar_url, phone, status, created_at FROM users WHERE id = ? AND deleted_at IS NULL LIMIT 1',
|
||||
[userId]
|
||||
)
|
||||
if (users.length === 0) {
|
||||
return RESPONSE.error(ERROR_CODES.USER_NOT_FOUND.code, ERROR_CODES.USER_NOT_FOUND.message)
|
||||
}
|
||||
|
||||
var bindings = await db.query(
|
||||
'SELECT b.bound_at, d.device_id, d.device_name, d.fw_version FROM bindings b JOIN devices d ON b.device_id = d.id WHERE b.user_id = ? AND b.status = 1',
|
||||
[userId]
|
||||
)
|
||||
|
||||
var subs = await db.query(
|
||||
'SELECT type, plan, status, started_at, expired_at, source FROM subscriptions WHERE user_id = ? ORDER BY created_at DESC LIMIT 5',
|
||||
[userId]
|
||||
)
|
||||
|
||||
var treatments = await db.query(
|
||||
'SELECT session_id, regions, total_duration_ms, mode, started_at, ended_at FROM treatment_records WHERE user_id = ? ORDER BY started_at DESC LIMIT 5',
|
||||
[userId]
|
||||
)
|
||||
|
||||
return RESPONSE.success(Object.assign({}, users[0], {
|
||||
devices: bindings,
|
||||
subscriptions: subs,
|
||||
recent_treatments: treatments
|
||||
}))
|
||||
}
|
||||
|
||||
async function listSubscriptions(body) {
|
||||
var page = parseInt(body.page) || 1
|
||||
var pageSize = parseInt(body.page_size) || 20
|
||||
var offset = (page - 1) * pageSize
|
||||
|
||||
var total = await db.query('SELECT COUNT(*) AS cnt FROM subscriptions')
|
||||
var subs = await db.query(
|
||||
'SELECT s.id, s.user_id, u.nickname, s.type, s.plan, s.status, s.trial_used, s.started_at, s.expired_at, s.source ' +
|
||||
'FROM subscriptions s ' +
|
||||
'JOIN users u ON s.user_id = u.id ' +
|
||||
'ORDER BY s.created_at DESC LIMIT ? OFFSET ?',
|
||||
[pageSize, offset]
|
||||
)
|
||||
|
||||
return RESPONSE.paginate(subs, total[0].cnt, page, pageSize)
|
||||
}
|
||||
|
||||
async function createSubscription(event, body) {
|
||||
if (!body.user_id || !body.plan || !body.days) {
|
||||
return RESPONSE.error(ERROR_CODES.PARAM_ERROR.code, 'missing user_id, plan or days')
|
||||
}
|
||||
|
||||
await db.insert(
|
||||
'INSERT INTO subscriptions (user_id, type, plan, status, started_at, expired_at, source) VALUES (?, 2, ?, 1, NOW(), DATE_ADD(NOW(), INTERVAL ? DAY), ?)',
|
||||
[body.user_id, body.plan, body.days, body.source || 'manual_create']
|
||||
)
|
||||
|
||||
await log.writeLog({
|
||||
operator_type: 1,
|
||||
operator_id: event.admin_id,
|
||||
target_type: 'subscription',
|
||||
target_id: String(body.user_id),
|
||||
action: 'create',
|
||||
remark: 'plan=' + body.plan + ',days=' + body.days
|
||||
})
|
||||
|
||||
return RESPONSE.success({ message: 'success' })
|
||||
}
|
||||
|
||||
async function getLogs(body) {
|
||||
var page = parseInt(body.page) || 1
|
||||
var pageSize = parseInt(body.page_size) || 20
|
||||
var offset = (page - 1) * pageSize
|
||||
|
||||
var where = '1=1'
|
||||
var params = []
|
||||
|
||||
if (body.type) {
|
||||
where += ' AND target_type = ?'
|
||||
params.push(body.type)
|
||||
}
|
||||
if (body.device_id) {
|
||||
where += ' AND target_id = ?'
|
||||
params.push(body.device_id)
|
||||
}
|
||||
if (body.start_time) {
|
||||
where += ' AND created_at >= ?'
|
||||
params.push(body.start_time)
|
||||
}
|
||||
if (body.end_time) {
|
||||
where += ' AND created_at <= ?'
|
||||
params.push(body.end_time)
|
||||
}
|
||||
|
||||
var total = await db.query('SELECT COUNT(*) AS cnt FROM operation_logs WHERE ' + where, params)
|
||||
var logs = await db.query(
|
||||
'SELECT * FROM operation_logs WHERE ' + where + ' ORDER BY created_at DESC LIMIT ? OFFSET ?',
|
||||
params.concat([pageSize, offset])
|
||||
)
|
||||
|
||||
return RESPONSE.paginate(logs, total[0].cnt, page, pageSize)
|
||||
}
|
||||
|
||||
function parseBody(event) {
|
||||
if (event.body) {
|
||||
try { return JSON.parse(event.body) } catch (e) { return {} }
|
||||
}
|
||||
return event.queryStringParameters || {}
|
||||
}
|
||||
|
||||
function getClientIp(event) {
|
||||
var headers = event.headers || {}
|
||||
return headers['x-forwarded-for'] || headers['X-Forwarded-For'] || ''
|
||||
}
|
||||
在新工单中引用
屏蔽一个用户