# 项目长期记忆

## 项目概述
- 项目名：xinlong-shop-taro（Taro 微信小程序商城）
- 技术栈：Taro + React + TypeScript + Tailwind CSS
- 后端 API 前缀：`/shop/`、`/api/`
- **后端项目路径：`/Users/gxwebsoft/JAVA/guilixu-java`**（⚠️ 不要找 paopao-java 或 websopy-java，那是旧的）
- **后端 Maven 编译命令**：`cd /Users/gxwebsoft/JAVA/guilixu-java && "/Applications/IntelliJ IDEA Ultimate.app/Contents/plugins/maven/lib/maven3/bin/mvn" compile`（guilixu 的 mvnw 损坏，用 IntelliJ 自带的 mvn）
- 门店店员身份判断：通过 `getMyClerk()` API (`/shop/shop-store-user/my`) 返回值判断
- VIP/分销商申请记录表：`ShopDealerApply`，状态码 10待审核/20通过/30驳回

## 用户状态与禁用机制
- 用户状态字段：`User.status`，`0` = 正常，`1` = 禁用
- 禁用接口：`updateUserStatus(userId, status)` → PUT `/system/user/status`
- 禁用拦截工具：`src/utils/auth.ts` 中 `isUserDisabled(user)` 和 `blockDisabledUser(user)`
- 拦截层级：UserContext 启动异步校验 + refreshUser/syncFromStorage/loginUser 检查 + 登录 API 和登录页检查
- 用户管理页：`src/pages/store/users/index.tsx`（门店中心入口，支持搜索/筛选/查看详情/禁用启用）

## VIP 会员机制
- 用户在 `vip-upgrade` 页面提交门店名称和地址申请
- 门店店员在 `vip-review` 页面审核（通过/驳回）
- VIP 会员特权：商品显示 `dealerPrice` 价格，下单使用该价格结算
- VIP 状态判断：`isVipMember()` 从 localStorage 缓存读取（同步），`checkAndCacheVipStatus(userId)` 异步更新
- **响应式 VIP 状态**：使用 `useVipStatus` hook（`src/hooks/useVipStatus.ts`），组件挂载时异步校验并更新缓存 + React state，isVip 变化触发重渲染
- **已接入 `useVipStatus` 的页面/组件**：商品详情、结算、商城首页、分类页、购物车页、CartContext、SkuSelector、ProductCard
- `OrderGoodsItem` 接口新增 `price` 字段，VIP 下单时传 dealerPrice 给后端

## 商品价格字段约定
- `product.price` - 到手价（主价格）
- `product.salePrice` - 市场价（划掉的）
- `product.dealerPrice` - VIP 会员专享价
- `product.memberStorePrice` - 会员价

## 关键文件位置
- VIP 工具：`src/utils/vip.ts`
- VIP Hook：`src/hooks/useVipStatus.ts`
- VIP 审核页：`src/pages/user/vip-review/index.tsx`
- VIP 升级页：`src/pages/user/vip-upgrade/index.tsx`
- 门店中心：`src/pages/store/center/index.tsx`
- 购物车 API：`src/api/shop/shopCart/index.ts`（批量删除接口 `/shop/shop-cart/batch` 后端期望直接接收 `ArrayList<Integer>` 即 `[1,2,3]`，不要用 `{ ids }` 包裹）
- 购物车 Context：`src/contexts/CartContext.tsx`
- 购物车合并加购：后端 `ShopCartServiceImpl.addToCart()` 按 user_id+goods_id+sku_id 判重，已存在则累加 cart_num，不存在才新增；skuId 为 null/0（单规格）匹配 IS NULL 或 =0。`ShopCart.userId` 是 Integer（非 Long）。历史重复数据清理脚本 `sql/shop_cart_merge_duplicates.sql`

