This commit is contained in:
2026-09-13 02:28:46 +08:00
parent df5492a48a
commit 42f9530a59
27 changed files with 979 additions and 149 deletions
@@ -0,0 +1,211 @@
---
name: 全项目日志补全 + debug/info/warn 分级
overview: 新建 utils/logger.ts 实现 DEBUG/INFO/WARN/ERROR/SILENT 分级(按 envVersion 自动判定 + config 手动覆盖 + 双输出 console 与 wx.getRealtimeLogManager),然后把 19 个页面、utils、组件、custom-tab-bar 的静默 catch 全部补上分级日志。
todos:
- id: logger-core
content: 新建 utils/logger.ts 实现四级过滤与双通道输出,并在 config.ts 追加 LogLevel 枚举与 LOG_LEVEL 配置
status: completed
- id: migrate-kernel
content: 迁移 request.ts 与 auth.ts 内核日志,补全请求发起/成功/失败/401刷新/登录态失效各节点
status: completed
dependencies:
- logger-core
- id: migrate-existing
content: 迁移 login.ts 与 closet-share-detail.ts 现有 24 处 console.log 为分级 logger 调用
status: completed
dependencies:
- logger-core
- id: explore-pages
content: 用 [subagent:code-explorer] 梳理 17 个页面的生命周期、请求点与静默 catch 精确位置
status: completed
- id: pages-tabcmp
content: 为 wardrobe/outfit/horoscope/closet-share/profile 五个 tab 页补全日志
status: completed
dependencies:
- logger-core
- explore-pages
- id: pages-rest
content: 为其余 12 个页面补全日志,并清除全部 13 处静默 catch
status: completed
dependencies:
- logger-core
- explore-pages
- id: bar-components
content: 为 custom-tab-bar 与 navigation-bar/clothes-card/empty-state 组件补日志
status: completed
dependencies:
- logger-core
- id: verify
content: 校验 lint 零错误,并确认全项目无遗留裸 console.log
status: completed
dependencies:
- migrate-kernel
- migrate-existing
- pages-tabcmp
- pages-rest
- bar-components
---
## 产品概述
为微信小程序「知途衣橱」全项目接入统一的分级日志系统,使开发、体验、生产三种环境下的可观测性可控可调。
## 核心功能
- **分级日志模块**:提供 debug / info / warn / error 四级(外加 SILENT 静默),统一输出格式 `[级别][模块名] 消息`
- **配置化级别控制**:在配置文件中定义日志级别,支持「按环境自动」与「手动写死覆盖」两种模式
- **环境自动映射**:develop(开发版)= DEBUG,trial(体验版)= INFO,release(正式版)= WARN
- **双通道输出**:console 输出(开发者工具可见)+ 微信实时日志(真机后台「实时日志」可查线上问题)
- **全项目覆盖**:17 个页面、请求内核、token 模块、自定义 tab-bar、3 个公共组件,覆盖生命周期、请求成败、用户操作、跳转失败
- **消除静默失败**:全部 13 处 `.catch(() => {})` 改为 `.catch((e) => logger.warn(...))`
## 技术栈
- 微信小程序原生框架 + TypeScript(`miniprogram-api-typings`)
- 现有工具层:`utils/config.ts`(纯常量)、`utils/request.ts`(请求内核)、`utils/auth.ts`(token)
- 无构建脚本、无本地 TS 编译器,仅依赖 IDE lint 校验
## 实现方案
### 核心策略
新建单一日志模块 `utils/logger.ts` 作为全项目唯一日志出口,所有页面通过 `logger.xxx('模块名', ...)` 调用;级别策略通过 `config.ts` 中的 `LOG_LEVEL` 常量控制。
### 关键技术决策
**决策 1:LogLevel 枚举定义在 config.ts 而非 logger.ts**
- 原因:`logger.ts` 需要 import `config.ts` 读取 `LOG_LEVEL`,若枚举定义在 logger 而 config 又要引用它,会形成 `config → logger → config` 循环依赖。
- 方案:枚举与配置常量都放 `config.ts`(本身零依赖的纯常量文件),依赖方向保持单向 `logger → config`。
- 收益:既满足「在配置里分级」的要求,又彻底避免循环依赖。
**决策 2:级别用 `LogLevel | null` 表示,null 代表自动**
- `LOG_LEVEL = null`(默认)→ 运行时调 `wx.getAccountInfoSync().miniProgram.envVersion` 判定环境
- 需要临时调级时改写为 `LogLevel.DEBUG` 等具体值即可,不改代码逻辑
**决策 3:实时日志用 `wx.getRealtimeLogManager()`**
- 相比 `wx.getLogManager()`,实时日志可在小程序后台「实时日志」秒级查询,更适合线上排障
- 单例惰性初始化,取不到时(低版本基础库/开发者工具)降级为仅 console,不抛错
- 实时日志有单条长度限制,参数序列化时截断保护
**决策 4:实时日志写入范围限制在 WARN 及以上**
- 实时日志有频率与体积配额,DEBUG/INFO 全量上报会击穿配额并产生噪声
- 故:console 输出按级别过滤;实时日志仅 WARN/ERROR 写入
### 性能与可靠性
- **热路径控制**:`request.ts` 的请求日志放到 debug 级,生产环境(WARN)自动零开销——级别判断前置,不做字符串拼接
- **参数序列化成本**:日志消息用 `...args` 原样透传,不主动 `JSON.stringify` 大对象;仅在实时日志通道对超长内容截断
- **无额外依赖**:不引入任何第三方库,包体积影响可忽略
- **降级安全**:`getRealtimeLogManager` 不可用时静默降级,绝不因日志抛错影响业务
## 架构设计
```mermaid
graph TD
A[config.ts<br/>LogLevel 枚举 + LOG_LEVEL 配置] --> B[logger.ts<br/>级别过滤 / 格式化 / 双通道输出]
B --> C[console 输出<br/>DEBUG 及以上]
B --> D[wx.getRealtimeLogManager<br/>仅 WARN 及以上]
E[request.ts 请求内核] --> B
F[auth.ts token 模块] --> B
G[17 个页面] --> B
H[custom-tab-bar] --> B
I[3 个公共组件] --> B
```
依赖方向严格单向:所有业务模块 → logger → config,无反向依赖、无循环依赖。
### 日志级别语义约定
| 级别 | 用途 | 典型场景 |
| --- | --- | --- |
| DEBUG | 开发期流程细节 | 页面 onLoad/onShow 及入参、请求发起、请求成功、数据条数 |
| INFO | 关键流程节点 | 登录成功、token 刷新成功、下单/支付发起、上传成功、分享创建成功 |
| WARN | 可恢复异常 / 静默降级 | 静默 catch、跳转失败、额度查询失败、非关键请求失败 |
| ERROR | 真正的错误 | 网络异常、业务错误码、账号禁用、登录态失效跳转 |
## 目录结构
```
miniprogram/
├── utils/
│ ├── config.ts # [MODIFY] 末尾追加日志配置区块:LogLevel 枚举(DEBUG/INFO/WARN/ERROR/SILENT)、
│ │ # LOG_LEVEL 主开关(null=按环境自动)、LOG_TO_REALTIME 实时日志开关、
│ │ # LOG_MODULE_MAX_LEN 等常量。注意此文件零依赖,不得 import 任何模块。
│ ├── logger.ts # [NEW] 全项目唯一日志出口。实现级别过滤、模块名格式化、双通道输出。
│ │ # 导出 logger.debug/info/warn/error 与 setLevel 运行时调级接口。
│ ├── request.ts # [MODIFY] 迁移现有 10 处 console.log;补全 request/uploadFile 生命周期日志:
│ │ # 请求发起(debug)/成功(debug)/业务错误(warn)/网络异常(error)/
│ │ # 401 刷新(info)/刷新失败(warn)/登录态失效跳转(error)。
│ └── auth.ts # [MODIFY] token 存取关键节点补 debug/warn 日志。
├── pages/ # [MODIFY] 17 个页面全部接入,详见下方任务拆分
├── custom-tab-bar/
│ └── index.ts # [MODIFY] 未读红点轮询、switchTab 跳转、节流命中补日志
└── components/ # [MODIFY] navigation-bar / clothes-card / empty-state 关键回调补日志
```
## 关键代码结构
`utils/config.ts` 追加(零依赖,供 logger 单向引用):
```ts
// ===== 日志配置 =====
// 日志级别:数值越大越严重,低于当前级别的日志不输出
export enum LogLevel {
DEBUG = 0,
INFO = 1,
WARN = 2,
ERROR = 3,
SILENT = 4, // 全部关闭
}
// 全局日志级别。null = 按运行环境自动判定(develop=DEBUG / trial=INFO / release=WARN)
// 需要临时调级时直接改写为具体值,例如 LogLevel.DEBUG
export const LOG_LEVEL: LogLevel | null = null
// 是否写入微信实时日志(真机可在后台「实时日志」查看)
export const LOG_TO_REALTIME = true
```
`utils/logger.ts` 对外接口(实现体不含):
```ts
import { LogLevel, LOG_LEVEL, LOG_TO_REALTIME } from './config'
// 运行时生效级别(LOG_LEVEL 为 null 时按 envVersion 自动推导)
export function getLevel(): LogLevel
export function setLevel(level: LogLevel | null): void
export const logger: {
debug(module: string, ...args: any[]): void
info(module: string, ...args: any[]): void
warn(module: string, ...args: any[]): void
error(module: string, ...args: any[]): void
}
```
## 实施注意事项
- **循环依赖红线**:`config.ts` 必须保持零 import,任何需要 `LogLevel` 的地方统一从 `config.ts` 引入
- **迁移而非新增**:现有 24 处 `console.log` 全部改为 `logger.*`,不得遗留裸 `console.log`(保持项目可 grep 性)
- **静默 catch 全清**:13 处 `.catch(() => {})` 逐个补 `logger.warn`,保留原有降级行为不变,只加观测
- **不动业务逻辑**:本次只加观测点与级别控制,不修改任何请求/跳转/状态逻辑,控制变更半径
- **模块名规范**:统一用页面目录名或文件名,如 `wardrobe`、`request`、`custom-tab-bar`
- **不打印敏感信息**:token、code 等只打长度或存在性,不打印完整值
## 工作量分布
- 基础模块(config + logger):2 个文件
- 内核层(request + auth):2 个文件,约 20+ 个日志点
- 页面层:17 个页面,每页 4-10 个日志点(含 13 处静默 catch)
- 组件与 tab-bar:4 个文件
## SubAgent
- **code-explorer**
- 用途:批量梳理 17 个页面的生命周期方法、请求调用点、跳转 fail 回调与静默 catch 的精确行号,避免逐文件全量读取造成的上下文浪费
- 预期结果:产出各页面待加日志的精确位置清单,作为批量改造的执行依据
@@ -1,3 +1,8 @@
import { logger } from '../../utils/logger'
// 日志模块名
const LOG = 'clothes-card'
Component({ Component({
properties: { properties: {
item: { type: Object, value: {} }, item: { type: Object, value: {} },
@@ -8,6 +13,8 @@ Component({
}, },
methods: { methods: {
onTap() { onTap() {
const item = this.data.item as any
logger.debug(LOG, '点击衣物卡片, uuid=', item && item.clothes_uuid)
this.triggerEvent('select', { item: this.data.item }) this.triggerEvent('select', { item: this.data.item })
}, },
}, },
@@ -1,3 +1,8 @@
import { logger } from '../../utils/logger'
// 日志模块名
const LOG = 'empty-state'
Component({ Component({
properties: { properties: {
icon: { type: String, value: 'icon-hanger' }, icon: { type: String, value: 'icon-hanger' },
@@ -8,6 +13,7 @@ Component({
}, },
methods: { methods: {
onTap() { onTap() {
logger.debug(LOG, '点击空态按钮, text=', this.data.buttonText)
this.triggerEvent('action') this.triggerEvent('action')
}, },
}, },
@@ -1,3 +1,8 @@
import { logger } from '../../utils/logger'
// 日志模块名
const LOG = 'navigation-bar'
Component({ Component({
options: { options: {
multipleSlots: true // 在组件定义时的选项中启用多slot支持 multipleSlots: true // 在组件定义时的选项中启用多slot支持
@@ -64,17 +69,30 @@ Component({
lifetimes: { lifetimes: {
attached() { attached() {
const rect = wx.getMenuButtonBoundingClientRect() const rect = wx.getMenuButtonBoundingClientRect()
const apply = (windowWidth: number, platform: string, safeTop: number) => {
const isAndroid = platform === 'android'
const isDevtools = platform === 'devtools'
this.setData({
ios: !isAndroid,
innerPaddingRight: `padding-right: ${windowWidth - rect.left}px`,
leftWidth: `width: ${windowWidth - rect.left}px`,
safeAreaTop: isDevtools || isAndroid ? `height: calc(var(--height) + ${safeTop}px); padding-top: ${safeTop}px` : ``
})
}
// wx.getSystemInfo 已废弃(基础库 2.20.1+ 拆分接口),改用同步的 window/device 接口
const wxAny = wx as any
if (typeof wxAny.getWindowInfo === 'function') {
const win = wxAny.getWindowInfo()
const dev = typeof wxAny.getDeviceInfo === 'function' ? wxAny.getDeviceInfo() : {}
apply(win.windowWidth, dev.platform || win.platform || '', win.safeArea ? win.safeArea.top : 0)
logger.debug(LOG, '布局计算完成, 窗口宽=', win.windowWidth, '平台=', dev.platform || win.platform)
return
}
// 低版本基础库走废弃接口兜底
logger.warn(LOG, 'getWindowInfo 不可用,降级到已废弃的 wx.getSystemInfo')
wx.getSystemInfo({ wx.getSystemInfo({
success: (res) => { success: (res) => apply(res.windowWidth, res.platform, res.safeArea ? res.safeArea.top : 0)
const isAndroid = res.platform === 'android'
const isDevtools = res.platform === 'devtools'
this.setData({
ios: !isAndroid,
innerPaddingRight: `padding-right: ${res.windowWidth - rect.left}px`,
leftWidth: `width: ${res.windowWidth - rect.left }px`,
safeAreaTop: isDevtools || isAndroid ? `height: calc(var(--height) + ${res.safeArea.top}px); padding-top: ${res.safeArea.top}px` : ``
})
}
}) })
}, },
}, },
@@ -99,8 +117,10 @@ Component({
back() { back() {
const data = this.data const data = this.data
if (data.delta) { if (data.delta) {
logger.debug(LOG, '返回上一页, delta=', data.delta)
wx.navigateBack({ wx.navigateBack({
delta: data.delta delta: data.delta,
fail: (e) => logger.warn(LOG, 'navigateBack 失败, err=', e && e.errMsg)
}) })
} }
this.triggerEvent('back', { delta: data.delta }, {}) this.triggerEvent('back', { delta: data.delta }, {})
+64 -8
View File
@@ -1,6 +1,9 @@
import { request } from '../utils/request' import { request } from '../utils/request'
import * as auth from '../utils/auth' import * as auth from '../utils/auth'
import { STORAGE_KEYS } from '../utils/config' import { STORAGE_KEYS } from '../utils/config'
import { logger } from '../utils/logger'
const LOG = 'tabbar'
// 未读红点轮询最小间隔,避免频繁切 tab 时重复打接口 // 未读红点轮询最小间隔,避免频繁切 tab 时重复打接口
const SYNC_INTERVAL = 30 * 1000 const SYNC_INTERVAL = 30 * 1000
@@ -21,31 +24,76 @@ Component({
lifetimes: { lifetimes: {
attached() { attached() {
// 首帧渲染流程结束后再更新:先用缓存渲染红点,未登录时不再发请求(否则会批量 401 并触发重定向) // 首帧渲染流程(FLOW_INITIAL_CREATION)还没结束就 setData,渲染层会抛
// 「Expected updated data but get first rendering data」/
// 「expect FLOW_INITIAL_CREATION end but get FLOW_CREATE_NODE」。
// 因此首帧内产生的更新先暂存,等首帧流程结束后再一次性提交。
;(this as any).rendered = false
;(this as any).pending = {}
// 每个 tab 页都有独立的 tabBar 实例,直接按当前页面路由推导选中态,
// 页面侧无需再在 onShow 里回写 selected(那一步正是首帧冲突的来源)
const index = this.matchRoute()
if (index >= 0) (this as any).pending.selected = index
const cached = (wx.getStorageSync(STORAGE_KEYS.UNREAD) as number) || 0 const cached = (wx.getStorageSync(STORAGE_KEYS.UNREAD) as number) || 0
if (cached) (this as any).pending.unread = cached
wx.nextTick(() => { wx.nextTick(() => {
this.setData({ unread: cached }) ;(this as any).rendered = true
const pending = (this as any).pending
;(this as any).pending = null
if (pending && Object.keys(pending).length) this.setData(pending)
// 未登录时 syncUnread 内部只清零不发请求,不会批量 401
this.syncUnread() this.syncUnread()
}) })
}, },
}, },
methods: { methods: {
// 当前页面路由在 list 中的下标,非 tab 页返回 -1
matchRoute(): number {
const pages = typeof getCurrentPages === 'function' ? getCurrentPages() : []
const route = pages.length ? (pages[pages.length - 1] as any).route : ''
if (!route) return -1
return this.data.list.findIndex((item) => item.pagePath === `/${route}`)
},
// 统一出口:首帧未结束时先合并暂存,避免打断渲染层首帧流程
applyData(data: Record<string, any>) {
if ((this as any).rendered) {
this.setData(data)
return
}
const pending = (this as any).pending
if (pending) Object.assign(pending, data)
else this.setData(data)
},
// 页面切换选中态(供页面调用,内部保证首帧安全)
setSelected(index: number) {
this.applyData({ selected: index })
},
// 直接更新红点(供页面在已知数量时调用,不再触发二次请求) // 直接更新红点(供页面在已知数量时调用,不再触发二次请求)
setUnread(n: number) { setUnread(n: number) {
const unread = Number(n) || 0 const unread = Number(n) || 0
wx.setStorageSync(STORAGE_KEYS.UNREAD, unread) wx.setStorageSync(STORAGE_KEYS.UNREAD, unread)
this.setData({ unread }) this.applyData({ unread })
}, },
// 拉取未读数:仅登录态才请求,且做节流 // 拉取未读数:仅登录态才请求,且做节流
syncUnread(force?: boolean) { syncUnread(force?: boolean) {
if (!auth.isLoggedIn()) { if (!auth.isLoggedIn()) {
logger.debug(LOG, '未登录,红点置 0')
this.setUnread(0) this.setUnread(0)
return return
} }
const now = Date.now() const now = Date.now()
if (!force && now - lastSyncAt < SYNC_INTERVAL) return if (!force && now - lastSyncAt < SYNC_INTERVAL) {
logger.debug(LOG, '红点轮询命中节流,跳过')
return
}
lastSyncAt = now lastSyncAt = now
request<{ unread_count: number }>({ request<{ unread_count: number }>({
@@ -53,17 +101,25 @@ Component({
showError: false, showError: false,
silentAuth: true, silentAuth: true,
}) })
.then((res) => this.setUnread(res.unread_count || 0)) .then((res) => {
.catch(() => { logger.debug(LOG, '红点同步成功, unread=', res.unread_count)
this.setUnread(res.unread_count || 0)
})
.catch((e: any) => {
// 请求失败时回滚节流时间戳,否则要白等 30s 才能重试 // 请求失败时回滚节流时间戳,否则要白等 30s 才能重试
lastSyncAt = 0 lastSyncAt = 0
logger.warn(LOG, '红点同步失败, code=', e && e.code)
}) })
}, },
switchTab(e: WechatMiniprogram.TouchEvent) { switchTab(e: WechatMiniprogram.TouchEvent) {
const { path, index } = e.currentTarget.dataset as { path: string; index: number } const { path, index } = e.currentTarget.dataset as { path: string; index: number }
wx.switchTab({ url: path }) logger.debug(LOG, 'switchTab, path=', path, 'index=', index)
this.setData({ selected: index }) wx.switchTab({
url: path,
fail: (err: any) => logger.warn(LOG, 'switchTab 失败, path=', path, 'err=', err && err.errMsg),
})
this.setSelected(Number(index) || 0)
}, },
}, },
}) })
+23 -4
View File
@@ -1,5 +1,9 @@
import { request } from '../../utils/request' import { request } from '../../utils/request'
import { AiRecommendResult, ClothesItem } from '../../types/models' import { AiRecommendResult, ClothesItem } from '../../types/models'
import { logger } from '../../utils/logger'
// 日志模块名
const LOG = 'ai-recommend'
Page({ Page({
data: { data: {
@@ -46,6 +50,7 @@ Page({
success: (res) => { success: (res) => {
if (res.confirm) this.doGenerate(sceneType, scene) if (res.confirm) this.doGenerate(sceneType, scene)
}, },
fail: (e) => logger.debug(LOG, '取消生成确认, err=', e && e.errMsg),
}) })
}, },
@@ -58,11 +63,13 @@ Page({
request<AiRecommendResult>({ url: '/ai-recommend', method: 'POST', data: body, showError: false }) request<AiRecommendResult>({ url: '/ai-recommend', method: 'POST', data: body, showError: false })
.then(async (res) => { .then(async (res) => {
logger.info(LOG, 'AI 推荐生成成功, 单品数=', res.outfit_item && res.outfit_item.length)
const clothes = await Promise.all( const clothes = await Promise.all(
res.outfit_item.map(async (it) => { res.outfit_item.map(async (it) => {
try { try {
return await request<ClothesItem>({ url: `/clothes/${it.clothes_uuid}`, showError: false }) return await request<ClothesItem>({ url: `/clothes/${it.clothes_uuid}`, showError: false })
} catch (e) { } catch (e: any) {
logger.warn(LOG, '单品详情拉取失败, uuid=', it.clothes_uuid, 'code=', e && e.code)
return { clothes_uuid: it.clothes_uuid } as ClothesItem return { clothes_uuid: it.clothes_uuid } as ClothesItem
} }
}) })
@@ -72,6 +79,7 @@ Page({
.catch((e: any) => { .catch((e: any) => {
this.setData({ generating: false }) this.setData({ generating: false })
const code = e && e.code const code = e && e.code
logger.warn(LOG, 'AI 推荐生成失败, code=', code)
if (code === 90400) { if (code === 90400) {
this.setData({ state: 'empty' }) this.setData({ state: 'empty' })
} else if (code === 90401) { } else if (code === 90401) {
@@ -93,8 +101,14 @@ Page({
confirmText: '去购买', confirmText: '去购买',
cancelText: '取消', cancelText: '取消',
success: (res) => { success: (res) => {
if (res.confirm) wx.navigateTo({ url: '/pages/mall/mall' }) if (res.confirm) {
wx.navigateTo({
url: '/pages/mall/mall',
fail: (e) => logger.warn(LOG, 'navigateTo 商城失败, err=', e && e.errMsg),
})
}
}, },
fail: (e) => logger.warn(LOG, '额度弹窗失败, err=', e && e.errMsg),
}) })
}, },
@@ -106,14 +120,16 @@ Page({
data: { outfit_uuid: this.data.result.outfit_uuid }, data: { outfit_uuid: this.data.result.outfit_uuid },
}) })
.then(() => { .then(() => {
logger.info(LOG, 'AI 穿搭保存成功')
wx.showToast({ title: '已保存到我的穿搭', icon: 'success' }) wx.showToast({ title: '已保存到我的穿搭', icon: 'success' })
setTimeout(() => { setTimeout(() => {
wx.redirectTo({ wx.redirectTo({
url: `/pages/outfit-detail/outfit-detail?uuid=${this.data.result!.outfit_uuid}&owner=1`, url: `/pages/outfit-detail/outfit-detail?uuid=${this.data.result!.outfit_uuid}&owner=1`,
fail: (e) => logger.warn(LOG, 'redirectTo 穿搭详情失败, err=', e && e.errMsg),
}) })
}, 500) }, 500)
}) })
.catch(() => {}) .catch((e: any) => logger.warn(LOG, 'AI 穿搭保存失败, code=', e && e.code))
}, },
onReroll() { onReroll() {
@@ -121,6 +137,9 @@ Page({
}, },
onGoUpload() { onGoUpload() {
wx.navigateTo({ url: '/pages/upload/upload' }) wx.navigateTo({
url: '/pages/upload/upload',
fail: (e) => logger.warn(LOG, 'navigateTo 上传页失败, err=', e && e.errMsg),
})
}, },
}) })
@@ -2,6 +2,10 @@ import { request } from '../../utils/request'
import { ClothesItem, PagedResult, UserInfo } from '../../types/models' import { ClothesItem, PagedResult, UserInfo } from '../../types/models'
import { STORAGE_KEYS } from '../../utils/config' import { STORAGE_KEYS } from '../../utils/config'
import { normalizePaged, hasMorePaged } from '../../utils/paged' import { normalizePaged, hasMorePaged } from '../../utils/paged'
import { logger } from '../../utils/logger'
// 日志模块名
const LOG = 'closet-share-create'
const CATEGORIES = [ const CATEGORIES = [
{ name: '全部', value: '' }, { name: '全部', value: '' },
@@ -55,10 +59,11 @@ Page({
// 头像为带时间戳的签名 URL,缓存可能过期,实时刷新并回写缓存。 // 头像为带时间戳的签名 URL,缓存可能过期,实时刷新并回写缓存。
request<UserInfo>({ url: '/user/info', showError: false }) request<UserInfo>({ url: '/user/info', showError: false })
.then((user) => { .then((user) => {
logger.debug(LOG, '用户信息加载成功')
wx.setStorageSync(STORAGE_KEYS.USER_INFO, user) wx.setStorageSync(STORAGE_KEYS.USER_INFO, user)
this.setData({ nickname: user.username || '微信用户', avatar: user.avatar || '' }) this.setData({ nickname: user.username || '微信用户', avatar: user.avatar || '' })
}) })
.catch(() => {}) .catch((e: any) => logger.warn(LOG, '用户信息加载失败(用缓存兜底), code=', e && e.code))
}, },
selectPartial() { selectPartial() {
@@ -91,6 +96,7 @@ Page({
.then((res) => { .then((res) => {
if ((this as any).reqSeq !== seq) return if ((this as any).reqSeq !== seq) return
const { list: raw, total } = normalizePaged<ClothesItem>(res) const { list: raw, total } = normalizePaged<ClothesItem>(res)
logger.debug(LOG, '衣物列表加载成功, 分类=', cat || '全部', 'page=', page, '条数=', raw && raw.length)
const list = raw.map((it) => ({ const list = raw.map((it) => ({
...it, ...it,
_selected: this.data.selectedUuids.indexOf(it.clothes_uuid) >= 0, _selected: this.data.selectedUuids.indexOf(it.clothes_uuid) >= 0,
@@ -104,8 +110,9 @@ Page({
hasMore: hasMorePaged(merged, total, PAGE_SIZE), hasMore: hasMorePaged(merged, total, PAGE_SIZE),
}) })
}) })
.catch(() => { .catch((e: any) => {
if ((this as any).reqSeq !== seq) return if ((this as any).reqSeq !== seq) return
logger.warn(LOG, '衣物列表加载失败, page=', page, 'code=', e && e.code)
this.setData({ selectLoading: false, loadingMore: false }) this.setData({ selectLoading: false, loadingMore: false })
}) })
}, },
@@ -168,15 +175,26 @@ Page({
// 提交加锁:连点会重复创建分享、重复扣额度 // 提交加锁:连点会重复创建分享、重复扣额度
this.setData({ submitting: true }) this.setData({ submitting: true })
request<{ share_uuid: string }>({ url: '/closet-share', method: 'POST', data: body }) request<{ share_uuid: string }>({ url: '/closet-share', method: 'POST', data: body })
.then((res) => this.setData({ shareUuid: res.share_uuid, view: 'success', submitting: false })) .then((res) => {
.catch(() => this.setData({ submitting: false })) logger.info(LOG, '分享创建成功, uuid=', res.share_uuid)
this.setData({ shareUuid: res.share_uuid, view: 'success', submitting: false })
})
.catch((e: any) => {
logger.warn(LOG, '分享创建失败, code=', e && e.code)
this.setData({ submitting: false })
})
}, },
onViewShare() { onViewShare() {
wx.redirectTo({ url: `/pages/closet-share-detail/closet-share-detail?uuid=${this.data.shareUuid}` }) wx.redirectTo({
url: `/pages/closet-share-detail/closet-share-detail?uuid=${this.data.shareUuid}`,
fail: (e) => logger.warn(LOG, 'redirectTo 分享详情失败, err=', e && e.errMsg),
})
}, },
onDone() { onDone() {
wx.navigateBack() wx.navigateBack({
fail: (e) => logger.warn(LOG, 'navigateBack 失败, err=', e && e.errMsg),
})
}, },
}) })
@@ -2,6 +2,10 @@ import { request, AUTH_EXPIRED_CODES } from '../../utils/request'
import { ClosetShareDetail } from '../../types/models' import { ClosetShareDetail } from '../../types/models'
import { ShareStatus } from '../../utils/config' import { ShareStatus } from '../../utils/config'
import { formatDateTime } from '../../utils/format' import { formatDateTime } from '../../utils/format'
import { logger } from '../../utils/logger'
// 日志模块名
const LOG = 'closet-share-detail'
Page({ Page({
data: { data: {
@@ -17,9 +21,11 @@ Page({
onLoad(query: Record<string, string>) { onLoad(query: Record<string, string>) {
const uuid = query.uuid || '' const uuid = query.uuid || ''
logger.debug(LOG, 'onLoad, uuid=', uuid)
this.setData({ uuid }) this.setData({ uuid })
if (!uuid) { if (!uuid) {
// uuid 缺失时若不兜底,loading 会永久为 true,页面白屏且无法退出 // uuid 缺失时若不兜底,loading 会永久为 true,页面白屏且无法退出
logger.warn(LOG, 'onLoad 缺少 uuid 参数')
this.setInvalid('链接不完整', '缺少分享标识,无法打开') this.setInvalid('链接不完整', '缺少分享标识,无法打开')
return return
} }
@@ -30,6 +36,7 @@ Page({
request<ClosetShareDetail>({ url: `/closet-share/${this.data.uuid}`, showError: false }) request<ClosetShareDetail>({ url: `/closet-share/${this.data.uuid}`, showError: false })
.then((detail) => { .then((detail) => {
// 后端可能惰性过期:本地再校验一次状态与有效期,避免展示已失效内容 // 后端可能惰性过期:本地再校验一次状态与有效期,避免展示已失效内容
logger.debug(LOG, '详情加载成功, status=', detail && detail.status)
if (this.checkInvalid(detail)) return if (this.checkInvalid(detail)) return
const expireText = const expireText =
@@ -38,6 +45,7 @@ Page({
}) })
.catch((e: any) => { .catch((e: any) => {
const code = e && e.code const code = e && e.code
logger.warn(LOG, '详情加载失败, code=', code)
if (AUTH_EXPIRED_CODES.indexOf(code) >= 0) { if (AUTH_EXPIRED_CODES.indexOf(code) >= 0) {
// 查看分享需登录:给「去登录」出口,登录成功后回到本页 // 查看分享需登录:给「去登录」出口,登录成功后回到本页
this.setData({ this.setData({
@@ -100,16 +108,27 @@ Page({
if (!res.confirm) return if (!res.confirm) return
request({ url: `/closet-share/${this.data.uuid}/cancel`, method: 'POST' }) request({ url: `/closet-share/${this.data.uuid}/cancel`, method: 'POST' })
.then(() => { .then(() => {
logger.info(LOG, '取消分享成功')
wx.showToast({ title: '已取消', icon: 'success' }) wx.showToast({ title: '已取消', icon: 'success' })
;(this as any).backTimer = setTimeout(() => wx.navigateBack(), 500) ;(this as any).backTimer = setTimeout(
() =>
wx.navigateBack({
fail: (e: any) => logger.warn(LOG, 'navigateBack 失败, err=', e && e.errMsg),
}),
500,
)
}) })
.catch(() => {}) .catch((e: any) => logger.warn(LOG, '取消分享失败, code=', e && e.code))
}, },
fail: (e: any) => logger.warn(LOG, '取消分享弹窗失败, err=', e && e.errMsg),
}) })
}, },
onBackHome() { onBackHome() {
wx.switchTab({ url: '/pages/wardrobe/wardrobe' }) wx.switchTab({
url: '/pages/wardrobe/wardrobe',
fail: (e: any) => logger.warn(LOG, 'switchTab 衣橱失败, err=', e && e.errMsg),
})
}, },
onGoLogin() { onGoLogin() {
@@ -120,12 +139,15 @@ Page({
fail: (e) => { fail: (e) => {
// 页面栈满(10 层)时 navigateTo 失败:改用 redirectTo 仍跳登录页, // 页面栈满(10 层)时 navigateTo 失败:改用 redirectTo 仍跳登录页,
// 且保留 redirect 参数(原先跳 profile 会丢失来源,登录后回不到分享页) // 且保留 redirect 参数(原先跳 profile 会丢失来源,登录后回不到分享页)
console.log('[share-detail] navigateTo 登录页失败,改用 redirectTo, err=', e && e.errMsg) logger.warn(LOG, 'navigateTo 登录页失败,改用 redirectTo, err=', e && e.errMsg)
wx.redirectTo({ wx.redirectTo({
url: loginUrl, url: loginUrl,
fail: (e2) => { fail: (e2) => {
console.log('[share-detail] redirectTo 登录页也失败, err=', e2 && e2.errMsg) logger.warn(LOG, 'redirectTo 登录页也失败,退回首页, err=', e2 && e2.errMsg)
wx.switchTab({ url: '/pages/profile/profile' }) wx.switchTab({
url: '/pages/profile/profile',
fail: (e3: any) => logger.warn(LOG, 'switchTab 我的失败, err=', e3 && e3.errMsg),
})
}, },
}) })
}, },
@@ -134,6 +156,7 @@ Page({
onShareAppMessage() { onShareAppMessage() {
const name = (this.data.detail && this.data.detail.owner_name) || '好友' const name = (this.data.detail && this.data.detail.owner_name) || '好友'
logger.debug(LOG, '转发分享, uuid=', this.data.uuid)
return { return {
title: `${name} 向你分享了 TA 的衣柜`, title: `${name} 向你分享了 TA 的衣柜`,
path: `/pages/closet-share-detail/closet-share-detail?uuid=${this.data.uuid}`, path: `/pages/closet-share-detail/closet-share-detail?uuid=${this.data.uuid}`,
+18 -5
View File
@@ -3,6 +3,11 @@ import * as auth from '../../utils/auth'
import { PagedResult, ClosetShareItem } from '../../types/models' import { PagedResult, ClosetShareItem } from '../../types/models'
import { ShareStatus, ShareType } from '../../utils/config' import { ShareStatus, ShareType } from '../../utils/config'
import { normalizePaged, mergePaged, hasMorePaged } from '../../utils/paged' import { normalizePaged, mergePaged, hasMorePaged } from '../../utils/paged'
import { setTabBarSelected } from '../../utils/tabbar'
import { logger } from '../../utils/logger'
// 日志模块名
const LOG = 'closet-share'
Page({ Page({
data: { data: {
@@ -30,8 +35,7 @@ Page({
}, },
syncTabBar() { syncTabBar() {
const tabBar = (this as any).getTabBar && (this as any).getTabBar() setTabBarSelected(3)
if (tabBar) tabBar.setData({ selected: 3 })
}, },
onTabTap(e: WechatMiniprogram.TouchEvent) { onTabTap(e: WechatMiniprogram.TouchEvent) {
@@ -63,6 +67,7 @@ Page({
.then((res) => { .then((res) => {
if ((this as any).reqSeq !== seq) return if ((this as any).reqSeq !== seq) return
const { list: raw, total } = normalizePaged<ClosetShareItem>(res) const { list: raw, total } = normalizePaged<ClosetShareItem>(res)
logger.debug(LOG, '列表加载成功, tab=', this.data.activeTab, 'page=', page, '条数=', raw && raw.length)
const list = raw.map((it) => this.decorate(it)) const list = raw.map((it) => this.decorate(it))
const merged = mergePaged(this.data.list, list, page === 1) const merged = mergePaged(this.data.list, list, page === 1)
this.setData({ this.setData({
@@ -73,8 +78,9 @@ Page({
page, page,
}) })
}) })
.catch(() => { .catch((e: any) => {
if ((this as any).reqSeq !== seq) return if ((this as any).reqSeq !== seq) return
logger.warn(LOG, '列表加载失败, tab=', this.data.activeTab, 'page=', page, 'code=', e && e.code)
this.setData({ loading: false, loadingMore: false }) this.setData({ loading: false, loadingMore: false })
}) })
}, },
@@ -108,11 +114,18 @@ Page({
}, },
onCreate() { onCreate() {
wx.navigateTo({ url: '/pages/closet-share-create/closet-share-create' }) wx.navigateTo({
url: '/pages/closet-share-create/closet-share-create',
fail: (e) => logger.warn(LOG, 'navigateTo 创建分享失败, err=', e && e.errMsg),
})
}, },
onItemTap(e: WechatMiniprogram.TouchEvent) { onItemTap(e: WechatMiniprogram.TouchEvent) {
const uuid = e.currentTarget.dataset.uuid as string const uuid = e.currentTarget.dataset.uuid as string
wx.navigateTo({ url: `/pages/closet-share-detail/closet-share-detail?uuid=${uuid}` }) logger.debug(LOG, '点击分享项, uuid=', uuid)
wx.navigateTo({
url: `/pages/closet-share-detail/closet-share-detail?uuid=${uuid}`,
fail: (err) => logger.warn(LOG, 'navigateTo 分享详情失败, err=', err && err.errMsg),
})
}, },
}) })
@@ -1,6 +1,10 @@
import { request } from '../../utils/request' import { request } from '../../utils/request'
import { ClothesItem } from '../../types/models' import { ClothesItem } from '../../types/models'
import { ProcessStatus } from '../../utils/config' import { ProcessStatus } from '../../utils/config'
import { logger } from '../../utils/logger'
// 日志模块名
const LOG = 'clothes-detail'
const ATTRS = [ const ATTRS = [
{ key: 'category_main', label: '大类', type: 'text' }, { key: 'category_main', label: '大类', type: 'text' },
@@ -44,9 +48,11 @@ Page({
onLoad(query: Record<string, string>) { onLoad(query: Record<string, string>) {
const uuid = query.uuid || '' const uuid = query.uuid || ''
logger.debug(LOG, 'onLoad, uuid=', uuid)
this.setData({ uuid }) this.setData({ uuid })
if (!uuid) { if (!uuid) {
// uuid 缺失时不请求不轮询,否则会停在空白页且无法退出 // uuid 缺失时不请求不轮询,否则会停在空白页且无法退出
logger.warn(LOG, 'onLoad 缺少 uuid 参数')
this.setData({ loading: false, notFound: true }) this.setData({ loading: false, notFound: true })
return return
} }
@@ -55,13 +61,22 @@ Page({
}, },
onBackWardrobe() { onBackWardrobe() {
wx.switchTab({ url: '/pages/wardrobe/wardrobe' }) wx.switchTab({
url: '/pages/wardrobe/wardrobe',
fail: (e) => logger.warn(LOG, 'switchTab 衣橱失败, err=', e && e.errMsg),
})
}, },
fetchDetail(): Promise<void> { fetchDetail(): Promise<void> {
return request<ClothesItem>({ url: `/clothes/${this.data.uuid}` }) return request<ClothesItem>({ url: `/clothes/${this.data.uuid}` })
.then((detail) => this.setData({ detail, loading: false })) .then((detail) => {
.catch(() => this.setData({ loading: false })) logger.debug(LOG, '详情加载成功, 处理状态=', detail && detail.image_process_status)
this.setData({ detail, loading: false })
})
.catch((e: any) => {
logger.warn(LOG, '详情加载失败, code=', e && e.code)
this.setData({ loading: false })
})
}, },
pollStatus() { pollStatus() {
@@ -77,17 +92,20 @@ Page({
if (status === ProcessStatus.SUCCESS) { if (status === ProcessStatus.SUCCESS) {
// 成功:先拉最新详情,再切换为详情视图,避免空属性闪烁 // 成功:先拉最新详情,再切换为详情视图,避免空属性闪烁
this.clearTimer() this.clearTimer()
logger.info(LOG, 'AI 处理完成')
this.fetchDetail().then(() => this.setData({ imageStatus: status, step: 2 })) this.fetchDetail().then(() => this.setData({ imageStatus: status, step: 2 }))
} else if (status === ProcessStatus.FAILED) { } else if (status === ProcessStatus.FAILED) {
this.clearTimer() this.clearTimer()
logger.warn(LOG, 'AI 识别失败, 原因=', res.fail_reason)
this.setData({ imageStatus: status, failReason: res.fail_reason || '识别失败' }) this.setData({ imageStatus: status, failReason: res.fail_reason || '识别失败' })
} else { } else {
this.setData({ imageStatus: status, step: statusToStep(status) }) this.setData({ imageStatus: status, step: statusToStep(status) })
} }
}) })
.catch(() => {}) .catch((e: any) => logger.warn(LOG, '状态轮询失败, code=', e && e.code))
if (count >= 120) { if (count >= 120) {
this.clearTimer() this.clearTimer()
logger.warn(LOG, '状态轮询超时(240s),停止轮询')
} }
}, 2000) }, 2000)
}, },
@@ -178,11 +196,12 @@ Page({
request({ url: `/clothes/${this.data.uuid}`, method: 'PUT', data: body }) request({ url: `/clothes/${this.data.uuid}`, method: 'PUT', data: body })
.then(() => { .then(() => {
logger.info(LOG, '衣物信息保存成功')
wx.showToast({ title: '已保存', icon: 'success' }) wx.showToast({ title: '已保存', icon: 'success' })
this.setData({ editMode: false }) this.setData({ editMode: false })
this.fetchDetail() this.fetchDetail()
}) })
.catch(() => {}) .catch((e: any) => logger.warn(LOG, '衣物信息保存失败, code=', e && e.code))
}, },
onDelete() { onDelete() {
@@ -197,11 +216,19 @@ Page({
if (!res.confirm) return if (!res.confirm) return
request({ url: `/clothes/${this.data.uuid}`, method: 'DELETE' }) request({ url: `/clothes/${this.data.uuid}`, method: 'DELETE' })
.then(() => { .then(() => {
logger.info(LOG, '衣物删除成功')
wx.showToast({ title: '已删除', icon: 'success' }) wx.showToast({ title: '已删除', icon: 'success' })
setTimeout(() => wx.navigateBack(), 500) setTimeout(
() =>
wx.navigateBack({
fail: (e) => logger.warn(LOG, 'navigateBack 失败, err=', e && e.errMsg),
}),
500,
)
}) })
.catch(() => {}) .catch((e: any) => logger.warn(LOG, '衣物删除失败, code=', e && e.code))
}, },
fail: (e) => logger.warn(LOG, '删除弹窗失败, err=', e && e.errMsg),
}) })
}, },
}) })
+16 -4
View File
@@ -1,6 +1,11 @@
import { request, redirectToLogin } from '../../utils/request' import { request, redirectToLogin } from '../../utils/request'
import * as auth from '../../utils/auth' import * as auth from '../../utils/auth'
import { HoroscopeData } from '../../types/models' import { HoroscopeData } from '../../types/models'
import { setTabBarSelected } from '../../utils/tabbar'
import { logger } from '../../utils/logger'
// 日志模块名
const LOG = 'horoscope'
const COLOR_MAP: Record<string, string> = { const COLOR_MAP: Record<string, string> = {
红色: '#FF3B30', 红色: '#FF3B30',
@@ -47,8 +52,7 @@ Page({
}, },
syncTabBar() { syncTabBar() {
const tabBar = (this as any).getTabBar && (this as any).getTabBar() setTabBarSelected(2)
if (tabBar) tabBar.setData({ selected: 2 })
}, },
fetchHoroscope(retry: number) { fetchHoroscope(retry: number) {
@@ -56,6 +60,7 @@ Page({
this.setData({ loading: true, error: false, errorText: '' }) this.setData({ loading: true, error: false, errorText: '' })
request<HoroscopeData>({ url: '/horoscope/today', showError: false }) request<HoroscopeData>({ url: '/horoscope/today', showError: false })
.then((res) => { .then((res) => {
logger.debug(LOG, '运势加载成功, statDate=', res && res.stat_date)
this.setData({ this.setData({
horoscope: res, horoscope: res,
statDate: this.formatStatDate(res.stat_date), statDate: this.formatStatDate(res.stat_date),
@@ -67,6 +72,7 @@ Page({
}) })
.catch((e: any) => { .catch((e: any) => {
const code = e && e.code const code = e && e.code
logger.warn(LOG, '运势加载失败, code=', code, 'retry=', retry)
const details = (e && e.details) || [] const details = (e && e.details) || []
const noBirth = details.some((d: string) => d.indexOf('生日') >= 0) const noBirth = details.some((d: string) => d.indexOf('生日') >= 0)
if (code === 10000 && noBirth) { if (code === 10000 && noBirth) {
@@ -104,11 +110,17 @@ Page({
}, },
onGoProfile() { onGoProfile() {
wx.switchTab({ url: '/pages/profile/profile' }) wx.switchTab({
url: '/pages/profile/profile',
fail: (e) => logger.warn(LOG, 'switchTab 个人资料失败, err=', e && e.errMsg),
})
}, },
onGenerate() { onGenerate() {
wx.navigateTo({ url: '/pages/ai-recommend/ai-recommend' }) wx.navigateTo({
url: '/pages/ai-recommend/ai-recommend',
fail: (e) => logger.warn(LOG, 'navigateTo AI 推荐失败, err=', e && e.errMsg),
})
}, },
onUnload() { onUnload() {
+32 -16
View File
@@ -1,5 +1,9 @@
import { request } from '../../utils/request' import { request } from '../../utils/request'
import * as auth from '../../utils/auth' import * as auth from '../../utils/auth'
import { logger } from '../../utils/logger'
// 日志模块名
const LOG = 'login'
import { STORAGE_KEYS, QuotaType } from '../../utils/config' import { STORAGE_KEYS, QuotaType } from '../../utils/config'
import { LoginResult, UserInfo, QuotaItem } from '../../types/models' import { LoginResult, UserInfo, QuotaItem } from '../../types/models'
import { formatDate } from '../../utils/format' import { formatDate } from '../../utils/format'
@@ -20,7 +24,7 @@ Page({
}, },
onLoad(query: Record<string, string>) { onLoad(query: Record<string, string>) {
console.log('[login] onLoad, query=', JSON.stringify(query), 'isLoggedIn=', auth.isLoggedIn()) logger.debug(LOG, 'onLoad, query=', JSON.stringify(query), 'isLoggedIn=', auth.isLoggedIn())
if (query.redirect) this.setData({ redirect: query.redirect }) if (query.redirect) this.setData({ redirect: query.redirect })
if (auth.isLoggedIn()) { if (auth.isLoggedIn()) {
this.goAfterLogin() this.goAfterLogin()
@@ -30,12 +34,12 @@ Page({
// 登录完成后的落地页:带 redirect 时回跳来源页(分享详情等),否则进衣橱 // 登录完成后的落地页:带 redirect 时回跳来源页(分享详情等),否则进衣橱
goAfterLogin(fallback = false) { goAfterLogin(fallback = false) {
const redirect = this.data.redirect const redirect = this.data.redirect
console.log('[login] goAfterLogin, redirect=', redirect, 'fallback=', fallback) logger.debug(LOG, 'goAfterLogin, redirect=', redirect, 'fallback=', fallback)
if (redirect && !fallback) { if (redirect && !fallback) {
wx.redirectTo({ wx.redirectTo({
url: redirect, url: redirect,
fail: (e) => { fail: (e) => {
console.log('[login] redirect 失败,回退衣橱, err=', e && e.errMsg) logger.warn(LOG, 'redirect 回跳失败,回退衣橱, err=', e && e.errMsg)
this.goAfterLogin(true) this.goAfterLogin(true)
}, },
}) })
@@ -43,16 +47,16 @@ Page({
} }
wx.switchTab({ wx.switchTab({
url: '/pages/wardrobe/wardrobe', url: '/pages/wardrobe/wardrobe',
fail: (e) => console.log('[login] switchTab 衣橱失败, err=', e && e.errMsg), fail: (e) => logger.warn(LOG, 'switchTab 衣橱失败, err=', e && e.errMsg),
}) })
}, },
onLogin() { onLogin() {
if (this.data.loading) { if (this.data.loading) {
console.log('[login] 登录中,忽略重复点击') logger.debug(LOG, '登录中,忽略重复点击')
return return
} }
console.log('[login] 点击登录, redirect=', this.data.redirect) logger.debug(LOG, '点击登录, redirect=', this.data.redirect)
this.setData({ loading: true }) this.setData({ loading: true })
// 超时兜底:wx.login / wx.request 异常挂起时复位按钮,避免永久 loading 点不动 // 超时兜底:wx.login / wx.request 异常挂起时复位按钮,避免永久 loading 点不动
@@ -60,7 +64,7 @@ Page({
;(this as any).loginTimer = setTimeout(() => { ;(this as any).loginTimer = setTimeout(() => {
;(this as any).loginTimer = null ;(this as any).loginTimer = null
if (!this.data.loading) return if (!this.data.loading) return
console.log('[login] 登录超时(30s),复位按钮') logger.warn(LOG, '登录超时(30s),复位按钮')
this.setData({ loading: false }) this.setData({ loading: false })
wx.showToast({ title: '登录超时,请重试', icon: 'none' }) wx.showToast({ title: '登录超时,请重试', icon: 'none' })
}, 30000) }, 30000)
@@ -68,12 +72,12 @@ Page({
wx.login({ wx.login({
success: (res) => { success: (res) => {
if (!res.code) { if (!res.code) {
console.log('[login] wx.login 返回空 code') logger.error(LOG, 'wx.login 返回空 code')
this.finishLogin() this.finishLogin()
wx.showToast({ title: '登录失败,请重试', icon: 'none' }) wx.showToast({ title: '登录失败,请重试', icon: 'none' })
return return
} }
console.log('[login] wx.login 成功, codeLen=', res.code.length) logger.debug(LOG, 'wx.login 成功, codeLen=', res.code.length)
request<LoginResult>({ request<LoginResult>({
url: '/user/wxlogin', url: '/user/wxlogin',
method: 'POST', method: 'POST',
@@ -81,8 +85,9 @@ Page({
auth: false, auth: false,
}) })
.then((data) => { .then((data) => {
console.log( logger.info(
'[login] wxlogin 成功, isNew=', LOG,
'wxlogin 成功, isNew=',
data.user_info && data.user_info.is_new_user, data.user_info && data.user_info.is_new_user,
'expireTime=', 'expireTime=',
data.expire_time, data.expire_time,
@@ -105,7 +110,7 @@ Page({
} }
}) })
.catch((e) => { .catch((e) => {
console.log('[login] wxlogin 失败, code=', e && e.code, 'msg=', e && e.message) logger.error(LOG, 'wxlogin 失败, code=', e && e.code, 'msg=', e && e.message)
}) })
.then(() => { .then(() => {
// 无论成功还是失败都复位:分支走向礼包/生日引导时若不复位,登录按钮会永久点不动 // 无论成功还是失败都复位:分支走向礼包/生日引导时若不复位,登录按钮会永久点不动
@@ -113,7 +118,7 @@ Page({
}) })
}, },
fail: (e) => { fail: (e) => {
console.log('[login] wx.login 失败, errMsg=', e && e.errMsg) logger.error(LOG, 'wx.login 失败, errMsg=', e && e.errMsg)
this.finishLogin() this.finishLogin()
wx.showToast({ title: '登录失败,请重试', icon: 'none' }) wx.showToast({ title: '登录失败,请重试', icon: 'none' })
}, },
@@ -142,7 +147,11 @@ Page({
// 额度只用于礼包展示:失败静默降级,绝不能触发登出/清 token // 额度只用于礼包展示:失败静默降级,绝不能触发登出/清 token
request<{ quotas: QuotaItem[] }>({ url: '/quota/info', showError: false, silentAuth: true }) request<{ quotas: QuotaItem[] }>({ url: '/quota/info', showError: false, silentAuth: true })
.then((res) => this.setData({ giftItems: this.buildGift(userInfo, res.quotas), giftVisible: true })) .then((res) => this.setData({ giftItems: this.buildGift(userInfo, res.quotas), giftVisible: true }))
.catch(() => this.setData({ giftItems: this.buildGift(userInfo, []), giftVisible: true })) .catch((e) => {
// 额度拉取失败不阻塞礼包展示,降级为空额度;绝不能触发登出/清 token
logger.warn(LOG, '礼包额度拉取失败,降级展示, code=', e && e.code)
this.setData({ giftItems: this.buildGift(userInfo, []), giftVisible: true })
})
}, },
buildGift(userInfo: UserInfo, quotas: QuotaItem[]): GiftItem[] { buildGift(userInfo: UserInfo, quotas: QuotaItem[]): GiftItem[] {
@@ -177,9 +186,16 @@ Page({
confirmText: '去完善', confirmText: '去完善',
cancelText: '稍后再说', cancelText: '稍后再说',
success: (res) => { success: (res) => {
if (res.confirm) wx.switchTab({ url: '/pages/profile/profile' }) if (res.confirm) {
else this.goAfterLogin() wx.switchTab({
url: '/pages/profile/profile',
fail: (e) => logger.warn(LOG, 'switchTab 个人资料失败, err=', e && e.errMsg),
})
} else {
this.goAfterLogin()
}
}, },
fail: (e) => logger.warn(LOG, '生日引导弹窗失败, err=', e && e.errMsg),
}) })
}, },
+29 -7
View File
@@ -3,6 +3,10 @@ import { QuotaItem, UserInfo, GoodsItem, QuotaFlowItem, PagedResult, VirtualPayR
import { QUOTA_NAMES, GoodsType, GOODS_TYPE_NAMES } from '../../utils/config' import { QUOTA_NAMES, GoodsType, GOODS_TYPE_NAMES } from '../../utils/config'
import { fenToYuan, formatDate, formatDateTime } from '../../utils/format' import { fenToYuan, formatDate, formatDateTime } from '../../utils/format'
import { normalizePaged } from '../../utils/paged' import { normalizePaged } from '../../utils/paged'
import { logger } from '../../utils/logger'
// 日志模块名
const LOG = 'mall'
const GOODS_ICONS: Record<number, string> = { const GOODS_ICONS: Record<number, string> = {
[GoodsType.MEMBER]: 'icon-crown', [GoodsType.MEMBER]: 'icon-crown',
@@ -54,6 +58,7 @@ Page({
}, },
onLoad(query: Record<string, string>) { onLoad(query: Record<string, string>) {
logger.debug(LOG, 'onLoad, query=', JSON.stringify(query))
if (query.tab === 'flow') this.setData({ view: 'flow' }) if (query.tab === 'flow') this.setData({ view: 'flow' })
}, },
@@ -67,6 +72,7 @@ Page({
fetchUser() { fetchUser() {
request<UserInfo>({ url: '/user/info', showError: false }) request<UserInfo>({ url: '/user/info', showError: false })
.then((user) => { .then((user) => {
logger.debug(LOG, '用户信息加载成功, vipType=', user.vip_type)
const isVip = user.vip_type !== 1 const isVip = user.vip_type !== 1
this.setData({ this.setData({
userInfo: user, userInfo: user,
@@ -74,7 +80,7 @@ Page({
vipExpireText: isVip && user.vip_expire ? `有效期至 ${formatDate(user.vip_expire)}` : '', vipExpireText: isVip && user.vip_expire ? `有效期至 ${formatDate(user.vip_expire)}` : '',
}) })
}) })
.catch(() => {}) .catch((e: any) => logger.warn(LOG, '用户信息加载失败(静默降级), code=', e && e.code))
}, },
fetchQuota() { fetchQuota() {
@@ -87,7 +93,7 @@ Page({
})) }))
this.setData({ quotas }) this.setData({ quotas })
}) })
.catch(() => {}) .catch((e: any) => logger.warn(LOG, '额度加载失败(静默降级), code=', e && e.code))
}, },
fetchGoods() { fetchGoods() {
@@ -103,9 +109,13 @@ Page({
_priceText: fenToYuan(g.discount_price), _priceText: fenToYuan(g.discount_price),
_originText: fenToYuan(g.price), _originText: fenToYuan(g.price),
})) }))
logger.debug(LOG, '商品列表加载成功, 条数=', goods.length)
this.setData({ goods, loading: false }) this.setData({ goods, loading: false })
}) })
.catch(() => this.setData({ loading: false })) .catch((e: any) => {
logger.warn(LOG, '商品列表加载失败, code=', e && e.code)
this.setData({ loading: false })
})
}, },
fetchFlow() { fetchFlow() {
@@ -119,7 +129,7 @@ Page({
})) }))
this.setData({ flowList: list }) this.setData({ flowList: list })
}) })
.catch(() => {}) .catch((e: any) => logger.warn(LOG, '额度流水加载失败(静默降级), code=', e && e.code))
}, },
onGoodsFilter(e: WechatMiniprogram.TouchEvent) { onGoodsFilter(e: WechatMiniprogram.TouchEvent) {
@@ -166,13 +176,18 @@ Page({
}, },
}) })
.then((res) => { .then((res) => {
logger.info(LOG, '下单成功, orderNo=', res.order_no)
this.setData({ orderVisible: false, submitting: false }) this.setData({ orderVisible: false, submitting: false })
this.doPayment(res) this.doPayment(res)
}) })
.catch(() => this.setData({ submitting: false })) .catch((e: any) => {
logger.warn(LOG, '下单失败, code=', e && e.code)
this.setData({ submitting: false })
})
}, },
doPayment(res: VirtualPayResult) { doPayment(res: VirtualPayResult) {
logger.debug(LOG, '拉起虚拟支付, orderNo=', res.order_no)
wx.requestVirtualPayment({ wx.requestVirtualPayment({
signData: res.sign_data, signData: res.sign_data,
paySig: res.pay_sig, paySig: res.pay_sig,
@@ -180,9 +195,12 @@ Page({
mode: 'short_series_goods', mode: 'short_series_goods',
success: () => { success: () => {
// 拉起成功仅代表进入支付页,最终到账以后端发货推送为准,轮询订单确认 // 拉起成功仅代表进入支付页,最终到账以后端发货推送为准,轮询订单确认
logger.info(LOG, '支付页拉起成功, orderNo=', res.order_no)
this.pollOrder(res.order_no) this.pollOrder(res.order_no)
}, },
fail: () => {}, fail: (e: any) => {
logger.error(LOG, '拉起支付失败, orderNo=', res.order_no, 'err=', e && e.errMsg)
},
}) })
}, },
@@ -196,18 +214,22 @@ Page({
const order = normalizePaged<OrderItem>(res).list.find((it) => it.order_no === orderNo) const order = normalizePaged<OrderItem>(res).list.find((it) => it.order_no === orderNo)
if (order && order.order_status === 2) { if (order && order.order_status === 2) {
this.clearPayTimer() this.clearPayTimer()
logger.info(LOG, '支付成功, orderNo=', orderNo)
wx.showToast({ title: '支付成功', icon: 'success' }) wx.showToast({ title: '支付成功', icon: 'success' })
this.fetchQuota() this.fetchQuota()
this.fetchUser() this.fetchUser()
} else if (order && (order.order_status === 3 || order.order_status === 4 || order.order_status === 5)) { } else if (order && (order.order_status === 3 || order.order_status === 4 || order.order_status === 5)) {
this.clearPayTimer() this.clearPayTimer()
logger.warn(LOG, '订单未成功支付, orderNo=', orderNo, 'status=', order.order_status)
const msg = order.order_status === 5 ? '订单已退款' : '订单未支付成功' const msg = order.order_status === 5 ? '订单已退款' : '订单未支付成功'
wx.showToast({ title: msg, icon: 'none' }) wx.showToast({ title: msg, icon: 'none' })
} else if (count >= 10) { } else if (count >= 10) {
logger.warn(LOG, '支付轮询超时, orderNo=', orderNo)
this.finishPayTimeout() this.finishPayTimeout()
} }
}) })
.catch(() => { .catch((e: any) => {
logger.warn(LOG, '支付轮询请求失败, orderNo=', orderNo, 'code=', e && e.code)
if (count >= 10) this.finishPayTimeout() if (count >= 10) this.finishPayTimeout()
}) })
}, 1500) }, 1500)
@@ -2,6 +2,10 @@ import { request } from '../../utils/request'
import { NotificationItem } from '../../types/models' import { NotificationItem } from '../../types/models'
import { formatDateTime } from '../../utils/format' import { formatDateTime } from '../../utils/format'
import { syncTabBarUnread } from '../../utils/tabbar' import { syncTabBarUnread } from '../../utils/tabbar'
import { logger } from '../../utils/logger'
// 日志模块名
const LOG = 'notification-detail'
Page({ Page({
data: { data: {
@@ -12,21 +16,28 @@ Page({
onLoad(query: Record<string, string>) { onLoad(query: Record<string, string>) {
const uuid = query.uuid || '' const uuid = query.uuid || ''
logger.debug(LOG, 'onLoad, uuid=', uuid)
this.setData({ uuid }) this.setData({ uuid })
if (uuid) { if (!uuid) {
this.fetchDetail() logger.warn(LOG, 'onLoad 缺少 uuid 参数')
request({ url: `/notification/${uuid}/read`, method: 'POST', showError: false }) return
.then(() => {
// 标记已读后同步红点:本页不是 tab 页,红点不会自动刷新
syncTabBarUnread(true)
})
.catch(() => {})
} }
this.fetchDetail()
request({ url: `/notification/${uuid}/read`, method: 'POST', showError: false })
.then(() => {
logger.debug(LOG, '标记已读成功')
// 标记已读后同步红点:本页不是 tab 页,红点不会自动刷新
syncTabBarUnread(true)
})
.catch((e: any) => logger.warn(LOG, '标记已读失败, code=', e && e.code))
}, },
fetchDetail() { fetchDetail() {
request<NotificationItem>({ url: `/notification/${this.data.uuid}` }) request<NotificationItem>({ url: `/notification/${this.data.uuid}` })
.then((item) => this.setData({ item, time: formatDateTime(item.created_at) })) .then((item) => {
.catch(() => {}) logger.debug(LOG, '详情加载成功')
this.setData({ item, time: formatDateTime(item.created_at) })
})
.catch((e: any) => logger.warn(LOG, '详情加载失败, code=', e && e.code))
}, },
}) })
+14 -3
View File
@@ -5,6 +5,10 @@ import { STORAGE_KEYS } from '../../utils/config'
import { formatRelativeTime } from '../../utils/format' import { formatRelativeTime } from '../../utils/format'
import { normalizePaged, mergePaged, hasMorePaged } from '../../utils/paged' import { normalizePaged, mergePaged, hasMorePaged } from '../../utils/paged'
import { getTabBarInstance } from '../../utils/tabbar' import { getTabBarInstance } from '../../utils/tabbar'
import { logger } from '../../utils/logger'
// 日志模块名
const LOG = 'notification'
const FILTERS = [ const FILTERS = [
{ name: '全部', value: 0 }, { name: '全部', value: 0 },
@@ -28,6 +32,7 @@ Page({
onShow() { onShow() {
// 未登录直接回登录页(幂等跳转),不再发出必然 401 的请求 // 未登录直接回登录页(幂等跳转),不再发出必然 401 的请求
if (!auth.isLoggedIn()) { if (!auth.isLoggedIn()) {
logger.debug(LOG, '未登录,跳转登录页')
redirectToLogin() redirectToLogin()
return return
} }
@@ -42,6 +47,7 @@ Page({
silentAuth: true, silentAuth: true,
}) })
.then((res) => { .then((res) => {
logger.debug(LOG, '未读数加载成功, unread=', res.unread_count)
const unread = res.unread_count || 0 const unread = res.unread_count || 0
wx.setStorageSync(STORAGE_KEYS.UNREAD, unread) wx.setStorageSync(STORAGE_KEYS.UNREAD, unread)
// 本页不是 tab 页,getTabBar() 恒为 undefined,需从页面栈里取 tab 页的 tabBar // 本页不是 tab 页,getTabBar() 恒为 undefined,需从页面栈里取 tab 页的 tabBar
@@ -49,7 +55,7 @@ Page({
// 直接更新红点,避免 tabBar 内部再发一次请求 // 直接更新红点,避免 tabBar 内部再发一次请求
if (tabBar && tabBar.setUnread) tabBar.setUnread(unread) if (tabBar && tabBar.setUnread) tabBar.setUnread(unread)
}) })
.catch(() => {}) .catch((e: any) => logger.warn(LOG, '未读数加载失败(静默降级), code=', e && e.code))
}, },
refresh() { refresh() {
@@ -76,6 +82,7 @@ Page({
.then((res) => { .then((res) => {
if ((this as any).reqSeq !== seq) return if ((this as any).reqSeq !== seq) return
const { list: raw, total } = normalizePaged<NotificationItem>(res) const { list: raw, total } = normalizePaged<NotificationItem>(res)
logger.debug(LOG, '通知加载成功, 筛选=', filter, 'page=', page, '条数=', raw && raw.length)
const list = raw.map((it) => ({ ...it, _time: formatRelativeTime(it.created_at) })) const list = raw.map((it) => ({ ...it, _time: formatRelativeTime(it.created_at) }))
const merged = mergePaged(this.data.list, list, page === 1) const merged = mergePaged(this.data.list, list, page === 1)
this.setData({ this.setData({
@@ -86,8 +93,9 @@ Page({
page, page,
}) })
}) })
.catch(() => { .catch((e: any) => {
if ((this as any).reqSeq !== seq) return if ((this as any).reqSeq !== seq) return
logger.warn(LOG, '通知加载失败, page=', page, 'code=', e && e.code)
this.setData({ loading: false, loadingMore: false }) this.setData({ loading: false, loadingMore: false })
}) })
}, },
@@ -101,6 +109,9 @@ Page({
onItemTap(e: WechatMiniprogram.TouchEvent) { onItemTap(e: WechatMiniprogram.TouchEvent) {
const uuid = e.currentTarget.dataset.uuid as string const uuid = e.currentTarget.dataset.uuid as string
wx.navigateTo({ url: `/pages/notification-detail/notification-detail?uuid=${uuid}` }) wx.navigateTo({
url: `/pages/notification-detail/notification-detail?uuid=${uuid}`,
fail: (e) => logger.warn(LOG, 'navigateTo 通知详情失败, err=', e && e.errMsg),
})
}, },
}) })
+17 -4
View File
@@ -2,6 +2,10 @@ import { request } from '../../utils/request'
import { PagedResult, OrderItem } from '../../types/models' import { PagedResult, OrderItem } from '../../types/models'
import { fenToYuan, formatDateTime, formatCountdown } from '../../utils/format' import { fenToYuan, formatDateTime, formatCountdown } from '../../utils/format'
import { normalizePaged, mergePaged, hasMorePaged } from '../../utils/paged' import { normalizePaged, mergePaged, hasMorePaged } from '../../utils/paged'
import { logger } from '../../utils/logger'
// 日志模块名
const LOG = 'order'
const STATUS_FILTERS = [ const STATUS_FILTERS = [
{ name: '全部', value: 0 }, { name: '全部', value: 0 },
@@ -94,6 +98,7 @@ Page({
.then((res) => { .then((res) => {
if ((this as any).reqSeq !== seq) return if ((this as any).reqSeq !== seq) return
const { list: raw, total } = normalizePaged<OrderItem>(res) const { list: raw, total } = normalizePaged<OrderItem>(res)
logger.debug(LOG, '订单加载成功, 筛选=', filter, 'page=', page, '条数=', raw && raw.length)
const now = Math.floor(Date.now() / 1000) const now = Math.floor(Date.now() / 1000)
const list = raw.map((it) => ({ const list = raw.map((it) => ({
...it, ...it,
@@ -116,8 +121,9 @@ Page({
page, page,
}) })
}) })
.catch(() => { .catch((e: any) => {
if ((this as any).reqSeq !== seq) return if ((this as any).reqSeq !== seq) return
logger.warn(LOG, '订单加载失败, page=', page, 'code=', e && e.code)
this.setData({ loading: false, loadingMore: false }) this.setData({ loading: false, loadingMore: false })
}) })
}, },
@@ -138,11 +144,13 @@ Page({
if (!res.confirm) return if (!res.confirm) return
request({ url: `/order/${orderNo}/cancel`, method: 'POST' }) request({ url: `/order/${orderNo}/cancel`, method: 'POST' })
.then(() => { .then(() => {
logger.info(LOG, '取消订单成功, orderNo=', orderNo)
wx.showToast({ title: '已取消', icon: 'success' }) wx.showToast({ title: '已取消', icon: 'success' })
this.refresh() this.refresh()
}) })
.catch(() => {}) .catch((e: any) => logger.warn(LOG, '取消订单失败, orderNo=', orderNo, 'code=', e && e.code))
}, },
fail: (e) => logger.warn(LOG, '取消订单弹窗失败, err=', e && e.errMsg),
}) })
}, },
@@ -165,15 +173,20 @@ Page({
} }
request({ url: '/order/refund', method: 'POST', data: { order_no: orderNo, reason } }) request({ url: '/order/refund', method: 'POST', data: { order_no: orderNo, reason } })
.then(() => { .then(() => {
logger.info(LOG, '退款申请提交成功, orderNo=', orderNo)
wx.showToast({ title: '退款申请已提交,等待审核', icon: 'none' }) wx.showToast({ title: '退款申请已提交,等待审核', icon: 'none' })
this.refresh() this.refresh()
}) })
.catch(() => {}) .catch((e: any) => logger.warn(LOG, '退款申请失败, orderNo=', orderNo, 'code=', e && e.code))
}, },
fail: (e) => logger.warn(LOG, '退款弹窗失败, err=', e && e.errMsg),
}) })
}, },
onGoMall() { onGoMall() {
wx.navigateTo({ url: '/pages/mall/mall' }) wx.navigateTo({
url: '/pages/mall/mall',
fail: (e) => logger.warn(LOG, 'navigateTo 商城失败, err=', e && e.errMsg),
})
}, },
}) })
@@ -1,6 +1,10 @@
import { request } from '../../utils/request' import { request } from '../../utils/request'
import { OutfitItem, ClothesItem } from '../../types/models' import { OutfitItem, ClothesItem } from '../../types/models'
import { IsPublic } from '../../utils/config' import { IsPublic } from '../../utils/config'
import { logger } from '../../utils/logger'
// 日志模块名
const LOG = 'outfit-detail'
Page({ Page({
data: { data: {
@@ -17,20 +21,24 @@ Page({
onLoad(query: Record<string, string>) { onLoad(query: Record<string, string>) {
const uuid = query.uuid || '' const uuid = query.uuid || ''
const isOwner = query.owner !== '0' const isOwner = query.owner !== '0'
logger.debug(LOG, 'onLoad, uuid=', uuid, 'isOwner=', isOwner)
this.setData({ uuid, isOwner }) this.setData({ uuid, isOwner })
if (uuid) this.fetchDetail() if (uuid) this.fetchDetail()
else logger.warn(LOG, 'onLoad 缺少 uuid 参数')
}, },
async fetchDetail() { async fetchDetail() {
try { try {
const outfit = await request<OutfitItem>({ url: `/outfit/${this.data.uuid}` }) const outfit = await request<OutfitItem>({ url: `/outfit/${this.data.uuid}` })
logger.debug(LOG, '穿搭详情加载成功, 单品数=', outfit.items && outfit.items.length)
this.setData({ outfit }) this.setData({ outfit })
const clothes = await Promise.all( const clothes = await Promise.all(
outfit.items.map(async (it) => { outfit.items.map(async (it) => {
try { try {
return await request<ClothesItem>({ url: `/clothes/${it.clothes_uuid}`, showError: false }) return await request<ClothesItem>({ url: `/clothes/${it.clothes_uuid}`, showError: false })
} catch (e) { } catch (e: any) {
logger.warn(LOG, '单品详情拉取失败, uuid=', it.clothes_uuid, 'code=', e && e.code)
return { clothes_uuid: it.clothes_uuid } as ClothesItem return { clothes_uuid: it.clothes_uuid } as ClothesItem
} }
}) })
@@ -49,8 +57,9 @@ Page({
showError: false, showError: false,
}) })
.then((collect) => this.setData({ isCollected: collect.is_collected })) .then((collect) => this.setData({ isCollected: collect.is_collected }))
.catch(() => {}) .catch((e: any) => logger.warn(LOG, '收藏状态拉取失败(保持未收藏), code=', e && e.code))
} catch (e) { } catch (e: any) {
logger.warn(LOG, '穿搭详情加载失败, code=', e && e.code)
this.setData({ loading: false }) this.setData({ loading: false })
} }
}, },
@@ -59,19 +68,20 @@ Page({
const action = this.data.isLiked ? 2 : 1 const action = this.data.isLiked ? 2 : 1
request({ url: '/like', method: 'POST', data: { target_uuid: this.data.uuid, action } }) request({ url: '/like', method: 'POST', data: { target_uuid: this.data.uuid, action } })
.then(() => { .then(() => {
logger.debug(LOG, '点赞操作成功, action=', action)
this.setData({ this.setData({
isLiked: action === 1, isLiked: action === 1,
likeCount: this.data.likeCount + (action === 1 ? 1 : -1), likeCount: this.data.likeCount + (action === 1 ? 1 : -1),
}) })
}) })
.catch(() => {}) .catch((e: any) => logger.warn(LOG, '点赞操作失败, action=', action, 'code=', e && e.code))
}, },
onCollect() { onCollect() {
const action = this.data.isCollected ? 2 : 1 const action = this.data.isCollected ? 2 : 1
request({ url: '/collect', method: 'POST', data: { target_uuid: this.data.uuid, action } }) request({ url: '/collect', method: 'POST', data: { target_uuid: this.data.uuid, action } })
.then(() => this.setData({ isCollected: action === 1 })) .then(() => this.setData({ isCollected: action === 1 }))
.catch(() => {}) .catch((e: any) => logger.warn(LOG, '收藏操作失败, action=', action, 'code=', e && e.code))
}, },
onTogglePublic() { onTogglePublic() {
@@ -86,6 +96,7 @@ Page({
success: (res) => { success: (res) => {
if (res.confirm) this.doPublic(IsPublic.PUBLIC) if (res.confirm) this.doPublic(IsPublic.PUBLIC)
}, },
fail: (e) => logger.warn(LOG, '公开弹窗失败, err=', e && e.errMsg),
}) })
} }
}, },
@@ -93,10 +104,11 @@ Page({
doPublic(isPublic: number) { doPublic(isPublic: number) {
request({ url: `/outfit/${this.data.uuid}/public`, method: 'PUT', data: { is_public: isPublic } }) request({ url: `/outfit/${this.data.uuid}/public`, method: 'PUT', data: { is_public: isPublic } })
.then(() => { .then(() => {
logger.info(LOG, '公开状态变更成功, isPublic=', isPublic)
this.setData({ 'outfit.is_public': isPublic }) this.setData({ 'outfit.is_public': isPublic })
wx.showToast({ title: isPublic === IsPublic.PUBLIC ? '已公开' : '已取消公开', icon: 'none' }) wx.showToast({ title: isPublic === IsPublic.PUBLIC ? '已公开' : '已取消公开', icon: 'none' })
}) })
.catch(() => {}) .catch((e: any) => logger.warn(LOG, '公开状态变更失败, code=', e && e.code))
}, },
onMore() { onMore() {
@@ -106,11 +118,15 @@ Page({
if (res.tapIndex === 0) this.onEdit() if (res.tapIndex === 0) this.onEdit()
else this.onDelete() else this.onDelete()
}, },
fail: (e) => logger.debug(LOG, '取消更多操作, err=', e && e.errMsg),
}) })
}, },
onEdit() { onEdit() {
wx.navigateTo({ url: `/pages/outfit-edit/outfit-edit?uuid=${this.data.uuid}` }) wx.navigateTo({
url: `/pages/outfit-edit/outfit-edit?uuid=${this.data.uuid}`,
fail: (e) => logger.warn(LOG, 'navigateTo 编辑穿搭失败, err=', e && e.errMsg),
})
}, },
onDelete() { onDelete() {
@@ -123,17 +139,28 @@ Page({
if (!res.confirm) return if (!res.confirm) return
request({ url: `/outfit/${this.data.uuid}`, method: 'DELETE' }) request({ url: `/outfit/${this.data.uuid}`, method: 'DELETE' })
.then(() => { .then(() => {
logger.info(LOG, '穿搭删除成功')
wx.showToast({ title: '已删除', icon: 'success' }) wx.showToast({ title: '已删除', icon: 'success' })
setTimeout(() => wx.navigateBack(), 500) setTimeout(
() =>
wx.navigateBack({
fail: (e) => logger.warn(LOG, 'navigateBack 失败, err=', e && e.errMsg),
}),
500,
)
}) })
.catch(() => {}) .catch((e: any) => logger.warn(LOG, '穿搭删除失败, code=', e && e.code))
}, },
fail: (e) => logger.warn(LOG, '删除弹窗失败, err=', e && e.errMsg),
}) })
}, },
onItemTap(e: WechatMiniprogram.TouchEvent) { onItemTap(e: WechatMiniprogram.TouchEvent) {
const uuid = e.currentTarget.dataset.uuid as string const uuid = e.currentTarget.dataset.uuid as string
wx.navigateTo({ url: `/pages/clothes-detail/clothes-detail?uuid=${uuid}` }) wx.navigateTo({
url: `/pages/clothes-detail/clothes-detail?uuid=${uuid}`,
fail: (err) => logger.warn(LOG, 'navigateTo 单品详情失败, err=', err && err.errMsg),
})
}, },
onShareAppMessage() { onShareAppMessage() {
+36 -7
View File
@@ -1,6 +1,10 @@
import { request } from '../../utils/request' import { request } from '../../utils/request'
import { OutfitItem, ClothesItem, PagedResult } from '../../types/models' import { OutfitItem, ClothesItem, PagedResult } from '../../types/models'
import { normalizePaged, hasMorePaged } from '../../utils/paged' import { normalizePaged, hasMorePaged } from '../../utils/paged'
import { logger } from '../../utils/logger'
// 日志模块名
const LOG = 'outfit-edit'
const CATEGORIES = [ const CATEGORIES = [
{ name: '全部', value: '' }, { name: '全部', value: '' },
@@ -36,6 +40,7 @@ Page({
onLoad(query: Record<string, string>) { onLoad(query: Record<string, string>) {
const uuid = query.uuid || '' const uuid = query.uuid || ''
logger.debug(LOG, 'onLoad, uuid=', uuid, '编辑态=', !!uuid)
if (uuid) { if (uuid) {
this.setData({ uuid, isEdit: true }) this.setData({ uuid, isEdit: true })
this.fetchOutfit(uuid) this.fetchOutfit(uuid)
@@ -50,7 +55,8 @@ Page({
uuids.map(async (id) => { uuids.map(async (id) => {
try { try {
return await request<ClothesItem>({ url: `/clothes/${id}`, showError: false }) return await request<ClothesItem>({ url: `/clothes/${id}`, showError: false })
} catch (e) { } catch (e: any) {
logger.warn(LOG, '单品详情拉取失败, uuid=', id, 'code=', e && e.code)
return { clothes_uuid: id } as ClothesItem return { clothes_uuid: id } as ClothesItem
} }
}) })
@@ -67,7 +73,9 @@ Page({
selectedUuids: uuids, selectedUuids: uuids,
selectedItems: clothes, selectedItems: clothes,
}) })
} catch (e) {} } catch (e: any) {
logger.warn(LOG, '穿搭详情加载失败, code=', e && e.code)
}
}, },
// 已选衣物快照:跨分类切换时列表只保留当前分类,快照保留全部已选项 // 已选衣物快照:跨分类切换时列表只保留当前分类,快照保留全部已选项
@@ -109,6 +117,7 @@ Page({
.then((res) => { .then((res) => {
if ((this as any).reqSeq !== seq) return if ((this as any).reqSeq !== seq) return
const { list: raw, total } = normalizePaged<ClothesItem>(res) const { list: raw, total } = normalizePaged<ClothesItem>(res)
logger.debug(LOG, '衣物列表加载成功, 分类=', cat || '全部', 'page=', page, '条数=', raw && raw.length)
const list = raw.map((it) => ({ const list = raw.map((it) => ({
...it, ...it,
_selected: this.data.selectedUuids.indexOf(it.clothes_uuid) >= 0, _selected: this.data.selectedUuids.indexOf(it.clothes_uuid) >= 0,
@@ -122,8 +131,9 @@ Page({
hasMore: hasMorePaged(merged, total, PAGE_SIZE), hasMore: hasMorePaged(merged, total, PAGE_SIZE),
}) })
}) })
.catch(() => { .catch((e: any) => {
if ((this as any).reqSeq !== seq) return if ((this as any).reqSeq !== seq) return
logger.warn(LOG, '衣物列表加载失败, page=', page, 'code=', e && e.code)
this.setData({ selectLoading: false, loadingMore: false }) this.setData({ selectLoading: false, loadingMore: false })
}) })
}, },
@@ -151,6 +161,7 @@ Page({
return return
} }
if (uuids.length >= 10) { if (uuids.length >= 10) {
logger.debug(LOG, '已选 10 件,达到上限')
wx.showToast({ title: '最多选择 10 件衣物', icon: 'none' }) wx.showToast({ title: '最多选择 10 件衣物', icon: 'none' })
return return
} }
@@ -202,11 +213,21 @@ Page({
: request({ url: '/outfit', method: 'POST', data: body }) : request({ url: '/outfit', method: 'POST', data: body })
req req
.then(() => { .then(() => {
logger.info(LOG, '穿搭保存成功, 编辑态=', this.data.isEdit, '件数=', body.items.length)
this.setData({ saving: false }) this.setData({ saving: false })
wx.showToast({ title: '已保存', icon: 'success' }) wx.showToast({ title: '已保存', icon: 'success' })
setTimeout(() => wx.navigateBack(), 500) setTimeout(
() =>
wx.navigateBack({
fail: (e) => logger.warn(LOG, 'navigateBack 失败, err=', e && e.errMsg),
}),
500,
)
})
.catch((e: any) => {
logger.warn(LOG, '穿搭保存失败, code=', e && e.code)
this.setData({ saving: false })
}) })
.catch(() => this.setData({ saving: false }))
}, },
onDelete() { onDelete() {
@@ -219,11 +240,19 @@ Page({
if (!res.confirm) return if (!res.confirm) return
request({ url: `/outfit/${this.data.uuid}`, method: 'DELETE' }) request({ url: `/outfit/${this.data.uuid}`, method: 'DELETE' })
.then(() => { .then(() => {
logger.info(LOG, '穿搭删除成功')
wx.showToast({ title: '已删除', icon: 'success' }) wx.showToast({ title: '已删除', icon: 'success' })
setTimeout(() => wx.navigateBack(), 500) setTimeout(
() =>
wx.navigateBack({
fail: (e) => logger.warn(LOG, 'navigateBack 失败, err=', e && e.errMsg),
}),
500,
)
}) })
.catch(() => {}) .catch((e: any) => logger.warn(LOG, '穿搭删除失败, code=', e && e.code))
}, },
fail: (e) => logger.warn(LOG, '删除弹窗失败, err=', e && e.errMsg),
}) })
}, },
}) })
+26 -7
View File
@@ -3,6 +3,11 @@ import * as auth from '../../utils/auth'
import { PagedResult, OutfitItem } from '../../types/models' import { PagedResult, OutfitItem } from '../../types/models'
import { OutfitSource, IsPublic, STORAGE_KEYS } from '../../utils/config' import { OutfitSource, IsPublic, STORAGE_KEYS } from '../../utils/config'
import { normalizePaged, mergePaged, hasMorePaged } from '../../utils/paged' import { normalizePaged, mergePaged, hasMorePaged } from '../../utils/paged'
import { setTabBarSelected } from '../../utils/tabbar'
import { logger } from '../../utils/logger'
// 日志模块名
const LOG = 'outfit'
Page({ Page({
data: { data: {
@@ -25,6 +30,7 @@ Page({
} }
const openCollect = wx.getStorageSync(STORAGE_KEYS.OPEN_COLLECT) const openCollect = wx.getStorageSync(STORAGE_KEYS.OPEN_COLLECT)
if (openCollect) { if (openCollect) {
logger.debug(LOG, '命中打开收藏夹标记,切换到收藏 tab')
wx.removeStorageSync(STORAGE_KEYS.OPEN_COLLECT) wx.removeStorageSync(STORAGE_KEYS.OPEN_COLLECT)
if (this.data.activeTab !== 1) { if (this.data.activeTab !== 1) {
this.setData({ activeTab: 1, list: [], loading: true }) this.setData({ activeTab: 1, list: [], loading: true })
@@ -34,8 +40,7 @@ Page({
}, },
syncTabBar() { syncTabBar() {
const tabBar = (this as any).getTabBar && (this as any).getTabBar() setTabBarSelected(1)
if (tabBar) tabBar.setData({ selected: 1 })
}, },
onTabChange(e: WechatMiniprogram.TouchEvent) { onTabChange(e: WechatMiniprogram.TouchEvent) {
@@ -71,6 +76,7 @@ Page({
return request<PagedResult<OutfitItem>>({ url, data: { page, page_size: this.data.pageSize } }) return request<PagedResult<OutfitItem>>({ url, data: { page, page_size: this.data.pageSize } })
.then(async (res) => { .then(async (res) => {
const { list: raw, total } = normalizePaged<OutfitItem>(res) const { list: raw, total } = normalizePaged<OutfitItem>(res)
logger.debug(LOG, '列表加载成功, url=', url, 'page=', page, '条数=', raw && raw.length)
const items = await this.decorateList(raw) const items = await this.decorateList(raw)
// decorateList 是异步的,期间可能已切换 tab:序号不匹配则丢弃 // decorateList 是异步的,期间可能已切换 tab:序号不匹配则丢弃
if ((this as any).reqSeq !== seq) return if ((this as any).reqSeq !== seq) return
@@ -83,8 +89,9 @@ Page({
page, page,
}) })
}) })
.catch(() => { .catch((e: any) => {
if ((this as any).reqSeq !== seq) return if ((this as any).reqSeq !== seq) return
logger.warn(LOG, '列表加载失败, url=', url, 'page=', page, 'code=', e && e.code)
this.setData({ loading: false, loadingMore: false }) this.setData({ loading: false, loadingMore: false })
}) })
}, },
@@ -99,7 +106,9 @@ Page({
showError: false, showError: false,
}) })
likeCount = like.like_count likeCount = like.like_count
} catch (e) {} } catch (e: any) {
logger.warn(LOG, '点赞数拉取失败, uuid=', it.outfit_uuid, 'code=', e && e.code)
}
return { ...it, _likeCount: likeCount, _status: this.getStatusText(it) } return { ...it, _likeCount: likeCount, _status: this.getStatusText(it) }
}) })
) )
@@ -112,16 +121,26 @@ Page({
}, },
onCreate() { onCreate() {
wx.navigateTo({ url: '/pages/outfit-edit/outfit-edit' }) wx.navigateTo({
url: '/pages/outfit-edit/outfit-edit',
fail: (e) => logger.warn(LOG, 'navigateTo 新建穿搭失败, err=', e && e.errMsg),
})
}, },
onAiRecommend() { onAiRecommend() {
wx.navigateTo({ url: '/pages/ai-recommend/ai-recommend' }) wx.navigateTo({
url: '/pages/ai-recommend/ai-recommend',
fail: (e) => logger.warn(LOG, 'navigateTo AI 推荐失败, err=', e && e.errMsg),
})
}, },
onOutfitTap(e: WechatMiniprogram.TouchEvent) { onOutfitTap(e: WechatMiniprogram.TouchEvent) {
const uuid = e.currentTarget.dataset.uuid as string const uuid = e.currentTarget.dataset.uuid as string
const owner = this.data.activeTab === 0 ? '1' : '0' const owner = this.data.activeTab === 0 ? '1' : '0'
wx.navigateTo({ url: `/pages/outfit-detail/outfit-detail?uuid=${uuid}&owner=${owner}` }) logger.debug(LOG, '点击穿搭, uuid=', uuid, 'owner=', owner)
wx.navigateTo({
url: `/pages/outfit-detail/outfit-detail?uuid=${uuid}&owner=${owner}`,
fail: (err) => logger.warn(LOG, 'navigateTo 穿搭详情失败, err=', err && err.errMsg),
})
}, },
}) })
+45 -13
View File
@@ -3,6 +3,11 @@ import * as auth from '../../utils/auth'
import { UserInfo, QuotaItem } from '../../types/models' import { UserInfo, QuotaItem } from '../../types/models'
import { QuotaType, STORAGE_KEYS } from '../../utils/config' import { QuotaType, STORAGE_KEYS } from '../../utils/config'
import { formatDate } from '../../utils/format' import { formatDate } from '../../utils/format'
import { setTabBarSelected } from '../../utils/tabbar'
import { logger } from '../../utils/logger'
// 日志模块名
const LOG = 'profile'
function getZodiac(birthDay: number): string { function getZodiac(birthDay: number): string {
if (!birthDay) return '' if (!birthDay) return ''
@@ -64,18 +69,18 @@ Page({
}, },
syncTabBar() { syncTabBar() {
const tabBar = (this as any).getTabBar && (this as any).getTabBar()
// 红点由 tabBar 自身在登录态下同步,这里只更新选中态 // 红点由 tabBar 自身在登录态下同步,这里只更新选中态
if (tabBar) tabBar.setData({ selected: 4 }) setTabBarSelected(4)
}, },
fetchUser() { fetchUser() {
request<UserInfo>({ url: '/user/info', showError: false }) request<UserInfo>({ url: '/user/info', showError: false })
.then((user) => { .then((user) => {
logger.debug(LOG, '用户信息加载成功')
wx.setStorageSync(STORAGE_KEYS.USER_INFO, user) wx.setStorageSync(STORAGE_KEYS.USER_INFO, user)
this.setData({ userInfo: user, zodiac: getZodiac(user.birth_day) }) this.setData({ userInfo: user, zodiac: getZodiac(user.birth_day) })
}) })
.catch(() => {}) .catch((e: any) => logger.warn(LOG, '用户信息加载失败, code=', e && e.code))
}, },
fetchQuota() { fetchQuota() {
@@ -83,8 +88,9 @@ Page({
.then((res) => { .then((res) => {
const upload = res.quotas.find((q) => q.quota_type === QuotaType.UPLOAD) const upload = res.quotas.find((q) => q.quota_type === QuotaType.UPLOAD)
if (upload) this.setData({ uploadRemain: upload.remain }) if (upload) this.setData({ uploadRemain: upload.remain })
logger.debug(LOG, '额度加载成功, 上传剩余=', upload ? upload.remain : 0)
}) })
.catch(() => {}) .catch((e: any) => logger.warn(LOG, '额度加载失败(静默降级), code=', e && e.code))
}, },
onEdit() { onEdit() {
@@ -120,7 +126,7 @@ Page({
wx.showActionSheet({ wx.showActionSheet({
itemList: ['男', '女'], itemList: ['男', '女'],
success: (res) => this.setData({ 'editForm.sex': res.tapIndex + 1 }), success: (res) => this.setData({ 'editForm.sex': res.tapIndex + 1 }),
fail: () => {}, // 取消选择属正常操作,不必抛错日志 fail: (e) => logger.debug(LOG, '取消性别选择, err=', e && e.errMsg),
}) })
}, },
@@ -164,39 +170,65 @@ Page({
} }
this.setData({ saving: true }) this.setData({ saving: true })
logger.debug(LOG, '提交资料变更, 字段=', Object.keys(body).join(','))
request({ url: '/user/profile', method: 'PUT', data: body }) request({ url: '/user/profile', method: 'PUT', data: body })
.then(() => { .then(() => {
logger.info(LOG, '资料保存成功')
wx.showToast({ title: '已保存', icon: 'success' }) wx.showToast({ title: '已保存', icon: 'success' })
finish() finish()
}) })
.catch(() => this.setData({ avatarUploading: false, saving: false })) .catch((e: any) => {
logger.warn(LOG, '资料保存失败, code=', e && e.code)
this.setData({ avatarUploading: false, saving: false })
})
} }
if (this.data.avatarChanged && avatar) { if (this.data.avatarChanged && avatar) {
this.setData({ avatarUploading: true }) this.setData({ avatarUploading: true })
uploadFile<{ avatar: string }>(avatar, 'file', '/user/avatar/upload') uploadFile<{ avatar: string }>(avatar, 'file', '/user/avatar/upload')
.then(() => saveProfile()) .then(() => {
.catch(() => this.setData({ avatarUploading: false, saving: false })) logger.info(LOG, '头像上传成功')
saveProfile()
})
.catch((e: any) => {
logger.warn(LOG, '头像上传失败, code=', e && e.code)
this.setData({ avatarUploading: false, saving: false })
})
} else { } else {
saveProfile() saveProfile()
} }
}, },
onTapMall() { onTapMall() {
wx.navigateTo({ url: '/pages/mall/mall' }) wx.navigateTo({
url: '/pages/mall/mall',
fail: (e) => logger.warn(LOG, 'navigateTo 商城失败, err=', e && e.errMsg),
})
}, },
onTapOrder() { onTapOrder() {
wx.navigateTo({ url: '/pages/order/order' }) wx.navigateTo({
url: '/pages/order/order',
fail: (e) => logger.warn(LOG, 'navigateTo 订单失败, err=', e && e.errMsg),
})
}, },
onTapNotification() { onTapNotification() {
wx.navigateTo({ url: '/pages/notification/notification' }) wx.navigateTo({
url: '/pages/notification/notification',
fail: (e) => logger.warn(LOG, 'navigateTo 通知失败, err=', e && e.errMsg),
})
}, },
onTapCollect() { onTapCollect() {
wx.setStorageSync(STORAGE_KEYS.OPEN_COLLECT, 1) wx.setStorageSync(STORAGE_KEYS.OPEN_COLLECT, 1)
wx.switchTab({ url: '/pages/outfit/outfit' }) wx.switchTab({
url: '/pages/outfit/outfit',
fail: (e) => logger.warn(LOG, 'switchTab 穿搭失败, err=', e && e.errMsg),
})
}, },
onTapFlow() { onTapFlow() {
wx.navigateTo({ url: '/pages/mall/mall?tab=flow' }) wx.navigateTo({
url: '/pages/mall/mall?tab=flow',
fail: (e) => logger.warn(LOG, 'navigateTo 流水失败, err=', e && e.errMsg),
})
}, },
onTapService() { onTapService() {
wx.showToast({ title: '客服与反馈敬请期待', icon: 'none' }) wx.showToast({ title: '客服与反馈敬请期待', icon: 'none' })
+32 -5
View File
@@ -1,5 +1,9 @@
import { request, uploadFile, NOT_LOGIN_CODE } from '../../utils/request' import { request, uploadFile, NOT_LOGIN_CODE } from '../../utils/request'
import { ProcessStatus } from '../../utils/config' import { ProcessStatus } from '../../utils/config'
import { logger } from '../../utils/logger'
// 日志模块名
const LOG = 'upload'
Page({ Page({
data: { data: {
@@ -17,7 +21,7 @@ Page({
const sourceType: Array<'camera' | 'album'> = res.tapIndex === 0 ? ['camera'] : ['album'] const sourceType: Array<'camera' | 'album'> = res.tapIndex === 0 ? ['camera'] : ['album']
this.chooseImage(sourceType) this.chooseImage(sourceType)
}, },
fail: () => {}, // 取消选择属正常操作,不必抛错日志 fail: (e) => logger.debug(LOG, '取消选择图片, err=', e && e.errMsg),
}) })
}, },
@@ -28,8 +32,10 @@ Page({
sourceType, sourceType,
success: (res) => { success: (res) => {
const file = res.tempFiles[0] const file = res.tempFiles[0]
logger.debug(LOG, '已选择图片, 来源=', sourceType.join(','))
this.startUpload(file.tempFilePath) this.startUpload(file.tempFilePath)
}, },
fail: (e) => logger.warn(LOG, '选择图片失败, err=', e && e.errMsg),
}) })
}, },
@@ -38,11 +44,13 @@ Page({
uploadFile<{ clothes_uuid: string; image_process_status: number }>(filePath) uploadFile<{ clothes_uuid: string; image_process_status: number }>(filePath)
.then((res) => { .then((res) => {
logger.info(LOG, '衣物上传成功, uuid=', res.clothes_uuid)
this.setData({ clothesUuid: res.clothes_uuid }) this.setData({ clothesUuid: res.clothes_uuid })
this.askNextAction() this.askNextAction()
}) })
.catch((e: any) => { .catch((e: any) => {
this.setData({ state: 'idle' }) this.setData({ state: 'idle' })
logger.warn(LOG, '衣物上传失败, code=', e && e.code)
if (e && e.code === 80000) { if (e && e.code === 80000) {
this.showQuotaModal() this.showQuotaModal()
return return
@@ -66,14 +74,22 @@ Page({
success: (res) => { success: (res) => {
this.clearTimer() this.clearTimer()
if (res.confirm) { if (res.confirm) {
logger.debug(LOG, '选择继续上传,重置状态')
this.setData({ state: 'idle', imagePath: '', clothesUuid: '', step: 0, failReason: '' }) this.setData({ state: 'idle', imagePath: '', clothesUuid: '', step: 0, failReason: '' })
} else { } else {
wx.navigateBack({ wx.navigateBack({
delta: 1, delta: 1,
fail: () => wx.switchTab({ url: '/pages/wardrobe/wardrobe' }), fail: (e) => {
logger.warn(LOG, 'navigateBack 失败,改跳衣橱, err=', e && e.errMsg)
wx.switchTab({
url: '/pages/wardrobe/wardrobe',
fail: (e2) => logger.warn(LOG, 'switchTab 衣橱失败, err=', e2 && e2.errMsg),
})
},
}) })
} }
}, },
fail: (e) => logger.warn(LOG, '上传完成弹窗失败, err=', e && e.errMsg),
}) })
}, },
@@ -96,13 +112,15 @@ Page({
this.clearTimer() this.clearTimer()
} else if (status === ProcessStatus.FAILED) { } else if (status === ProcessStatus.FAILED) {
this.clearTimer() this.clearTimer()
logger.warn(LOG, 'AI 识别失败, 原因=', res.fail_reason)
this.setData({ state: 'failed', failReason: res.fail_reason || '识别失败' }) this.setData({ state: 'failed', failReason: res.fail_reason || '识别失败' })
} }
}) })
.catch(() => {}) .catch((e: any) => logger.warn(LOG, '状态轮询失败, code=', e && e.code))
if (count >= 60) { if (count >= 60) {
this.clearTimer() this.clearTimer()
logger.warn(LOG, 'AI 处理超时(90s)')
this.setData({ state: 'failed', failReason: '处理超时,请重试' }) this.setData({ state: 'failed', failReason: '处理超时,请重试' })
} }
}, 1500) }, 1500)
@@ -127,8 +145,14 @@ Page({
confirmText: '去购买', confirmText: '去购买',
cancelText: '取消', cancelText: '取消',
success: (res) => { success: (res) => {
if (res.confirm) wx.navigateTo({ url: '/pages/mall/mall' }) if (res.confirm) {
wx.navigateTo({
url: '/pages/mall/mall',
fail: (e) => logger.warn(LOG, 'navigateTo 商城失败, err=', e && e.errMsg),
})
}
}, },
fail: (e) => logger.warn(LOG, '额度弹窗失败, err=', e && e.errMsg),
}) })
}, },
@@ -138,6 +162,9 @@ Page({
onManualEdit() { onManualEdit() {
if (!this.data.clothesUuid) return if (!this.data.clothesUuid) return
wx.redirectTo({ url: `/pages/clothes-detail/clothes-detail?uuid=${this.data.clothesUuid}` }) wx.redirectTo({
url: `/pages/clothes-detail/clothes-detail?uuid=${this.data.clothesUuid}`,
fail: (e) => logger.warn(LOG, 'redirectTo 衣物详情失败, err=', e && e.errMsg),
})
}, },
}) })
+20 -6
View File
@@ -3,6 +3,11 @@ import * as auth from '../../utils/auth'
import { PagedResult, ClothesItem, QuotaItem } from '../../types/models' import { PagedResult, ClothesItem, QuotaItem } from '../../types/models'
import { QuotaType } from '../../utils/config' import { QuotaType } from '../../utils/config'
import { normalizePaged, mergePaged, hasMorePaged } from '../../utils/paged' import { normalizePaged, mergePaged, hasMorePaged } from '../../utils/paged'
import { setTabBarSelected } from '../../utils/tabbar'
import { logger } from '../../utils/logger'
// 日志模块名
const LOG = 'wardrobe'
const CATEGORIES = [ const CATEGORIES = [
{ name: '全部', value: '' }, { name: '全部', value: '' },
@@ -40,8 +45,7 @@ Page({
}, },
syncTabBar() { syncTabBar() {
const tabBar = (this as any).getTabBar && (this as any).getTabBar() setTabBarSelected(0)
if (tabBar) tabBar.setData({ selected: 0 })
}, },
refresh() { refresh() {
@@ -76,6 +80,7 @@ Page({
.then((res) => { .then((res) => {
if ((this as any).reqSeq !== seq) return if ((this as any).reqSeq !== seq) return
const { list: raw, total } = normalizePaged<ClothesItem>(res) const { list: raw, total } = normalizePaged<ClothesItem>(res)
logger.debug(LOG, '列表加载成功, 分类=', cat || '全部', 'page=', page, '条数=', raw && raw.length)
const list = raw.map((it) => ({ const list = raw.map((it) => ({
...it, ...it,
_statusText: !it.category_main && !it.processed_image ? 'AI 处理中' : '', _statusText: !it.category_main && !it.processed_image ? 'AI 处理中' : '',
@@ -91,8 +96,9 @@ Page({
page, page,
}) })
}) })
.catch(() => { .catch((e: any) => {
if ((this as any).reqSeq !== seq) return if ((this as any).reqSeq !== seq) return
logger.warn(LOG, '列表加载失败, page=', page, 'code=', e && e.code)
this.setData({ loading: false, loadingMore: false }) this.setData({ loading: false, loadingMore: false })
}) })
}, },
@@ -102,8 +108,9 @@ Page({
.then((res) => { .then((res) => {
const upload = res.quotas.find((q) => q.quota_type === QuotaType.UPLOAD) const upload = res.quotas.find((q) => q.quota_type === QuotaType.UPLOAD)
if (upload) this.setData({ uploadRemain: upload.remain }) if (upload) this.setData({ uploadRemain: upload.remain })
logger.debug(LOG, '额度加载成功, 上传剩余=', upload ? upload.remain : 0)
}) })
.catch(() => {}) .catch((e: any) => logger.warn(LOG, '额度加载失败(静默降级), code=', e && e.code))
}, },
onCategoryTap(e: WechatMiniprogram.TouchEvent) { onCategoryTap(e: WechatMiniprogram.TouchEvent) {
@@ -115,10 +122,17 @@ Page({
onClothesTap(e: WechatMiniprogram.CustomEvent) { onClothesTap(e: WechatMiniprogram.CustomEvent) {
const item = e.detail.item as ClothesItem const item = e.detail.item as ClothesItem
wx.navigateTo({ url: `/pages/clothes-detail/clothes-detail?uuid=${item.clothes_uuid}` }) logger.debug(LOG, '点击衣物, uuid=', item && item.clothes_uuid)
wx.navigateTo({
url: `/pages/clothes-detail/clothes-detail?uuid=${item.clothes_uuid}`,
fail: (err) => logger.warn(LOG, 'navigateTo 衣物详情失败, err=', err && err.errMsg),
})
}, },
onUpload() { onUpload() {
wx.navigateTo({ url: '/pages/upload/upload' }) wx.navigateTo({
url: '/pages/upload/upload',
fail: (e) => logger.warn(LOG, 'navigateTo 上传页失败, err=', e && e.errMsg),
})
}, },
}) })
+7
View File
@@ -1,4 +1,8 @@
import { STORAGE_KEYS } from './config' import { STORAGE_KEYS } from './config'
import { logger } from './logger'
// 日志模块名
const LOG = 'auth'
export interface TokenData { export interface TokenData {
accessToken: string accessToken: string
@@ -22,12 +26,15 @@ export function setTokens(data: TokenData): void {
wx.setStorageSync(STORAGE_KEYS.ACCESS_TOKEN, data.accessToken) wx.setStorageSync(STORAGE_KEYS.ACCESS_TOKEN, data.accessToken)
wx.setStorageSync(STORAGE_KEYS.REFRESH_TOKEN, data.refreshToken) wx.setStorageSync(STORAGE_KEYS.REFRESH_TOKEN, data.refreshToken)
wx.setStorageSync(STORAGE_KEYS.EXPIRE_TIME, data.expireTime) wx.setStorageSync(STORAGE_KEYS.EXPIRE_TIME, data.expireTime)
// 不打印 token 明文,只记录过期时间便于核对单位(秒/毫秒)
logger.debug(LOG, 'token 已写入, expireTime=', data.expireTime)
} }
export function clearTokens(): void { export function clearTokens(): void {
wx.removeStorageSync(STORAGE_KEYS.ACCESS_TOKEN) wx.removeStorageSync(STORAGE_KEYS.ACCESS_TOKEN)
wx.removeStorageSync(STORAGE_KEYS.REFRESH_TOKEN) wx.removeStorageSync(STORAGE_KEYS.REFRESH_TOKEN)
wx.removeStorageSync(STORAGE_KEYS.EXPIRE_TIME) wx.removeStorageSync(STORAGE_KEYS.EXPIRE_TIME)
logger.info(LOG, 'token 已清理')
} }
export function isLoggedIn(): boolean { export function isLoggedIn(): boolean {
+21
View File
@@ -154,3 +154,24 @@ export const ERROR_MESSAGES: Record<number, string> = {
19003: '服务繁忙,请稍后重试', 19003: '服务繁忙,请稍后重试',
19999: '服务繁忙,请稍后重试', 19999: '服务繁忙,请稍后重试',
} }
// ===== 日志配置 =====
// 注意:本文件保持零 import(纯常量),LogLevel 定义在此以避免 config→logger→config 循环依赖
// 日志级别:数值越大越严重,低于当前级别的日志不输出
export enum LogLevel {
DEBUG = 0,
INFO = 1,
WARN = 2,
ERROR = 3,
SILENT = 4, // 全部关闭
}
// 全局日志级别开关:
// null = 按运行环境自动判定(develop=DEBUG / trial=INFO / release=WARN)
// LogLevel.DEBUG = 手动写死,临时调级用(调试完记得改回 null)
export const LOG_LEVEL: LogLevel | null = null
// 是否写入微信实时日志(真机可在小程序后台「实时日志」查看线上问题)
// 仅 WARN 及以上写入,避免击穿实时日志配额
export const LOG_TO_REALTIME = true
+141
View File
@@ -0,0 +1,141 @@
import { LogLevel, LOG_LEVEL, LOG_TO_REALTIME } from './config'
/**
* 全项目统一日志出口。
*
* 用法:logger.debug('模块名', '消息', 任意参数)
* 级别:DEBUG < INFO < WARN < ERROR < SILENT,低于当前生效级别的调用直接返回,
* 且级别判断前置,不做任何字符串拼接,热路径(如每次请求)零开销。
*
* 输出:
* 1. console —— 按级别选用 log / warn / error,开发者工具与真机调试可见
* 2. 微信实时日志 —— 仅 WARN 及以上写入,可在小程序后台「实时日志」查询线上问题
*/
// 手动覆盖的级别(LOG_LEVEL 为 null 时按环境自动推导)
let manualLevel: LogLevel | null = LOG_LEVEL
// 解析后的生效级别缓存,避免每次调用都读环境信息
let resolvedLevel: LogLevel | null = null
const LEVEL_NAMES: Record<number, string> = {
[LogLevel.DEBUG]: 'DEBUG',
[LogLevel.INFO]: 'INFO',
[LogLevel.WARN]: 'WARN',
[LogLevel.ERROR]: 'ERROR',
}
// 按运行环境推导默认级别:开发版最详细,正式版只看异常
function resolveByEnv(): LogLevel {
try {
const info = wx.getAccountInfoSync()
const env = info.miniProgram && info.miniProgram.envVersion
if (env === 'develop') return LogLevel.DEBUG
if (env === 'trial') return LogLevel.INFO
return LogLevel.WARN
} catch (e) {
// 取不到环境信息时(低版本基础库等)退化为 DEBUG,保证开发期可见
return LogLevel.DEBUG
}
}
// 当前生效级别
export function getLevel(): LogLevel {
if (resolvedLevel === null) {
resolvedLevel = manualLevel === null ? resolveByEnv() : manualLevel
}
return resolvedLevel
}
/**
* 运行时调整日志级别(调级后对全局生效)。
* 传 null 恢复为「按环境自动」。
*/
export function setLevel(level: LogLevel | null): void {
manualLevel = level
resolvedLevel = null
}
// 实时日志单例(惰性初始化,取不到时降级为仅 console,绝不因日志抛错影响业务)
let realtimeManager: any = null
let realtimeInited = false
function getRealtimeManager(): any {
if (realtimeInited) return realtimeManager
realtimeInited = true
try {
if (typeof wx.getRealtimeLogManager === 'function') {
realtimeManager = wx.getRealtimeLogManager()
}
} catch (e) {
realtimeManager = null
}
return realtimeManager
}
// 序列化日志参数:实时日志有单条长度限制,做截断保护
function stringify(args: any[]): string {
try {
return args
.map((a) => {
if (typeof a === 'string') return a
try {
return JSON.stringify(a)
} catch (e) {
return String(a)
}
})
.join(' ')
.slice(0, 1000)
} catch (e) {
return ''
}
}
function emit(level: LogLevel, module: string, args: any[]): void {
if (level < getLevel()) return
const prefix = `[${LEVEL_NAMES[level]}][${module}]`
// console 通道
if (level === LogLevel.ERROR) {
console.error(prefix, ...args)
} else if (level === LogLevel.WARN) {
console.warn(prefix, ...args)
} else {
console.log(prefix, ...args)
}
// 实时日志通道:仅 WARN 及以上,避免击穿配额
if (!LOG_TO_REALTIME || level < LogLevel.WARN) return
const manager = getRealtimeManager()
if (!manager) return
const text = `${prefix} ${stringify(args)}`
try {
if (level === LogLevel.ERROR && typeof manager.error === 'function') manager.error(text)
else if (typeof manager.warn === 'function') manager.warn(text)
} catch (e) {
// 日志写入失败不影响业务
}
}
export const logger = {
/** 开发期流程细节:页面入参、请求发起/成功、数据条数 */
debug(module: string, ...args: any[]): void {
emit(LogLevel.DEBUG, module, args)
},
/** 关键流程节点:登录成功、刷新成功、下单/上传/分享成功 */
info(module: string, ...args: any[]): void {
emit(LogLevel.INFO, module, args)
},
/** 可恢复异常与静默降级:静默 catch、跳转失败、非关键请求失败 */
warn(module: string, ...args: any[]): void {
emit(LogLevel.WARN, module, args)
},
/** 真正的错误:网络异常、业务错误码、账号禁用、登录态失效 */
error(module: string, ...args: any[]): void {
emit(LogLevel.ERROR, module, args)
},
}
export default logger
+29 -10
View File
@@ -1,5 +1,9 @@
import { BASE_URL, API_PREFIX, ERROR_MESSAGES } from './config' import { BASE_URL, API_PREFIX, ERROR_MESSAGES } from './config'
import * as auth from './auth' import * as auth from './auth'
import { logger } from './logger'
// 日志模块名
const LOG = 'request'
// 业务错误对象(携带 code,页面可按 code 定制处理) // 业务错误对象(携带 code,页面可按 code 定制处理)
export class ApiError extends Error { export class ApiError extends Error {
@@ -44,12 +48,12 @@ function isAuthExpiredCode(code: number, statusCode?: number): boolean {
// 登录失效后的收尾:非关键请求只 reject,不动登录态 // 登录失效后的收尾:非关键请求只 reject,不动登录态
function failAuth(code: number, msg: string, silentAuth: boolean, showError: boolean) { function failAuth(code: number, msg: string, silentAuth: boolean, showError: boolean) {
if (!silentAuth) { if (!silentAuth) {
console.log('[auth] failAuth 清理登录态并跳转登录页, code=', code, 'msg=', msg) logger.warn(LOG, '登录态失效,清理 token 并跳转登录页, code=', code, 'msg=', msg)
auth.clearTokens() auth.clearTokens()
handleAuthExpired() handleAuthExpired()
if (showError) showToast('登录已过期,请重新登录') if (showError) showToast('登录已过期,请重新登录')
} else { } else {
console.log('[auth] failAuth 静默失败(不动登录态), code=', code) logger.warn(LOG, '登录态失效但静默处理(不动登录态), code=', code)
} }
return new ApiError(code, msg || '登录已过期') return new ApiError(code, msg || '登录已过期')
} }
@@ -69,11 +73,11 @@ function doRefresh(): Promise<boolean> {
const refreshToken = auth.getRefreshToken() const refreshToken = auth.getRefreshToken()
const accessToken = auth.getAccessToken() const accessToken = auth.getAccessToken()
if (!refreshToken || !accessToken) { if (!refreshToken || !accessToken) {
console.log('[auth] doRefresh 无 token,放弃刷新') logger.warn(LOG, 'doRefresh 缺少 refresh/access token,放弃刷新')
resolve(false) resolve(false)
return return
} }
console.log('[auth] doRefresh 发起刷新') logger.debug(LOG, 'doRefresh 发起刷新')
wx.request({ wx.request({
url: `${BASE_URL}${API_PREFIX}/user/refresh-token`, url: `${BASE_URL}${API_PREFIX}/user/refresh-token`,
method: 'POST', method: 'POST',
@@ -88,15 +92,15 @@ function doRefresh(): Promise<boolean> {
refreshToken: body.data.refresh_token, refreshToken: body.data.refresh_token,
expireTime: body.data.expire_time, expireTime: body.data.expire_time,
}) })
console.log('[auth] doRefresh 成功') logger.debug(LOG, 'doRefresh 成功')
resolve(true) resolve(true)
} else { } else {
console.log('[auth] doRefresh 响应异常, code=', body && body.code) logger.error(LOG, 'doRefresh 响应异常, code=', body && body.code)
resolve(false) resolve(false)
} }
}, },
fail: () => { fail: () => {
console.log('[auth] doRefresh 网络失败') logger.error(LOG, 'doRefresh 网络失败')
resolve(false) resolve(false)
}, },
}) })
@@ -127,17 +131,17 @@ let redirecting = false
*/ */
export function redirectToLogin(): void { export function redirectToLogin(): void {
if (redirecting) { if (redirecting) {
console.log('[auth] redirectToLogin 幂等锁生效,丢弃本次跳转') logger.debug(LOG, 'redirectToLogin 幂等锁生效,丢弃本次跳转')
return return
} }
const pages = typeof getCurrentPages === 'function' ? getCurrentPages() : [] const pages = typeof getCurrentPages === 'function' ? getCurrentPages() : []
const current = pages[pages.length - 1] const current = pages[pages.length - 1]
if (current && current.route === 'pages/login/login') { if (current && current.route === 'pages/login/login') {
console.log('[auth] redirectToLogin 已在登录页,跳过') logger.debug(LOG, 'redirectToLogin 已在登录页,跳过')
return return
} }
console.log('[auth] redirectToLogin 跳转登录页, 当前页=', current && current.route, '栈深=', pages.length) logger.info(LOG, '跳转登录页, 当前页=', current && current.route, '栈深=', pages.length)
redirecting = true redirecting = true
wx.reLaunch({ wx.reLaunch({
url: '/pages/login/login', url: '/pages/login/login',
@@ -171,6 +175,7 @@ export function request<T = any>(options: RequestOptions): Promise<T> {
return new Promise<T>((resolve, reject) => { return new Promise<T>((resolve, reject) => {
// 未登录:直接失败,不发请求,避免无 token 时批量 401 引发重定向风暴 // 未登录:直接失败,不发请求,避免无 token 时批量 401 引发重定向风暴
if (needAuth && !auth.isLoggedIn()) { if (needAuth && !auth.isLoggedIn()) {
logger.debug(LOG, '未登录直接 reject,不发请求, url=', url)
reject(new ApiError(NOT_LOGIN_CODE, '未登录')) reject(new ApiError(NOT_LOGIN_CODE, '未登录'))
return return
} }
@@ -185,6 +190,7 @@ export function request<T = any>(options: RequestOptions): Promise<T> {
if (token) finalHeader['Authorization'] = `Bearer ${token}` if (token) finalHeader['Authorization'] = `Bearer ${token}`
} }
logger.debug(LOG, '请求发起', method, fullUrl, 'needAuth=', needAuth, 'retried=', retried)
wx.request({ wx.request({
url: fullUrl, url: fullUrl,
method, method,
@@ -196,6 +202,7 @@ export function request<T = any>(options: RequestOptions): Promise<T> {
// 网络层异常:响应体无 code // 网络层异常:响应体无 code
if (!body || typeof body.code !== 'number') { if (!body || typeof body.code !== 'number') {
logger.error(LOG, '响应体无 code(网络层异常)', method, fullUrl, 'statusCode=', res.statusCode)
if (showError) showToast('网络异常,请稍后重试') if (showError) showToast('网络异常,请稍后重试')
reject(new Error('network error')) reject(new Error('network error'))
return return
@@ -205,6 +212,7 @@ export function request<T = any>(options: RequestOptions): Promise<T> {
// 成功 // 成功
if (code === 0) { if (code === 0) {
logger.debug(LOG, '请求成功', method, fullUrl)
resolve(body.data as T) resolve(body.data as T)
return return
} }
@@ -215,6 +223,7 @@ export function request<T = any>(options: RequestOptions): Promise<T> {
reject(failAuth(code, body.msg, silentAuth, showError)) reject(failAuth(code, body.msg, silentAuth, showError))
return return
} }
logger.info(LOG, '命中登录失效码,尝试刷新后重试, code=', code, 'url=', fullUrl)
doRefresh().then((ok) => { doRefresh().then((ok) => {
if (ok) { if (ok) {
doCall(true) doCall(true)
@@ -229,6 +238,7 @@ export function request<T = any>(options: RequestOptions): Promise<T> {
// 账号禁用(HTTP 200,需单独处理) // 账号禁用(HTTP 200,需单独处理)
if (code === 21001) { if (code === 21001) {
logger.error(LOG, '账号被禁用,清理登录态, code=', code)
auth.clearTokens() auth.clearTokens()
handleAuthExpired() handleAuthExpired()
if (showError) showToast(ERROR_MESSAGES[21001] || '账号已被禁用') if (showError) showToast(ERROR_MESSAGES[21001] || '账号已被禁用')
@@ -237,6 +247,7 @@ export function request<T = any>(options: RequestOptions): Promise<T> {
} }
// 其余业务错误 // 其余业务错误
logger.warn(LOG, '业务错误, code=', code, 'url=', fullUrl, 'msg=', body.msg)
if (showError) { if (showError) {
const msg = const msg =
(body.details && body.details[0]) || ERROR_MESSAGES[code] || body.msg || '请求失败' (body.details && body.details[0]) || ERROR_MESSAGES[code] || body.msg || '请求失败'
@@ -245,6 +256,7 @@ export function request<T = any>(options: RequestOptions): Promise<T> {
reject(new ApiError(code, body.msg || '请求失败', body.details)) reject(new ApiError(code, body.msg || '请求失败', body.details))
}, },
fail: () => { fail: () => {
logger.error(LOG, 'wx.request 失败(网络层)', method, fullUrl)
if (showError) showToast('网络异常,请检查网络后重试') if (showError) showToast('网络异常,请检查网络后重试')
reject(new Error('request fail')) reject(new Error('request fail'))
}, },
@@ -276,6 +288,7 @@ export function uploadFile<T = any>(
return new Promise<T>((resolve, reject) => { return new Promise<T>((resolve, reject) => {
// 未登录:直接失败,不发请求 // 未登录:直接失败,不发请求
if (!auth.isLoggedIn()) { if (!auth.isLoggedIn()) {
logger.debug(LOG, '未登录直接 reject,不上传, url=', url)
reject(new ApiError(NOT_LOGIN_CODE, '未登录')) reject(new ApiError(NOT_LOGIN_CODE, '未登录'))
return return
} }
@@ -285,6 +298,7 @@ export function uploadFile<T = any>(
const token = auth.getAccessToken() const token = auth.getAccessToken()
if (token) header['Authorization'] = `Bearer ${token}` if (token) header['Authorization'] = `Bearer ${token}`
logger.debug(LOG, '上传发起, url=', url, 'retried=', retried)
wx.uploadFile({ wx.uploadFile({
url: url.startsWith('http') ? url : `${BASE_URL}${API_PREFIX}${url}`, url: url.startsWith('http') ? url : `${BASE_URL}${API_PREFIX}${url}`,
filePath, filePath,
@@ -300,12 +314,14 @@ export function uploadFile<T = any>(
} }
if (!body || typeof body.code !== 'number') { if (!body || typeof body.code !== 'number') {
logger.error(LOG, '上传响应解析失败(非 JSON)', url, 'raw=', res.data)
if (showError) showToast('上传失败,请稍后重试') if (showError) showToast('上传失败,请稍后重试')
reject(new Error('upload fail')) reject(new Error('upload fail'))
return return
} }
if (body.code === 0) { if (body.code === 0) {
logger.info(LOG, '上传成功, url=', url)
resolve(body.data as T) resolve(body.data as T)
return return
} }
@@ -316,6 +332,7 @@ export function uploadFile<T = any>(
reject(failAuth(body.code, body.msg, silentAuth, showError)) reject(failAuth(body.code, body.msg, silentAuth, showError))
return return
} }
logger.info(LOG, '上传命中登录失效码,尝试刷新后重试, code=', body.code)
doRefresh().then((ok) => { doRefresh().then((ok) => {
if (ok) { if (ok) {
doUpload(true) doUpload(true)
@@ -326,12 +343,14 @@ export function uploadFile<T = any>(
return return
} }
logger.warn(LOG, '上传业务错误, code=', body.code, 'url=', url, 'msg=', body.msg)
const msg = const msg =
(body.details && body.details[0]) || ERROR_MESSAGES[body.code] || body.msg || '上传失败' (body.details && body.details[0]) || ERROR_MESSAGES[body.code] || body.msg || '上传失败'
if (showError) showToast(msg) if (showError) showToast(msg)
reject(new ApiError(body.code, body.msg || '上传失败', body.details)) reject(new ApiError(body.code, body.msg || '上传失败', body.details))
}, },
fail: () => { fail: () => {
logger.error(LOG, 'wx.uploadFile 失败(网络层), url=', url)
if (showError) showToast('上传失败,请检查网络后重试') if (showError) showToast('上传失败,请检查网络后重试')
reject(new Error('upload fail')) reject(new Error('upload fail'))
}, },
+9
View File
@@ -16,3 +16,12 @@ export function syncTabBarUnread(force = true): void {
const tabBar = getTabBarInstance() const tabBar = getTabBarInstance()
if (tabBar && typeof tabBar.syncUnread === 'function') tabBar.syncUnread(force) if (tabBar && typeof tabBar.syncUnread === 'function') tabBar.syncUnread(force)
} }
// 更新选中态:走 tabBar 的 setSelected(内部保证首帧安全),
// 老版本实例没有该方法时降级为 setData
export function setTabBarSelected(index: number): void {
const tabBar = getTabBarInstance()
if (!tabBar) return
if (typeof tabBar.setSelected === 'function') tabBar.setSelected(index)
else tabBar.setData({ selected: index })
}