# 2026-07-15

## shopOrder 选项卡默认改为全部

- 需求：`/shop/shopOrder` 选项卡默认从"待发货"改为"全部"。
- 改动 `src/views/shop/shopOrder/index.vue`：`activeKey` 初始值在路由参数 `tab` 无效时的回退值由 `'undelivered'` 改为 `'all'`。
- `datasource` 中 `getStatusFilterByTab('all')` 返回 `undefined`，即不传 `statusFilter`，查全部订单，符合预期。

## 修复 shopOrder 页面默认待发货tab数据不正确

- 根因：`activeKey` 默认为 `'undelivered'`，但 `datasource` 函数只设了 `where.type = 0`，没有设 `statusFilter`。`statusFilter` 只在用户点击 tab 触发 `onTabs` 时才设置。所以页面初次加载时查的是全部订单而非待发货订单。
- 修复：提取 `getStatusFilterByTab(key)` 公共映射函数，在 `datasource` 中当 `where.statusFilter` 未设置时从 `activeKey` 自动推导。`onTabs` 也简化为复用同一函数。
- 文件：`src/views/shop/shopOrder/index.vue`

## Dashboard 订单总数点击跳转支持指定 tab

- Dashboard 中"订单总数"/"总营业额"跳转链接改为 `/shop/shopOrder?tab=all`，"待发货订单"改为 `?tab=undelivered`，"退款申请"改为 `?tab=refunded`。
- shopOrder 页面读取 `route.query.tab` 初始化 `activeKey`，无效值回退到 `'undelivered'`。
- 文件：`src/views/shop/dashboard/index.vue`、`src/views/shop/shopOrder/index.vue`

## Dashboard 待处理事项新增"待付款订单"

- 在待发货订单上方新增"待付款订单"统计项，使用 `statusFilter=0` 查询，点击跳转 `/shop/shopOrder?tab=unpaid`。
- shopOrder 页面取消注释"待付款"tab，`validTabs` 加入 `unpaid`。
- 新增 `dot-gold` 样式。
- 文件：`src/views/shop/dashboard/index.vue`、`src/views/shop/shopOrder/index.vue`

## Dashboard 引入 useTenantStore，创建时间读租户信息

- 引入 `useTenantStore`，`loadData` 中调用 `tenantStore.fetchTenantInfo()` 替代直接调用 `getTenantInfo()` API。
- 基本信息"创建时间"改为读取 `tenantStore.company?.createTime`。
- 运行天数也改为从 `tenantStore.company?.createTime` 计算，移除独立的 `tenantCreateTime` ref。
- 文件：`src/views/shop/dashboard/index.vue`

## Dashboard 欢迎横幅新增商城Logo

- 通过 `getShopSettingCategoryValues('basic')` 获取商城设置，读取 `shopLogo` 字段。
- 使用 `getCompressedImageUrl(shopLogo, { width: 200, quality: 90 })` 压缩图片。
- 在 welcome-banner 左侧用 `a-avatar` (64px, square) 展示，无 Logo 时不显示。
- 文件：`src/views/shop/dashboard/index.vue`

## 新增 useAppSubscriptionStore（websopy 应用订阅特殊接口）

- 新增 `src/api/app/appSubscription/model.ts`：AppSubscription 实体及参数/返回类型，对照后端 `com.gxwebsoft.app.entity.AppSubscription`。
- 新增 `src/api/app/appSubscription/index.ts`：封装 `AppSubscriptionController` 全部 14 个接口。
  - 特殊接口域名 `https://websopy-api.websoft.top/api`（与项目主 API 域名不同），复用 `@/utils/request`（共享登录态 token、401 处理），传绝对 URL。
  - 后端无 context-path；baseURL=`https://websopy-api.websoft.top/api`，接口路径用 `/app/subscription/xxx`。
  - 后端 `BaseController` 成功 code=0；`success(IPage)` 转成 `PageResult{list,count}`，与项目 `@/api` 的 PageResult 结构一致。
- 新增 `src/store/modules/appSubscription.ts`：`useAppSubscriptionStore`（Pinia Options API 风格，对齐 site.ts/statistics.ts），聚合列表/详情/支付状态/订阅管理方法。列表/详情缓存5分钟；`checkStatus`/`checkPurchased` 实时请求；订阅/支付/管理类操作成功后调 `invalidateCache()` 失效缓存。

## 确认线下收款新增上传凭证功能