## 订阅消息
- 模板 ID：`sh1K9iK7vZjebUNFu6OsMsnsJxm4whThWGrhN7I4zVg`（交易提醒）
- 字段：订单号(`character_string1`)、商品名称(`thing4`)、联系人(`thing10`)、联系电话(`phone_number7`)、送货地址(`thing8`)
- 场景说明：下单提醒
- 新订单检测 Hook：`src/hooks/useNewOrderDetector.ts`（30秒轮询，页面隐藏暂停）

## 微信隐私协议（基础库 3.16.1+ 强制）
- 报错特征：`chooseImage:fail api scope is not declared in the privacy agreement` + `errno:112`
- 两处代码修复：
  1. 调用 `Taro.chooseImage` 前必须先 `getPrivacySetting` → `requirePrivacyAuthorize` 预检（封装在 `src/api/system/file/index.ts` 的 `ensurePrivacyAuthorized()` 里）
  2. `src/app.tsx` 的 `useLaunch` 中注册 `Taro.onNeedPrivacyAuthorization` 回调（用 `showModal` 自定义弹窗）
- 一处手动配置：**小程序管理后台** → 设置 → 第三方设置 → 用户隐私保护指引 → 添加「开发者收集你的相册/摄像头」声明
- ⚠️ **`requiredPrivateInfos` 字段只接受位置类 API 白名单**（chooseAddress/chooseLocation/choosePoi/getFuzzyLocation/getLocation/onLocationChange/startLocationUpdate/startLocationUpdateBackground），不要加 `chooseImage`/`chooseMedia` 等非位置类，会导致 app.json 解析失败
- 直接调 `Taro.chooseImage` 的页面：`pages/order/evaluate/index.tsx`、`pages/after-sale/apply/index.tsx`、`pages/store/orders/index.tsx` —— 这些页面绕过了 `uploadFile`，需要单独加 `ensurePrivacyAuthorized` 预检

## 手机号授权登录降级方案（2026-07-15）
- **业务现实**：`getPhoneNumber` 按钮被用户拒绝后，微信会短期"记住"用户的选择，再次点击会直接 fail（`errMsg: getPhoneNumber:fail user deny`）。一旦进入这个状态，前端必须提供降级入口
- **统一模式**：`passport/login.tsx` 和 `passport/register.tsx` 都有 `getPhoneNumber` 回调，**不允许**只 toast 一句"未授权手机号"就完事
  - 必做：失败时调 `showPhoneAuthFailedModal(errMsg)`，按 errMsg 区分场景弹窗（user deny / privacy / no permission / 通用）
  - 弹窗带"短信登录"按钮自动跳 `/passport/sms-login`（透传 redirect + 邀请参数 inviter/source/t）
  - 必做：主登录按钮**长期可见**地提供"使用短信验证码登录"链接，不要依赖弹窗
- **短信登录页**：`/passport/sms-login`（路由表里已有），调 `loginBySms` + `sendSmsCaptcha` 接口
- **已落地页面**：`passport/login.tsx`（新增 goSmsLogin + showPhoneAuthFailedModal + alt 链接）、`passport/register.tsx`（新增 showPhoneAuthFailedModal + weapp 下也显示短信按钮）

