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

31 KiB
Raw Permalink Blame History

2026-07-15

shopOrder 选项卡默认改为全部

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

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

  • 根因:activeKey 默认为 'undelivered',但 datasource 函数只设了 where.type = 0,没有设 statusFilterstatusFilter 只在用户点击 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.vuesrc/views/shop/shopOrder/index.vue

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

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

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

  • 引入 useTenantStoreloadData 中调用 tenantStore.fetchTenantInfo() 替代直接调用 getTenantInfo() API。
  • 基本信息"创建时间"改为读取 tenantStore.company?.createTime
  • 运行天数也改为从 tenantStore.company?.createTime 计算,移除独立的 tenantCreateTime ref。
  • 文件:src/views/shop/dashboard/index.vue
  • 通过 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.tsAppSubscription 实体及参数/返回类型,对照后端 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=0success(IPage) 转成 PageResult{list,count},与项目 @/api 的 PageResult 结构一致。
  • 新增 src/store/modules/appSubscription.tsuseAppSubscriptionStorePinia Options API 风格,对齐 site.ts/statistics.ts聚合列表/详情/支付状态/订阅管理方法。列表/详情缓存5分钟checkStatus/checkPurchased 实时请求;订阅/支付/管理类操作成功后调 invalidateCache() 失效缓存。

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

  • 背景:原"确认线下收款"用 Modal.confirm + createVNode 动态拼一个 input只有备注没有上传凭证。用户需要加凭证上传。
  • 字段决策:新增 paymentVoucher?: string 字段(不复用 comments/merchantRemarks因为凭证是图片URL非文本。顺带修复 buyerRemarks/merchantRemarks 类型 bugundefinedstring?)。
  • 重构为独立组件 src/views/shop/shopOrder/components/OfflinePaymentModal.vue
    • <a-modal> + <a-form> + <a-upload list-type="picture-card"> 单张图片上传。
    • 上传走 uploadOsssrc/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.tssrc/api/shop/shopOrder/index.tssrc/views/shop/shopOrder/components/OfflinePaymentModal.vue(新增)、src/views/shop/shopOrder/index.vue
  • 后端待办(已完成):后端项目 /Users/gxwebsoft/JAVA/guilixu-javaMyBatis-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.sqlALTER 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}=1Web 端 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.generatePayQrcodewebsopy-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/100yearPrice = 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=pendingpay(subscriptionId,'wechat') 生成小程序码 → Modal 展示 + 轮询 checkStatus
  • 移除 confirmSubscribe 里的 isFreeProduct 分支(改用 subResult.status 判断);isFreeProduct 仍用于订阅 Modal 的免费提示。
  • vue-tsc 验证 dashboard 零 error TS总错误 1778 不变)。
  • 后端 generatePayQrcodesetPriceType 修复仍保留(重启后端后该接口也能用),但前端已不依赖它。

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

  • 需求dashboard 立即订阅请求的 productId 从动态(按 tenantIdapp_product)改为写死 65
  • 改动 src/views/shop/dashboard/index.vue
    • importspageProductsgetProductDetail
    • 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/100yearPrice = 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)"
  • shopGoodsshopOrderdashboard 等页面保持一致:默认宽度 240px、质量 90、自动补全 OSS 域名、跳过已含 x-oss-process 的 URL。
  • 预览行为未改(仍走 a-image 默认 src 预览)。

电脑版后台新订单红点提示(全局)

  • 需求:电脑版后台有新订单时显示红点,让管理员知道有新订单。
  • 关键发现:系统已有菜单红点基础设施(menu-title.vueitem.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.vuebadge==='dot' 时用 <a-badge dot /> 纯红点,否则原数字徽章逻辑。
  • src/layout/index.vuesetup 调 useOrderNotify().start() 全局启动,后台任意页面生效。
  • src/views/shop/shopOrder/index.vueonMounted + onActivatedclearBadge()(兼容 keep-alive cache-key,用 orderBadgeMounted 标志防首次双触发)。
  • src/views/shop/dashboard/index.vuequickLinks 改 computed订单管理项 badge: hasNewOrder.valuequick-icon 加红点 span + pulse 动画样式。
  • 决策(用户确认):红点=新订单未查看(进订单页清除);位置=订单管理菜单+dashboard快速入口订单管理图标声音保留并全局化。
  • 构建验证:vite build --outDir dist_verify 通过EXIT:0。原 pnpm build 失败仅因 vite 清空 dist/assets 被安全删除保护拦截83文件>50阈值与代码无关。

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

  • 背景:之前 OfflinePaymentModal 确认线下收款时已支持上传 paymentVoucherOSS 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 })(按用户要求宽度 750preview-src-listensureFullUrl(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天」。
    • 新增续费按钮:showRenewButtondaysToExpire <= 7 即即将到期或已过期时显示),点击 onRenew
    • 复用订阅 Modal新增 subscribeMode'subscribe'|'renew'+ renewing 状态Modal 的 title/okText/confirm-loading/@ok 按 mode 动态切换;免费应用 alert 文案也按 mode 区分。
    • confirmRenewsubscriptionStore.renew(sub.id, selectedPeriod)POST /app/subscription/renew/{id}?period=month|year成功后 refreshSubscription()
    • onSubscribe 补设 subscribeMode='subscribe'
  • 续费 API 语义:renewSubscription 返回 string同步完成疑似余额扣款/免费续期)。若后端续费需走微信扫码支付,需改为返回支付二维码结构并接 pay 流程——待后端确认。
  • 关键字段再确认(用户):开始时间用 appSubscription.startTime,到期时间用 appSubscription.expireTimedaysToExpire 与 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.vueconfirmRenew
    • 删掉 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.tsrenewSubscription 函数可保留(防止后端后续补上接口),但 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:返回类型 stringSubscribeResult;取 res.data.data(原取 res.data.message)。
    2. src/store/modules/appSubscription.ts renew:返回类型 stringSubscribeResult,返回 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} 生成微信小程序码,与订阅流程一致
  • 续费按钮触发条件不变:showRenewButtondaysToExpire<=7即将到期或已过期

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

  • 关键发现:后端 AppSubscriptionController 其实已经实现了续费,方法是 renewPayline 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;返回类型 SubscribeResultPayResult | WechatNativePayResult
    2. store/.../appSubscription.ts renew:参数加 method/envVersion;返回类型 PayResult | WechatNativePayResult
    3. dashboard/index.vue confirmRenewrenew(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:5DEFAULT_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.javagetAccessToken line 182-276getMiniprogramSetting line 291-316

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

  • 隐患:原 renewPay 把原订阅 status 改 pending + 提前改 expireTime用户放弃支付会服务中断、到期时间错乱且 handleBalancePay/mpConfirm 激活时 expireTime=now+period 覆盖了续费应得的"原到期+period",未过期续费用户损失剩余时长。
  • 后端改动 AppSubscriptionController.java4 处):
    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+periodmax(原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.javaTenantLineHandler.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.javaignoreTable 列表在 "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/subscriptionapi/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}=1TTL 30分钟未清除。checkStatus 逻辑 paidViaRedis || paidViaDbpay_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.vuechooseFile 函数:
    • images.value.pushurldata.downloadUrl + '?x-oss-process=...' 改为 data.downloadUrl
    • form.images 同步去掉后缀,直接用 data.downloadUrl
  • 注意本次只改广告cmsAdcmsArticle/articleEdit.vuecmsModel/cmsModelEdit.vue 同样有此后缀,用户未提及,暂不动。