- 背景：原"确认线下收款"用 `Modal.confirm` + `createVNode` 动态拼一个 input，只有备注，没有上传凭证。用户需要加凭证上传。
- 字段决策：新增 `paymentVoucher?: string` 字段（不复用 comments/merchantRemarks，因为凭证是图片URL非文本）。顺带修复 `buyerRemarks`/`merchantRemarks` 类型 bug（`undefined` → `string?`）。
- 重构为独立组件 `src/views/shop/shopOrder/components/OfflinePaymentModal.vue`：
  - 用 `<a-modal>` + `<a-form>` + `<a-upload list-type="picture-card">` 单张图片上传。
  - 上传走 `uploadOss`（`src/api/system/file`），存储返回的 `path`（与项目 `UploadCert` 组件约定一致），预览用 `getUrl(path)` 补全。
  - 凭证必填，备注可选；校验 jpg/png、≤10MB；支持点击预览大图。
- API `confirmOfflinePayment(id, remarks?, paymentVoucher?)` 新增第三参数，通过 query params 传给后端。
- `index.vue`：移除原 `Modal.confirm`/`createVNode` 逻辑，改为打开新组件（`v-model:visible` + `:data="current"` + `@done="reload"`），与 `DeliveryModal` 模式一致。
- 类型检查：新增 0 错误，顺带消掉原 `okType: 'success'` 的类型错误（antdv4 ButtonType 不含 success，新组件去掉了 ok-type）。
- 改动文件：`src/api/shop/shopOrder/model/index.ts`、`src/api/shop/shopOrder/index.ts`、`src/views/shop/shopOrder/components/OfflinePaymentModal.vue`（新增）、`src/views/shop/shopOrder/index.vue`
- 后端待办（已完成）：后端项目 `/Users/gxwebsoft/JAVA/guilixu-java`（MyBatis-Plus 3.4.3.3，无 Flyway/Liquibase，DDL 手动管理，SQL 脚本放 `sql/` 目录）：
  - `ShopOrder.java` 实体新增 `paymentVoucher` 字段（payTime 之后）。
  - `ShopOrderController.confirmOfflinePayment` 新增 `@RequestParam paymentVoucher`。
  - `ShopOrderService` 接口 + `ShopOrderServiceImpl` 实现方法签名加 `paymentVoucher` 参数，lambdaUpdate 加 `.set(paymentVoucher 非空, ShopOrder::getPaymentVoucher, paymentVoucher)`。
  - DDL 脚本 `sql/shop_order_payment_voucher.sql`：`ALTER TABLE shop_order ADD COLUMN payment_voucher VARCHAR(500) NULL ... AFTER pay_time`。
  - 注意：方法签名从 2 参变 3 参，全项目仅 Controller 一处调用，无断裂。

## Dashboard 到期时间显示订阅信息 + 立即订阅/扫码支付

- 改造 `src/views/shop/dashboard/index.vue` 基本信息「到期时间」行：三态渲染（active 显示 expireTime + 已激活 tag + ≤7天到期提醒；pending 显示待支付 + 去支付；无订阅显示立即订阅）。
- **productId 关联条件**：`app_product.tenantId = userStore.info.tenantId`（非 siteInfo.appId）。通过 `pageProducts({tenantId,current:1,size:1})` 取该租户应用。
- 新增 `src/api/app/appProduct/{model.ts,index.ts}`：AppProduct 类型 + `pageProducts` 接口（GET /api/app/product/page，分页参数 current/size，注意与项目 page/limit 不同）。
- 「立即订阅」流程：选 month/year → 免费应用调 `subscribe()` 直接激活；付费应用调 `generatePayQrcode()` 生成小程序码 → Modal 展示二维码 → 每 2.5s 轮询 `checkStatus(subscriptionNo)`，paid 后关闭+刷新（最多 5 分钟）。envVersion 按 `import.meta.env.DEV` 自动判断 trial/release。
- pending 订阅「去支付」复用 `subscriptionStore.pay(id,'wechat')`（用 `'miniappQrcode' in result` 类型守卫窄化）生成小程序码，不重复创建订阅。
- 小程序端支付页 `websopy-taro/src/passport/pay/index.tsx`：扫码 scene=subscriptionNo → detail-by-no → mp-prepay → requestPayment → mp-confirm，后端写 Redis `wxpay:paid:{no}=1`，Web 端 checkStatus 据此感知。
- 移除 dashboard 不再使用的 `siteInfo` 解构（原到期时间读 `siteInfo.expirationTime`，已改为订阅 expireTime），修复 noUnusedLocals 报错。

