const { query, one, transaction, limitClause } = require('../lib/db') /** * Find user by WeChat openid * @param {string} openid * @returns {Promise} */ async function findByOpenid(openid) { return one( 'SELECT * FROM users WHERE openid = :openid', { openid } ) } /** * Create a new user from WeChat login * @param {string} openid * @returns {Promise} query result with insertId */ async function create(openid) { return query( 'INSERT INTO users (openid, nickname, avatar, status) VALUES (:openid, :nickname, :avatar, 1)', { openid, nickname: '', avatar: '' } ) } /** * Find user by user_id * @param {number} userId * @returns {Promise} */ async function findById(userId) { return one( 'SELECT * FROM users WHERE user_id = :user_id', { user_id: userId } ) } /** * Find active user by user_id (status=1) * @param {number} userId * @returns {Promise} */ async function findActiveById(userId) { return one( 'SELECT * FROM users WHERE user_id = :user_id AND status = 1', { user_id: userId } ) } /** * Update user profile fields (nickname, avatar, gender) * @param {number} userId * @param {Object} fields * @param {string|null} [fields.nickname] * @param {string|null} [fields.avatar] * @param {number|null} [fields.gender] * @returns {Promise} query result */ async function updateProfile(userId, fields) { return query( 'UPDATE users SET nickname = COALESCE(:nickname, nickname), avatar = COALESCE(:avatar, avatar), gender = COALESCE(:gender, gender) WHERE user_id = :user_id', { user_id: userId, nickname: fields.nickname || null, avatar: fields.avatar || null, gender: fields.gender === undefined ? null : fields.gender } ) } /** * Update user phone number * @param {number} userId * @param {string} phone * @returns {Promise} query result */ async function updatePhone(userId, phone) { return query( 'UPDATE users SET phone = :phone WHERE user_id = :user_id', { user_id: userId, phone } ) } /** * Admin paginated user list with device/treatment/subscription subquery stats * @param {Object} opts * @param {string} [opts.keyword] - search nickname, phone, or exact user_id * @param {number} opts.pageSize * @param {number} opts.offset * @returns {Promise<{records: Array, total: number}>} */ async function listAdmin({ keyword, pageSize, offset }) { let where = '' const params = {} if (keyword) { where = ' WHERE u.nickname LIKE :kw OR u.phone LIKE :kw OR u.user_id = :keyword' params.kw = '%' + keyword + '%' params.keyword = keyword } const total = await query('SELECT COUNT(*) AS total FROM users u' + where, params) const records = await query( 'SELECT u.*,' + ' (SELECT COUNT(*) FROM bindings WHERE user_id = u.user_id AND bind_status = 1) AS device_count,' + ' (SELECT COUNT(*) FROM treatment_records WHERE user_id = u.user_id) AS treatment_count,' + ' COALESCE((SELECT status FROM subscriptions WHERE user_id = u.user_id AND status = 1 AND expire_time > NOW() ORDER BY expire_time DESC LIMIT 1), 0) AS subscription_status' + ' FROM users u' + where + ' ORDER BY u.created_at DESC' + limitClause(pageSize, offset), params ) return { records, total: total[0].total } } /** * Count admin users with optional keyword * @param {Object} opts * @param {string} [opts.keyword] * @returns {Promise} */ async function countAdmin({ keyword }) { let where = '' const params = {} if (keyword) { where = ' WHERE u.nickname LIKE :kw OR u.phone LIKE :kw OR u.user_id = :keyword' params.kw = '%' + keyword + '%' params.keyword = keyword } const rows = await query('SELECT COUNT(*) AS total FROM users u' + where, params) return rows[0].total } /** * Admin detail view: user + bound devices, recent treatments, subscription, stats * @param {number} userId * @returns {Promise} enriched user object or null */ async function findByIdAdmin(userId) { const user = await one('SELECT * FROM users WHERE user_id = :user_id', { user_id: userId }) if (!user) return null const devices = await query( 'SELECT d.device_id, d.device_name FROM bindings b JOIN devices d ON d.device_id = b.device_id WHERE b.user_id = :user_id AND b.bind_status = 1', { user_id: userId } ) const treatments = await query( 'SELECT * FROM treatment_records WHERE user_id = :user_id ORDER BY created_at DESC LIMIT 5', { user_id: userId } ) const subscription = await one( 'SELECT plan, status, start_time, expire_time FROM subscriptions WHERE user_id = :user_id AND status = 1 AND expire_time > NOW() ORDER BY expire_time DESC LIMIT 1', { user_id: userId } ) const stats = await one( 'SELECT COUNT(*) AS treatment_count, COALESCE(SUM(total_duration_ms), 0) AS total_duration FROM treatment_records WHERE user_id = :user_id', { user_id: userId } ) return Object.assign({}, user, { devices, recent_treatments: treatments, subscription_status: subscription ? subscription.status : 0, subscription_type: subscription ? subscription.plan : null, subscription_expire: subscription ? subscription.expire_time : null, treatment_count: stats ? stats.treatment_count : 0, total_duration: stats ? stats.total_duration : 0 }) } /** * Update user status. * @param {number} userId * @param {number} status - 1=active, 2=disabled, 3=cancelled * @returns {Promise} query result */ async function updateStatus(userId, status) { return query( 'UPDATE users SET status = :status WHERE user_id = :user_id', { user_id: userId, status } ) } /** * Soft-cancel a user account and unbind all active devices. * Historical treatment/subscription/log rows are kept. * @param {number} userId * @returns {Promise<{userAffectedRows: number, unboundRows: number}>} */ async function deactivate(userId) { return transaction(async conn => { const [userResult] = await conn.execute( 'UPDATE users SET status = 3 WHERE user_id = ? AND status <> 3', [userId] ) const [unbindResult] = await conn.execute( 'UPDATE bindings SET bind_status = 2, unbind_time = NOW() WHERE user_id = ? AND bind_status = 1', [userId] ) return { userAffectedRows: userResult.affectedRows || 0, unboundRows: unbindResult.affectedRows || 0 } }) } module.exports = { findByOpenid, create, findById, findActiveById, updateProfile, updatePhone, listAdmin, countAdmin, findByIdAdmin, updateStatus, deactivate }