Replace '光子美容仪' with '智美'/'智能面膜'/'我的设备' throughout.
Replace '护理' with '使用' in all UI text.
Replace '皮肤' with '检测'/'智能' where applicable.
Replace 🌸 with 💎 for brand identity.
Internal code (variable names, API paths, file names) unchanged.
- New grantTrialIfEligible() in subscription.dao.js with SELECT FOR UPDATE
to prevent concurrent bind race condition granting double trials
- confirmBind + mockBind now call the shared function (was duplicated)
- Trial days no longer hardcoded to 7 in binding — respects settings
confirmBind and mockBind now check trial history before granting trial.
Previously only checked for active subscriptions — if trial expired,
rebinding would create a new 7-day trial, allowing infinite free usage.
Now checks if user ever had a trial (any status), skips if so.
- ADC (17-byte) now emits 'adc' + 'battery' events, not 'status'
- Fixes CRITICAL timer jump: remaining_ms:0 was resetting countdown
- Drop ADC packets with bad XOR checksum
- treating.js + index.js subscribe to 'battery' for live battery updates
- onStatus only updates remaining_ms when > 0
Protocol update adds FFE4 Status format: 7×PD sensors (little-endian)
+ VBAT battery voltage (mV) + XOR checksum = 17 bytes.
- parseVendorStatus: handles 1-byte heartbeat, 17-byte ADC, 33-byte params
- handleValueChange: ADC type extracts PD array + battery % from VBAT
- Battery calculated as linear 3000mV(0%) to 4200mV(100%)
- PD data passed through status event for treatment pages
- Updated protocol doc with full byte table and old/new comparison
- 01-快速开始: local setup for all 3 modules, common issues
- 02-配置说明: all env vars, WeChat/Pay/DB/COS config details
- 03-架构说明: system overview, directory structure, data flows
- 04-部署指南: Tencent Cloud SCF/COS deployment, launch checklist
- 05-API接口文档: all 42 endpoints with params and response format
- README index with audience guide and quick links
- Loading animation only shows on first visit, subsequent onShow updates silently
- Subscription row always navigates to plans page (was no-op when active)
- Move devMode init to onLoad (only needs to run once)
- Device card: top row with icon+name+badge, reconnect button below
- Action buttons wrapped in flex column with 20rpx gap
- Removed inline device-battery, now part of device-meta row
- httpsRequest: 15s timeout, prevents hanging on WeChat API outage
- fetchPlatformCertificates: keeps old certs if refresh fails,
only throws if no certs at all (first time failure)
- Replace literal '\n' with real newlines when reading private key from env
- Add PEM header validation to catch format errors early
- File-based key (privateKeyPath) unaffected
- closeStaleOrders: marks payment orders older than 24h as 'closed'
- closeExpiredPending: marks expired binding requests (bind_status=3) as cancelled
- Both run on app startup (SCF cold start), no cron needed
- countPendingByUser already filters to 2h window (previous commit)
CRITICAL fixes:
- C1: Notify amount validation now unconditional (was skippable if amount field missing)
- C2: Sync endpoint validates payer_total against order amount before activation
- C3: Order ID uses crypto.randomBytes(6) instead of Math.random (collision-safe)
HIGH fixes:
- H1: Payment endpoints rate limited to 5/min per IP
- H2: Max 5 pending orders per user, reject new ones until completed/cancelled
- H3: Purchase endpoint returns error (not mock) when wxpay unconfigured in production
Also fixed:
- Notify handler asserts Buffer body, rejects non-Buffer (L3)
- Notify error response is generic, no internal message leak (M1)
- Private key cached in memory after first read (L2)
- Fixed duplicate paymentOrderDao const declaration
1. Move notify route before authMiddleware (WeChat callback has no JWT)
2. Add await to verifyNotifySignature call (was fire-and-forget)
3. Remove dangerous verify fallback — all signature failures now throw
4. Payment sync polls 3x before giving up, never redirects to success
page unless confirmed paid
5. Production guard enforces all WX_MCH_* env vars on startup
Server:
- New lib/wxpay.js: native crypto RSA-SHA256 signing, JSAPI prepay,
AES-256-GCM notify decryption, order query (no npm deps)
- New dao/payment-order.dao.js: createOrder, markPrepay,
markPaidAndActivateSubscription (idempotent + transactional)
- New routes/payment.js: GET order status, POST order sync
- New routes/payment-notify.js: WeChat async callback handler with
signature verification, amount/appid/mchid validation
- Modified subscription/purchase: auto-detects wxpay config, returns
real payment_params or mock fallback
- Schema: payment_orders table with out_trade_no unique key
- app.js: express.raw() for notify path, payment routes mounted
- config.js: wxpay block with 7 env vars
- .env.example: all WeChat Pay fields documented
- .gitignore: certs/, *.pem, *.p12
Miniprogram:
- subscribe-plans doPurchase: calls real purchase API, falls back to
mockPurchase only when server returns mock:true
- Added syncAndRedirect for post-payment order confirmation
- api.js: getPaymentOrder, syncPaymentOrder
- Removed "模拟支付"/"测试环境" from UI text
Miniprogram:
- Add BLE reconnect button on home page when disconnected
- Add loading states to index, profile, subscribe-plans pages
- Add profile editing (avatar + nickname) with COS upload
- Enable pull-to-refresh on history page
- Fix auto-scan self.options → self.data inconsistency
- Add console.error to silent catch blocks
- Gate 'Simple Peripheral' BLE scan behind __DEV__ flag
- Add production config comment to env.js
Admin console:
- Add empty state '暂无数据' to all 5 list views
- Replace plain text plan input with select dropdown
- Add type="date" to record date filters
- Add production config comment
Server:
- CORS origin restricted in production (env CORS_ORIGIN)
- DB pool size configurable via DB_POOL_SIZE env var
- .env.example updated with WeChat Pay + production fields
- Retry button disconnects then re-scans for device (no page navigation)
- Prevents duplicate scan with checking guard
- Disconnect before scan with 500ms delay for adapter recovery
- Clears reconnect timer on success/error/unload
- Error text updated to guide user to retry button
- vendor_33 mode skips BLE wear detection (protocol doesn't support it)
and passes check immediately if device is connected
- Show error message if device is not connected
- Retry button navigates back if disconnected instead of re-checking
- onUnload disconnects BLE if not in 'done' state (prevents connection leak)
- onUnload clears retry timer to prevent setData on dead page
- Store retry setTimeout ref for proper cleanup
- onRetry clears bind_result listener before disconnect
- Add 1s delay after closeBluetoothAdapter before re-scanning
- Fix binding state text from '点击连接' to '正在配对...'
Heartbeat (1-byte FFE4 notify) now emits 'heartbeat' event only.
Only 33-byte state responses emit 'status' with parsed IO data.
Fixes timer jumping to 0 every second during treatment.
- handleValueChange now calls parseVendorStatus and emits 'status'
with legacy-compatible shape (mode_state, battery, remaining_ms, etc.)
- 1-byte heartbeat mapped to mode_state, 33-byte state parsed for
active IO detection
- Fixes regression where treating, index, wear-check, auto-scan pages
never received status updates in vendor_33 mode
- Also emits 'vendor_raw' with full parsed data for future use
- discoverChars uses properties (write/notify) to distinguish FFE1
command characteristic from FFE1 service UUID
- handleValueChange: vendor_33 mode emits vendor_status (FFE4 heartbeat)
and vendor_data separately, no longer tries parseFrame on raw bytes
- parseVendorStatus handles 1-byte heartbeat and 33-byte state readback
with proper IO1-IO5 field parsing
- RecordView passes user_id as separate filter param, not as keyword
- Admin records endpoint forwards user_id to DAO
- treatmentDao.listAdmin supports exact user_id match filter
- Added visible filter tag with clear button in RecordView
- Always sync treatment end (not just early stop), pass start/end times
- treatment-done uses api.syncTreatment instead of raw http.post
- Move getApp() from module level to onLoad in treatment-done
- Avatar upload only returns URL, no longer updates profile directly
- COS CDN domain configurable via COS_CDN_DOMAIN env var
- Fix operator precedence in request.js token expiry check
- Remove unused result variable from COS putObject
- Treatment start immediately syncs record with full 10min duration
- Early stop re-syncs with actual elapsed time (upsert by session_id)
- treatment-done page uses same session_id from treating page
- Replace chooseAvatar button with chooseMedia (more reliable, works on all versions)
- COS avatar URL uses tx.vsai.net.cn instead of generated bucket domain
- Phone shows '已授权' only (no actual number displayed)
- Keep body as Buffer instead of utf8 string for multipart/form-data
- Set content-length header for multer compatibility
- Phone authorization shows '已授权' instead of actual number
- loadProfile() now returns Promise (was missing return)
- Avatar upload via COS instead of storing WeChat temp path
- Add POST /user/avatar endpoint with multer + COS SDK
- Incomplete registration detection: redirect to register if phone is empty
- New register page with chooseAvatar, nickname input (random default),
and required phone number authorization via getPhoneNumber
- Login simplified: removed deprecated wx.getUserProfile, checks
is_new_user flag to redirect new users to registration
- Server login response now includes is_new_user field