## 修复后端 generatePayQrcode 报错 "Field 'price_type' doesn't have a default value"

- 现象：dashboard 点「立即订阅」付费应用调 `generatePayQrcode` 时，后端 insert app_subscription 报 `price_type` 无默认值。
- 根因：后端 `AppSubscriptionController.generatePayQrcode`（websopy-java）创建订阅时漏了 `setPriceType()`，而 DB `price_type` 为 NOT NULL 无默认值。对比 `subscribe` 方法有 `setPriceType`。
- 参考 `websopy-pc/app/pages/console/pay/[subscriptionNo].vue`：它用 `subscribe` 创建订阅 + `pay(id,'wechat')` 生成二维码（两步），避开了 `generatePayQrcode`，所以没遇到此 bug。
- 修复（后端 `AppSubscriptionController.java` generatePayQrcode 方法）：补全 `setPriceType(product.getPriceType())` + `setOriginalPrice`/`setPayStatus(0)`/`setTenantId`，对齐 subscribe。**前端无需改动**，重启后端即可。
- 注意：`subscribe` 价格计算用 `price/100`（当分转元），`generatePayQrcode` 直接用 `price`（当元），两者不一致；schema 标注 price 为「元」，故 `generatePayQrcode` 价格逻辑更合理，保留不动。

## 修复价格多了100倍（product.price 实际单位是分）

- 用户反馈：`generatePayQrcode` 算出的金额多了100倍。确认 `product.price` 实际存的是**分**（非 schema 标注的元），故 `subscribe` 的 `/100` 是对的，`generatePayQrcode` 直接用 price 是错的。
- 后端 `AppSubscriptionController.generatePayQrcode` 价格计算改为 `price/100` 转元（对齐 subscribe），年付按10个月（*10，对齐 subscribe 年付优惠），季付*3，月付*1。
- 前端 `dashboard/index.vue` 参考价同步修正：`monthPrice = price/100`，`yearPrice = monthPrice*10`（原为 price 直接 + *12）。
- 结论：product.price 实际单位是**分**，所有价格展示/计算都需 /100 转元。

## 改用 subscribe+pay 避开 generatePayQrcode 的 price_type 报错

- 现象：用户反馈 `generatePayQrcode` 仍报 `price_type` 无默认值（报错 SQL 字段集是修复前的，说明后端未重启/未重编译，`setPriceType` 修复未生效）。
- 关键认知：`generatePayQrcode` 后端**不接收 priceType 参数**（从 `product.getPriceType()` 取），所以前端传 priceType 给它也没用，无法绕过。
- 修复（前端方案，不依赖后端重启）：dashboard `confirmSubscribe` 改用 `subscribe` + `pay` 两步（参考 websopy-pc 的 `pay/[subscriptionNo].vue` 模式）：
  - `subscribe` 创建订阅（后端有 `setPriceType`，不报错；价格 `/100` 正确）→ 免费 `status=active` 直接成功
  - 付费 `status=pending` → `pay(subscriptionId,'wechat')` 生成小程序码 → Modal 展示 + 轮询 `checkStatus`
- 移除 `confirmSubscribe` 里的 `isFreeProduct` 分支（改用 `subResult.status` 判断）；`isFreeProduct` 仍用于订阅 Modal 的免费提示。
- vue-tsc 验证 dashboard 零 error TS（总错误 1778 不变）。
- 后端 `generatePayQrcode` 的 `setPriceType` 修复仍保留（重启后端后该接口也能用），但前端已不依赖它。

## 订阅 productId 改为固定 65（小程序商城）

- 需求：dashboard 立即订阅请求的 productId 从动态（按 `tenantId` 查 `app_product`）改为写死 `65`。
- 改动 `src/views/shop/dashboard/index.vue`：
  - imports：`pageProducts` → `getProductDetail`。
  - `loadSubscriptionInfo`：移除 `userStore.info.tenantId` 判断和 `pageProducts({tenantId,current:1,size:1})` 调用，改为 `getProductDetail(65)` 查固定产品详情；定义 `FIXED_PRODUCT_ID = 65` 常量，订阅列表过滤和 `currentProduct.productId` 统一使用该常量。
- `confirmSubscribe` 无需改动（用 `currentProduct.productId`，已自动是 65）。
- `getProductDetail(65)` 返回的 product 含 name/priceType/price，前端参考价 `monthPrice = price/100`、`yearPrice = monthPrice*10` 仍适用。
- vue-tsc 验证 dashboard 零 error TS（总错误 1778 不变）。