## 分享 / 朋友圈能力（2026-07-13 接入）
- 统一封装：`src/hooks/useShare.ts`（`useShare({title, path?, query?, imageUrl?, enableTimeline?, enableCopyUrl?})`，一次注册 `useShareAppMessage` + `useShareTimeline` + 可选 `onCopyUrl` 复制链接；自动在 path/query 追加 `inviter=${当前用户id}` 做裂变归因；`isTimelineSinglePage()` 判断朋友圈单页模式 scene===1154）
- 海报组件：`src/components/SharePoster/index.tsx`（Canvas 2D 绘制封面+标题+价格+小程序码，返回临时图路径作 imageUrl）
- 小程序码接口：`src/api/share.ts` `generateShareCode(page, inviterId)` 复用 `generateMiniProgramCode`（scene 用 `uid_${inviterId}`，page 透传但后端需读取转发才生效）
- 裂变约定：分享链接 query 用 `inviter`，与 `src/utils/invite.ts` 的 `parseInviteParams` 读取字段一致
- 已接入页面：商品详情、积分商品、拼团、秒杀、活动、赛事（带 canvas 海报）；邀请下级、分销推广、升级VIP、分享赚佣金（仅 enableShare + useShare，无海报）；首页（好友+朋友圈+复制链接三按钮全亮）
- 注意：tabBar 页**也支持朋友圈分享**（之前误判已纠正）——只要 config 开 enableShareTimeline + 定义 onShareTimeline 即显示按钮；"tabBar 不渲染"仅指朋友圈打开后的单页模式里底部 tabBar 不显示。朋友圈/复制链接开发者工具可能看不到入口，必须真机测试。复制链接是 Beta（基础库 2.14.3+）。

## 订单备注字段约定（2026-07-15）
- `buyerRemarks` — 买家备注：用户下单时填写的订单备注（checkout 页提交）
- `merchantRemarks` — 商户备注：门店修改金额时填写的原因记录
- `comments` — 系统/其他备注：保留给系统内部使用
- `ShopOrder` 和 `OrderCreateRequest` 接口均已包含 `buyerRemarks` 和 `merchantRemarks` 字段

## 用户浏览记录功能（2026-07-15）
- 存储模式：**去重累加**（同一用户+商品只留一条，visit_count 累加，5分钟内去重）
- 数据库表：`shop_goods_browse`（字段：id/user_id/goods_id/tenant_id/merchant_id/visit_count/last_visit_time/browse_source/create_time/update_time）
- 唯一索引：`uk_user_goods (user_id, goods_id)`；查询索引：`idx_user_time`、`idx_goods_time`
- 后端 API 路径：`/api/shop/shop-goods-browse`（POST 上报 / GET page 我的足迹 / DELETE {id} 删单条 / DELETE 清空 / GET admin/page 后台查询）
- 前端 API：`src/api/shop/shopGoodsBrowse/index.ts`（reportBrowse 静默上报 / pageBrowseHistory / deleteBrowseRecord / clearBrowseHistory）
- 商品详情页 `product-detail.tsx` 的 `addToHistory()` 已接入：已登录时异步调 reportBrowse，未登录仅本地 browse_history
- 浏览历史页 `src/pages/user/history-list/index.tsx` 已改为服务端分页（登录），未登录回退本地存储
- 后端文件：`ShopGoodsBrowse.java`(entity) / `ShopGoodsBrowseParam.java` / `ShopGoodsBrowseMapper.java`+XML / `ShopGoodsBrowseService.java`+Impl / `ShopGoodsBrowseController.java`
- 建表 SQL：`/Users/gxwebsoft/JAVA/guilixu-java/sql/shop_goods_browse.sql`
- 后台管理前端：`/Users/gxwebsoft/VUE/guilixu-admin`（Vue3 + Ant Design Vue + Vite，非 RuoYi）
- 注意：`BaseController.success()` 有 `success(IPage<T>)` 重载，泛型方法返回类型用 `ApiResult<?>` 而非 `ApiResult<PageResult<T>>`（否则 fail() 分支类型不兼容）

## tabBar 配置（2026-07-15）
- 当前 4 个 tabBar：`/pages/index/index`（首页）、`/pages/shop/index`（分类）、`/pages/shop/cart`（购物车）、`/pages/user/user`（我的）
- 「订单」(pages/order/list) 已从 tabBar 移除，改为普通页，所有进入订单列表的地方用 `navigateTo` 而非 `switchTab`
- 多处 tabBar 判断列表（login/register/sms-login/auth-flow/user/index/index/index）已统一为实际 4 个 tabBar 路径
- ⚠️ pages/user/index.tsx:140 仍用 `switchTab(points/index)`，积分页非 tabBar，待修
