- 修正 `MybatisPlusConfig.java` 中租户拦截器忽略表列表,新增 `"app_setting"` 以防止租户隔离导致配置查询失败 - 解决续费时 `platform_miniprogram` 配置查询因租户过滤返回空的问题,从而避免报“配置不存在或解析失败” - 复核 `app_setting` 表查询应无租户隔离,与代码注释设计意图保持一致 - 记录排查方法,使用 pymysql 连接远程数据库验证数据状态 - 纠正广告编辑保存逻辑,取消广告图片 URL 拼接 OSS 压缩处理后缀 - 修改 cmsAd 组件 `chooseFile` 函数,图片 URL 直接使用原始链接,不再添加图片压缩参数 - 保持其他 cms 组件未改动,根据用户需求仅调整广告相关逻辑
31 KiB
31 KiB
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计算,移除独立的tenantCreateTimeref。 - 文件:
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,后端写 Rediswxpay: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(),而 DBprice_type为 NOT NULL 无默认值。对比subscribe方法有setPriceType。 - 参考
websopy-pc/app/pages/console/pay/[subscriptionNo].vue:它用subscribe创建订阅 +pay(id,'wechat')生成二维码(两步),避开了generatePayQrcode,所以没遇到此 bug。 - 修复(后端
AppSubscriptionController.javageneratePayQrcode 方法):补全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统一使用该常量。
- imports:
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.tssetMenuBadge(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-alivecache-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,对齐ShopOrdermodel。
- 模板:在「支付方式」
ShopOrdermodel 中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:- 删除硬编码
systemInforeactive(已确认 cms 那两个 systemInfo 是独立 const,互不影响)。 - import 移除
reactive。 - 新增
productStatusTextcomputed:publishStatus → {text,color} 映射(published=已发布/green、pending_review=审核中/orange、developing=开发中/blue、rejected=已驳回/red、deprecated=已下架/red),未知状态显示原始值,无 product 显示 '-'。 - 重写
runDayscomputed:数据源由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。 payProductNamefallback 文案改为「应用续费」(与订阅的「应用订阅」区分)。
- 删掉
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 接口即可联调通):
src/api/app/appSubscription/index.tsrenewSubscription:返回类型string→SubscribeResult;取res.data.data(原取res.data.message)。src/store/modules/appSubscription.tsrenew:返回类型string→SubscribeResult,返回 result 而非 msg。src/views/shop/dashboard/index.vueconfirmRenew:改回调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一步返码"):
api/.../index.tsrenewSubscription:路径/renew/{id}→/renew-pay/{id};参数加method/envVersion;返回类型SubscribeResult→PayResult | WechatNativePayResult。store/.../appSubscription.tsrenew:参数加method/envVersion;返回类型PayResult | WechatNativePayResult。dashboard/index.vueconfirmRenew: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)也会同样失败。
- access_token 有 Redis 缓存,key=
AppSettingServiceImpl.getByKey只按 setting_key 查、不按租户过滤(注释"平台配置不按租户隔离"),排除租户隔离问题。- 排查方向(按可能性):
- app_setting 表 platform_miniprogram 记录被误删/setting_value 被清空。
- setting_value JSON 格式损坏(多/少逗号引号),
JSON.parseObject抛异常 → catch return null。 - 表里有多条 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 处):renewPay:删setStatus("pending")和setExpireTime(newExpireTime);只设 payStatus=0 + payPrice/originalPrice/period。原订阅 status 保持 active(或 expired),expireTime 不变 → 放弃支付零影响。newExpireTime 仍计算,仅用于日志"预计到期(支付后)"。handleBalancePay激活:sub.setStartTime(now)→if (startTime==null) setStartTime(now)(续费保留首次生效时间);expireTime 由now+period→max(原expireTime, now)+period。mpConfirm拦截:if (status=="active")→if (status=="active" && payStatus==1),放行续费的 active+payStatus=0。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。
- app_setting 查询受租户拦截器隔离 →
- 修复(后端
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同样有此后缀,用户未提及,暂不动。