## shopGoodsBrowse 列表商品图片改用 OSS 压缩函数

- 文件：`src/views/shop/shopGoodsBrowse/index.vue`
- 引入 `getCompressedImageUrl`（来自 `src/utils/image.ts`），将商品图片 `:src="record.goodsImage"` 改为 `:src="getCompressedImageUrl(record.goodsImage)"`。
- 与 `shopGoods`、`shopOrder`、`dashboard` 等页面保持一致：默认宽度 240px、质量 90、自动补全 OSS 域名、跳过已含 `x-oss-process` 的 URL。
- 预览行为未改（仍走 a-image 默认 src 预览）。

## 电脑版后台新订单红点提示（全局）

- 需求：电脑版后台有新订单时显示红点，让管理员知道有新订单。
- 关键发现：系统已有菜单红点基础设施（`menu-title.vue` 读 `item.meta.badge` 渲染徽章；`user.ts` `setMenuBadge(path,value,color)`；`layout/index.vue` 有 `.ele-menu-badge` 样式），但 `useOrderNotify` 原为组件级（dashboard/cms dashboard/shopOrder 三处各自独立轮询），未接通红点。
- 改造 `src/views/shop/shopOrder/useOrderNotify.ts` 为**模块级全局单例**：
  - 轮询/状态/红点全部模块级变量（enabled、hasNewOrder、lastOrderId、timer），多组件调用只一个轮询。
  - 检测到新订单 → triggerNotify（叮声+语音+遍历回调Set）+ `setOrderBadge(true)` 调 `userStore.setMenuBadge('/shop/shopOrder','dot','#ff4d4f')`。
  - 新增 `clearBadge()`：立即清红点 + 重查最新订单同步 lastOrderId（标记"已查看"）。
  - `onNewOrder` 回调用 Set 管理，组件 onBeforeUnmount 自动移除；`start()` 幂等，由 layout 调一次。
- `src/layout/components/menu-title.vue`：`badge==='dot'` 时用 `<a-badge dot />` 纯红点，否则原数字徽章逻辑。
- `src/layout/index.vue`：setup 调 `useOrderNotify().start()` 全局启动，后台任意页面生效。
- `src/views/shop/shopOrder/index.vue`：`onMounted` + `onActivated` 调 `clearBadge()`（兼容 keep-alive `cache-key`，用 `orderBadgeMounted` 标志防首次双触发）。
- `src/views/shop/dashboard/index.vue`：`quickLinks` 改 computed，订单管理项 `badge: hasNewOrder.value`；`quick-icon` 加红点 span + pulse 动画样式。
- 决策（用户确认）：红点=新订单未查看（进订单页清除）；位置=订单管理菜单+dashboard快速入口订单管理图标；声音保留并全局化。
- 构建验证：`vite build --outDir dist_verify` 通过（EXIT:0）。原 `pnpm build` 失败仅因 vite 清空 `dist/assets` 被安全删除保护拦截（83文件>50阈值），与代码无关。

## shopOrder 订单详情页（orderInfo.vue）支付方式下增加支付凭证图

- 背景：之前 `OfflinePaymentModal` 确认线下收款时已支持上传 `paymentVoucher`（OSS path），但订单详情页只展示「线下付款」tag，看不到凭证图。
- 改动 `src/views/shop/shopOrder/components/orderInfo.vue`：
  - 模板：在「支付方式」`<a-descriptions-item>` 之后、「开票状态」之前新增「支付凭证」项，`v-if="form.paymentVoucher"`，`:span="3"` 占满整行。
  - 缩略图用 `a-image`，`:width="120"`，`getCompressedImageUrl(form.paymentVoucher, { width: 750 })`（按用户要求宽度 750），`preview-src-list` 用 `ensureFullUrl(form.paymentVoucher)` 给原图预览。
  - 引入 `ensureFullUrl` 来自 `@/utils/image`（已有 `getCompressedImageUrl`）。
  - form reactive 默认值新增 `paymentVoucher: undefined`，对齐 `ShopOrder` model。
- `ShopOrder` model 中 `paymentVoucher?: string` 字段早就存在（payTime 之后），无需改 model/API。
- 用户场景：仅线下付款（payType=9）的订单才有此凭证图，条件渲染自然处理。

## Dashboard 基本信息 6 字段数据源重构（product + subscription）

