Files
zhuomi-hui/server/lib/serialize.js
T
Michelle ZanandCursor cd05113755 Add 卓米会 mini program and admin server.
Initial commit of the WeChat mini program, Express backend, and admin console.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-29 19:01:36 +08:00

133 lines
3.8 KiB
JavaScript

const { uid, str, has } = require('./http')
const { isAdmin } = require('./auth')
const STATUSES = ['current', 'past', 'draft']
const DEFAULT_BRAND = '卓米会 ZHUOMI'
function publicActivity(activity, posters) {
const poster = (posters || [])
.filter((item) => item.activityId === activity.id && item.enabled && item.image)
.sort((a, b) => a.sort - b.sort)[0]
return {
id: activity.id,
title: activity.title,
brandName: activity.brandName || DEFAULT_BRAND,
brandDesc: activity.brandDesc || '',
startAt: activity.startAt || '',
address: activity.address || '',
latitude: activity.latitude,
longitude: activity.longitude,
status: activity.status,
isCurrent: !!activity.isCurrent,
pinned: !!activity.pinned,
pinnedAt: activity.pinnedAt || '',
theme: activity.theme || 0,
cover: activity.cover || '',
video: activity.video || '',
poster: (poster && poster.image) || activity.cover || '',
createdAt: activity.createdAt
}
}
function currentActivity(data) {
return (
data.activities.find((item) => item.isCurrent && item.status !== 'draft') ||
data.activities.find((item) => item.status === 'current') ||
null
)
}
function byPinnedThenNewest(a, b) {
const ap = a.pinned ? 1 : 0
const bp = b.pinned ? 1 : 0
if (ap !== bp) return bp - ap
if (ap) return String(b.pinnedAt || '').localeCompare(String(a.pinnedAt || ''))
return String(b.createdAt).localeCompare(String(a.createdAt))
}
function pastActivities(data) {
return data.activities.filter((item) => item.status === 'past').sort(byPinnedThenNewest)
}
function nextPastTheme(data) {
return data.activities.filter((item) => item.status === 'past').length % 2
}
function setPinned(activity, pinned) {
const now = new Date().toISOString()
activity.pinned = !!pinned
activity.pinnedAt = pinned ? now : ''
activity.updatedAt = now
return activity
}
const PROFILE_FIELDS = ['nickName', 'avatarUrl', 'name', 'phone', 'company', 'position']
function publicProfile(user, data) {
const profile = { openid: user.openid }
PROFILE_FIELDS.forEach((key) => {
profile[key] = user[key] || ''
})
profile.isAdmin = isAdmin(user.openid, data)
return profile
}
function coordinate(value) {
if (value === '' || value == null) return null
const num = Number(value)
return Number.isFinite(num) ? num : null
}
function activityPayload(body, prev, data) {
const now = new Date().toISOString()
const status = STATUSES.includes(body.status) ? body.status : 'draft'
const keep = (key, fallback) => (has(body, key) ? str(body[key]) : prev ? prev[key] || fallback : fallback)
let pinned = prev ? !!prev.pinned : false
let pinnedAt = prev ? prev.pinnedAt || '' : ''
if (status !== 'past') {
pinned = false
pinnedAt = ''
} else if (has(body, 'pinned')) {
const requested = body.pinned === true || body.pinned === 'on' || body.pinned === 'true'
if (requested !== pinned) pinnedAt = requested ? now : ''
pinned = requested
}
let theme = Number(body.theme)
if (!has(body, 'theme') || !Number.isFinite(theme)) {
theme = prev ? prev.theme || 0 : status === 'past' ? nextPastTheme(data) : 0
}
return {
id: prev ? prev.id : uid('a'),
title: str(body.title, 60),
brandName: str(body.brandName) || DEFAULT_BRAND,
brandDesc: str(body.brandDesc, 300),
cover: keep('cover', ''),
video: keep('video', ''),
startAt: str(body.startAt, 32),
address: str(body.address, 120),
latitude: coordinate(body.latitude),
longitude: coordinate(body.longitude),
status,
isCurrent: !!body.isCurrent,
pinned,
pinnedAt,
theme,
createdAt: prev ? prev.createdAt : now,
updatedAt: now
}
}
module.exports = {
publicActivity,
currentActivity,
pastActivities,
nextPastTheme,
setPinned,
publicProfile,
activityPayload,
DEFAULT_BRAND
}