Files
guilixu-admin/.workbuddy/memory/2026-07-15.md
赵忠林 6fcc120e91 fix(subscription): 修复续费配置查询被租户隔离导致问题
- 修正 `MybatisPlusConfig.java` 中租户拦截器忽略表列表,新增 `"app_setting"` 以防止租户隔离导致配置查询失败
- 解决续费时 `platform_miniprogram` 配置查询因租户过滤返回空的问题,从而避免报“配置不存在或解析失败”
- 复核 `app_setting` 表查询应无租户隔离,与代码注释设计意图保持一致
- 记录排查方法,使用 pymysql 连接远程数据库验证数据状态
- 纠正广告编辑保存逻辑,取消广告图片 URL 拼接 OSS 压缩处理后缀
- 修改 cmsAd 组件 `chooseFile` 函数,图片 URL 直接使用原始链接,不再添加图片压缩参数
- 保持其他 cms 组件未改动,根据用户需求仅调整广告相关逻辑
2026-07-15 22:51:47 +08:00

280 lines
31 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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`
## 新增 useAppSubscriptionStorewebsopy 应用订阅特殊接口)
- 新增 `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-pathbaseURL=`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/LiquibaseDDL 手动管理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` computedpublishStatus → {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=原订阅IDperiod=month/year
- 逻辑:校验原订阅归属 → 创建 pending 续费记录startTime=原expireTime或nowexpireTime=新到期)→ 返回 `{ 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=5TTL=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-276getMiniprogramSetting 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或 expiredexpireTime 不变 → 放弃支付零影响。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` 激活:同 handleBalancePaystartTime 仅 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=0Redis wxpay:paid:SUB202607152237396107=1 残留。
- 修复方案A用户确认`AppSubscriptionController.renewPay` 在 updateById 后加 `redisUtil.delete("wxpay:paid:" + sub.getSubscriptionNo())`RedisUtil.delete 在 line 214。配合 pay_status=0checkStatus 两个维度都 false → paid=false。
- 次要问题(未解决):续费把原订阅 pay_status 1→0若放弃支付原订阅变 active+pay_status=0语义"已激活但未支付"不影响功能。彻底解决需方案CrenewPay 新建独立订阅记录,原订阅不动),暂不做。
- 排查手段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` 同样有此后缀,用户未提及,暂不动。