- 需求：dashboard「基本信息」面板原先混用硬编码/tenant/siteStore，改为统一从 `app_product.product_id=65` + `app_subscription` 读取，语义更准确。
- 字段映射决策（用户确认）：
  - 系统名称 → `currentProduct.productName`（原硬编码 '小程序商城'）
  - 版本号 → `currentProduct.version`（原硬编码 '2.0.0'）
  - 运行状态 → `currentProduct.publishStatus` + 中文映射 + a-tag 颜色（原 `siteStore.statusText`）
  - 创建时间 → `currentSubscription.startTime`（原 `tenantStore.company?.createTime`）
  - 到期时间 → `currentSubscription.expireTime`（保持不变，原本就是这个）
  - 系统运行天数 → 自 `currentSubscription.startTime` 起累计（原自 `tenantStore.company?.createTime`；后改为 startTime，与"创建时间"行同源，语义统一）
- 改动 `src/views/shop/dashboard/index.vue`：
  - 删除硬编码 `systemInfo` reactive（已确认 cms 那两个 systemInfo 是独立 const，互不影响）。
  - import 移除 `reactive`。
  - 新增 `productStatusText` computed：publishStatus → {text,color} 映射（published=已发布/green、pending_review=审核中/orange、developing=开发中/blue、rejected=已驳回/red、deprecated=已下架/red），未知状态显示原始值，无 product 显示 '-'。
  - 重写 `runDays` computed：数据源由 `tenantStore.company?.createTime` 改为 `currentSubscription.value?.startTime`（与"创建时间"行同源），注释说明续费新增订阅记录会重置计数的边界行为。
  - 模板 6 个字段全部重绑，每个字段加 `subscriptionLoading` 骨架屏分支（避免空白/老数据闪现）。
  - 「系统运行」加 `a-tooltip title="自您首次开通小程序商城起"`（用户要求）。
- 数据流复用：`loadSubscriptionInfo()` 原本就调 `getProductDetail(65)` 填充 `currentProduct`，无需新增接口调用。
- 类型校验：vue-tsc 对 `shop/dashboard/index.vue` 行级错误 0 条；cms 那批历史错误与本次无关。

## Dashboard 到期时间续费功能 + 标签矛盾修复

- 背景：dashboard「到期时间」行原先在订阅已过期时同时显示绿色「已激活」标签 + 「（已过期）」红字，状态自相矛盾；且即将到期/已过期时无续费入口（用户反馈"没有按钮呢"）。
- 改动 `src/views/shop/dashboard/index.vue`：
  - 标签动态化：`a-tag :color="isExpired ? 'red' : 'green'"`，文案 `已过期/已激活`；已过期时显示「（已过期N天）」。
  - 新增续费按钮：`showRenewButton`（`daysToExpire <= 7` 即即将到期或已过期时显示），点击 `onRenew`。
  - 复用订阅 Modal：新增 `subscribeMode`（'subscribe'|'renew'）+ `renewing` 状态，Modal 的 title/okText/confirm-loading/@ok 按 mode 动态切换；免费应用 alert 文案也按 mode 区分。
  - `confirmRenew` 调 `subscriptionStore.renew(sub.id, selectedPeriod)`（POST /app/subscription/renew/{id}?period=month|year），成功后 `refreshSubscription()`。
  - `onSubscribe` 补设 `subscribeMode='subscribe'`。
- 续费 API 语义：`renewSubscription` 返回 string（同步完成，疑似余额扣款/免费续期）。若后端续费需走微信扫码支付，需改为返回支付二维码结构并接 pay 流程——待后端确认。
- 关键字段再确认（用户）：开始时间用 `appSubscription.startTime`，到期时间用 `appSubscription.expireTime`，daysToExpire 与 `dayjs()`（浏览器本地时间）对比。

## Dashboard 续费改走 subscribe+pay 扫码（后端 renew 接口未实现）

- 触发：用户截图显示 `POST /api/app/subscription/renew/90` 返回 404，确认后端没有实现 renew 接口。
- 方案：续费不再调 `store.renew` / `renewSubscription`，改为复用现有 `subscribe + pay` 扫码支付链路（与首次订阅完全一致）。后端 subscribe 对已有 active 订阅会处理续期/创建新记录。
- 改动 `src/views/shop/dashboard/index.vue` 的 `confirmRenew`：
  - 删掉 `await subscriptionStore.renew(sub.id, ...)` 改为 `subscribe({ productId, subscriptionPeriod })`。
  - 免费应用走 `subResult.status === 'active'` 分支，提示「续费成功」并刷新。
  - 付费应用走 `pay(subscriptionId, 'wechat', envVersion)` → 拿到 `miniappQrcode` → 弹支付 Modal + `startPolling`。
  - `payProductName` fallback 文案改为「应用续费」（与订阅的「应用订阅」区分）。
- `onRenew` 入口、`subscribeMode='renew'`、续费 Modal title/按钮复用不变，UI 上用户感知不到底层调的是 subscribe。
- 教训/记录：调后端接口前最好先确认接口是否真实存在；前端 `api/app/appSubscription/index.ts` 的 `renewSubscription` 函数可保留（防止后端后续补上接口），但 dashboard 暂不调用。

## Dashboard 续费最终定稿：方案1 renew+pay（前端已就绪，等后端实现）

- 触发：上一步用 subscribe 走续费，后端返回「您已订阅该应用，无需重复购买」——subscribe 对已有 active 订阅是拒绝的，续费不能复用 subscribe。
- 定稿方案（用户确认）：方案1 —— 后端实现 `/renew/{id}?period=xxx` 返回 `SubscribeResult`（含 subscriptionId），前端再调 `pay` 生成小程序码。与 subscribe+pay 对称，职责清晰。
- 三处改动（前端已全部就绪，等后端补 renew 接口即可联调通）：
  1. `src/api/app/appSubscription/index.ts` `renewSubscription`：返回类型 `string` → `SubscribeResult`；取 `res.data.data`（原取 `res.data.message`）。
  2. `src/store/modules/appSubscription.ts` `renew`：返回类型 `string` → `SubscribeResult`，返回 result 而非 msg。
  3. `src/views/shop/dashboard/index.vue` `confirmRenew`：改回调 `subscriptionStore.renew(sub.id, period)` 拿 `renewResult` → 免费(status=active)直接成功；付费走 `pay(renewResult.subscriptionId, 'wechat', envVersion)` → 弹码 + `startPolling`。与 confirmSubscribe 完全对称，仅第一步 subscribe→renew、传 sub.id 而非 productId。
- **后端接口约定（待实现）**：`POST /api/app/subscription/renew/{id}?period=month|year`
  - 入参：id=原订阅ID，period=month/year
  - 逻辑：校验原订阅归属 → 创建 pending 续费记录（startTime=原expireTime或now，expireTime=新到期）→ 返回 `{ subscriptionId, subscriptionNo, status, message, payPrice?, orderNo? }`（同 SubscribeResult）
  - 前端拿到后调 `pay/{subscriptionId}` 生成微信小程序码，与订阅流程一致
- 续费按钮触发条件不变：`showRenewButton`（daysToExpire<=7，即将到期或已过期）。

## 续费最终对齐：后端已有 /renew-pay（路径不匹配导致 404，改前端即可）

- 关键发现：后端 `AppSubscriptionController` 其实**已经实现了续费**，方法是 `renewPay`（line 849-910），路径是 `/api/app/subscription/renew-pay/{id}`（带连字符），不是前端调的 `/renew/{id}`，所以之前 404。
- 后端 renew-pay 逻辑（已实现，无需改动）：
  - 校验订阅存在 + priceType=subscription + 产品存在
  - 计算续费价格（年付按10个月，对齐 subscribe）
  - 计算新到期：`baseTime = max(原expireTime, now)`，`newExpireTime = baseTime + period`（未过期从原到期延后，已过期从现在延后，不丢时长）
  - 原订阅状态改 pending、覆盖 payPrice/period/expireTime
  - method=balance → handleBalancePay（直接扣款激活）；method=wechat → handleWechatPay（返回 miniappQrcode）
- 前端三处改动（对齐后端，从方案1改为方案2"renew一步返码"）：
  1. `api/.../index.ts` `renewSubscription`：路径 `/renew/{id}` → `/renew-pay/{id}`；参数加 `method/envVersion`；返回类型 `SubscribeResult` → `PayResult | WechatNativePayResult`。
  2. `store/.../appSubscription.ts` `renew`：参数加 `method/envVersion`；返回类型 `PayResult | WechatNativePayResult`。
  3. `dashboard/index.vue` `confirmRenew`：`renew(sub.id, period, 'wechat', envVersion)` 直接拿支付结果 → `'paid' in result` 余额成功分支；`'miniappQrcode' in result` 弹码+轮询。不再二次调 pay。
- 教训：前端 404 时应先 grep 后端 Controller 确认接口是否真实存在及准确路径（带连字符的 `/renew-pay` 容易被误写为 `/renew`）。后端项目位于 `/Users/gxwebsoft/JAVA/websopy-java/`，Controller 全路径 `com.gxwebsoft.app.controller.AppSubscriptionController`。

## 续费报错"微信小程序配置不存在或解析失败"根因排查

- 报错：`POST /renew-pay/90` 返回"生成小程序码失败: 微信小程序配置不存在或解析失败 — 请检查 app_setting 表中 key='platform_miniprogram' 的记录"。用户说"之前订阅还可以"。
- 根因机制（`WxMiniprogramUtil.getAccessToken`）：
  - access_token 有 Redis 缓存，key=`WX_ACCESS_TOKEN:5`（DEFAULT_TENANT_ID=5），TTL=7000秒（约2小时）。
  - **缓存命中时直接返回 token，不读 app_setting 配置**；缓存未命中才调 `getMiniprogramSetting()` 读 `app_setting` 表 key='platform_miniprogram'。
  - 所以"之前订阅可以"= access_token 缓存命中掩盖了配置读取问题；现在续费失败=缓存过期，走到读配置那步发现读不到。
  - 这**不是续费特有 bug**，现在去测订阅（subscribe+pay）也会同样失败。
- `AppSettingServiceImpl.getByKey` 只按 setting_key 查、不按租户过滤（注释"平台配置不按租户隔离"），排除租户隔离问题。
- 排查方向（按可能性）：
  1. app_setting 表 platform_miniprogram 记录被误删/setting_value 被清空。
  2. setting_value JSON 格式损坏（多/少逗号引号），`JSON.parseObject` 抛异常 → catch return null。
  3. 表里有多条 setting_key='platform_miniprogram' 记录，MyBatis-Plus getOne 抛 TooManyResultsException → catch return null。
- 后端 handleWechatPay 开头会调 `diagnoseConfig()` 打印详细日志（app_setting 是否存在、setting_value 原始内容、解析出的 appId/appSecret），看后端日志最快定位。
- 修复：配置丢了去小程序配置页重新保存（batchSave 会写入）；多条记录则清理只留一条。
- 关键文件：`/Users/gxwebsoft/JAVA/websopy-java/src/main/java/com/gxwebsoft/common/core/utils/WxMiniprogramUtil.java`（getAccessToken line 182-276，getMiniprogramSetting line 291-316）。

## 后端 renew-pay 隐患修复（放弃支付不中断服务 + 续费不丢时长）

- 隐患：原 renewPay 把原订阅 status 改 pending + 提前改 expireTime，用户放弃支付会服务中断、到期时间错乱；且 handleBalancePay/mpConfirm 激活时 expireTime=now+period 覆盖了续费应得的"原到期+period"，未过期续费用户损失剩余时长。
- 后端改动 `AppSubscriptionController.java`（4 处）：
  1. `renewPay`：删 `setStatus("pending")` 和 `setExpireTime(newExpireTime)`；只设 payStatus=0 + payPrice/originalPrice/period。原订阅 status 保持 active（或 expired），expireTime 不变 → 放弃支付零影响。newExpireTime 仍计算，仅用于日志"预计到期(支付后)"。
  2. `handleBalancePay` 激活：`sub.setStartTime(now)` → `if (startTime==null) setStartTime(now)`（续费保留首次生效时间）；expireTime 由 `now+period` → `max(原expireTime, now)+period`。
  3. `mpConfirm` 拦截：`if (status=="active")` → `if (status=="active" && payStatus==1)`，放行续费的 active+payStatus=0。
  4. `mpConfirm` 激活：同 handleBalancePay，startTime 仅 null 时设、expireTime 用 max(原expireTime, now)+period。
- max 逻辑对新订阅和续费都正确：新订阅 expireTime 为 null → expireBase=now → now+period；续费未过期 → 原+period（不丢时长）；续费已过期 → now+period。
- 放弃支付现状：订阅保持 active+payStatus=0，服务不中断；用户可再次点"立即续费"重新 renew-pay（会覆盖 payPrice/period 并重新生成码）。无清理 pending 续费订单的需求（不再产生 pending 记录）。
- 前端无需改动（renew-pay 调用不变）。

## 续费"配置不存在"真正根因：app_setting 未加入租户忽略列表（已修复）

- 纠正之前猜测：app_setting 表 platform_miniprogram 配置**完全正常**（pymysql 直查 db_websopy 确认：setting_id=12, tenant_id=5, 169字节, JSON 合法, appId=wx541db955e7a62709, appSecret 齐全）。
- 真正根因：`MybatisPlusConfig.java` 的 `TenantLineHandler.ignoreTable` 列表里有 `app_subscription` 但**没有 `app_setting`**。
  - app_setting 查询受租户拦截器隔离 → `getByKey('platform_miniprogram')` 被自动加 `AND tenant_id=当前租户`。
  - 配置记录 tenant_id=5；续费用户租户 ≠ 5（或请求头/域名取不到租户）→ 查询过滤掉这条记录 → 返回 null → 报"配置不存在或解析失败"。
  - 订阅 pay 能用 = access_token 缓存命中（Redis key 固定 `WX_ACCESS_TOKEN:5` 不分租户，TTL 7000秒）没走 app_setting 查询；续费报错 = 缓存过期走了查询被拦截。与"之前还可以、现在不行"的时间差完全吻合。
  - `AppSettingServiceImpl` 代码注释明说"平台配置不按租户隔离"、getByKey 不带租户条件，但拦截器在 SQL 层强制隔离，两者矛盾——这是历史遗留 bug。
- 修复（后端 `MybatisPlusConfig.java`）：ignoreTable 列表在 `"app_subscription"` 后加 `"app_setting"`（一行改动）。改完 app_setting 所有查询忽略租户，与代码设计意图一致。
- 排查方法值得记录：pymysql 直连远程库（47.119.165.234:13308 db_websopy / redis 16379 db0）验证数据；mysql 客户端未装时用 managed python venv 装 pymysql 查。dev profile 连此库。
- 前端订阅 API 域名硬编码 `https://websopy-api.websoft.top/api/app/subscription`（api/index.ts WEBSOPY_API_BASE），主站 API 走 shop-api.websoft.top。用户对比的 `websoft.top/api/_app/...` 是另一个入口（疑 nginx 转发 _app→app）。

## 续费"弹二维码自动完成支付"修复：清除旧 Redis 支付标记

- 现象：续费 renew-pay 弹出小程序码后几秒，前端轮询 checkStatus 返回 paid=true，自动判定支付成功（用户没扫码）。
- 根因（pymysql+redis 直查确认）：续费复用原订阅记录的 subscriptionNo，原订阅支付时 mpConfirm/handleBalancePay 写入的 Redis 标记 `wxpay:paid:{subscriptionNo}=1`（TTL 30分钟）未清除。checkStatus 逻辑 `paidViaRedis || paidViaDb`，pay_status 维度（renewPay 设 0）已返回 false，但 Redis 维度残留 → 误判。
  - 数据证据：id=94 status=active pay_status=0 expire_time=2027-06-15（订阅支付成功→renewPay 改 pay_status=0）；Redis wxpay:paid:SUB202607152237396107=1 残留。
- 修复（方案A，用户确认）：`AppSubscriptionController.renewPay` 在 updateById 后加 `redisUtil.delete("wxpay:paid:" + sub.getSubscriptionNo())`（RedisUtil.delete 在 line 214）。配合 pay_status=0，checkStatus 两个维度都 false → paid=false。
- 次要问题（未解决）：续费把原订阅 pay_status 1→0，若放弃支付，原订阅变 active+pay_status=0（语义"已激活但未支付"），不影响功能。彻底解决需方案C（renewPay 新建独立订阅记录，原订阅不动），暂不做。
- 排查手段：managed python venv 装 pymysql+redis 直连远程库（47.119.165.234:13308/16379）查 app_subscription 表 + Redis scan wxpay:paid:*，比看日志更直接。

## cmsAd 编辑保存不再拼 OSS 压缩后缀

- 需求：广告编辑保存时图片 URL 不再拼接 `?x-oss-process=image/resize,m_fixed,w_2000/quality,Q_90` 后缀，因为图片压缩已统一在小程序端处理。
- 改动 `src/views/cms/cmsAd/components/cmsAdEdit.vue` 的 `chooseFile` 函数：
  - `images.value.push` 的 `url` 由 `data.downloadUrl + '?x-oss-process=...'` 改为 `data.downloadUrl`。
  - `form.images` 同步去掉后缀，直接用 `data.downloadUrl`。
- 注意：本次只改广告（cmsAd）；`cmsArticle/articleEdit.vue`、`cmsModel/cmsModelEdit.vue` 同样有此后缀，用户未提及，暂不动。
