- 修正续费请求路径为后端实际的 /renew-pay/{id},避免 404 错误
- 续费接口新增 method 和 envVersion 参数支持余额与微信支付
- 续费请求返回支付结果,微信支付返回小程序码,余额支付直接成功
- 续费不再分步调起支付,前端确认续费逻辑简化为一步调用后端接口
- 后端 renewPay 优化,避免放弃支付导致服务中断及时长丢失
- 后端续费激活逻辑调整,续费保留原订阅有效期累计时长
- 排查并解决微信小程序支付配置缺失导致续费失败的问题
- 代码及注释同步更新,完善续费流程整体一致性与稳定性
376 lines
10 KiB
TypeScript
376 lines
10 KiB
TypeScript
/**
|
||
* 应用订阅 store
|
||
* 数据来源:websopy 特殊接口(db_websopy.app_subscription 表)
|
||
* 接口域名:https://websopy-api.websoft.top/api
|
||
* 后端:com.gxwebsoft.app.controller.AppSubscriptionController
|
||
*/
|
||
import { defineStore } from 'pinia';
|
||
import {
|
||
pageMySubscriptions,
|
||
getSubscriptionDetail,
|
||
getSubscriptionDetailByNo,
|
||
checkSubscriptionStatus,
|
||
checkPurchased,
|
||
getBalance,
|
||
subscribe,
|
||
generatePayQrcode,
|
||
paySubscription,
|
||
mpPrepay,
|
||
mpConfirm,
|
||
renewSubscription,
|
||
cancelSubscription,
|
||
toggleEnable
|
||
} from '@/api/app/appSubscription';
|
||
import type {
|
||
AppSubscription,
|
||
AppSubscriptionQueryParam,
|
||
SubscribeParam,
|
||
SubscribeResult,
|
||
GeneratePayQrcodeParam,
|
||
GeneratePayQrcodeResult,
|
||
CheckStatusResult,
|
||
PayResult,
|
||
WechatNativePayResult,
|
||
MpPrepayResult
|
||
} from '@/api/app/appSubscription/model';
|
||
import type { PageResult } from '@/api';
|
||
|
||
export interface AppSubscriptionState {
|
||
// 我的订阅列表
|
||
subscriptionList: AppSubscription[];
|
||
// 总数量
|
||
total: number;
|
||
// 当前查看的订阅详情
|
||
currentSubscription: AppSubscription | null;
|
||
// 用户余额
|
||
balance: number;
|
||
// 加载状态
|
||
loading: boolean;
|
||
// 最后更新时间
|
||
lastUpdateTime: number | null;
|
||
// 缓存有效期(毫秒)
|
||
cacheExpiry: number;
|
||
}
|
||
|
||
export const useAppSubscriptionStore = defineStore('appSubscription', {
|
||
state: (): AppSubscriptionState => ({
|
||
subscriptionList: [],
|
||
total: 0,
|
||
currentSubscription: null,
|
||
balance: 0,
|
||
loading: false,
|
||
lastUpdateTime: null,
|
||
// 默认缓存5分钟(订阅涉及支付状态,不宜过长)
|
||
cacheExpiry: 5 * 60 * 1000
|
||
}),
|
||
|
||
getters: {
|
||
/**
|
||
* 已激活订阅
|
||
*/
|
||
activeSubscriptions: (state): AppSubscription[] => {
|
||
return state.subscriptionList.filter((s) => s.status === 'active');
|
||
},
|
||
|
||
/**
|
||
* 待支付订阅
|
||
*/
|
||
pendingSubscriptions: (state): AppSubscription[] => {
|
||
return state.subscriptionList.filter((s) => s.status === 'pending');
|
||
},
|
||
|
||
/**
|
||
* 已过期订阅
|
||
*/
|
||
expiredSubscriptions: (state): AppSubscription[] => {
|
||
return state.subscriptionList.filter((s) => s.status === 'expired');
|
||
},
|
||
|
||
/**
|
||
* 检查缓存是否有效
|
||
*/
|
||
isCacheValid: (state): boolean => {
|
||
if (!state.lastUpdateTime) return false;
|
||
const now = Date.now();
|
||
return now - state.lastUpdateTime < state.cacheExpiry;
|
||
}
|
||
},
|
||
|
||
actions: {
|
||
// ============================================================
|
||
// 查询类
|
||
// ============================================================
|
||
|
||
/**
|
||
* 获取我的订阅列表(分页,带缓存)
|
||
* @param params 查询参数(page/limit/status)
|
||
* @param forceRefresh 是否强制刷新(切换 status 过滤时建议传 true)
|
||
*/
|
||
async fetchMySubscriptions(
|
||
params: AppSubscriptionQueryParam = { page: 1, limit: 10 },
|
||
forceRefresh = false
|
||
): Promise<PageResult<AppSubscription>> {
|
||
// 缓存有效且不强制刷新,直接返回缓存列表
|
||
if (!forceRefresh && this.isCacheValid && this.subscriptionList.length > 0) {
|
||
return { list: this.subscriptionList, count: this.total };
|
||
}
|
||
|
||
this.loading = true;
|
||
try {
|
||
const data = await pageMySubscriptions(params);
|
||
this.subscriptionList = data.list || [];
|
||
this.total = data.count || 0;
|
||
this.lastUpdateTime = Date.now();
|
||
return data;
|
||
} catch (error) {
|
||
console.error('获取我的订阅列表失败:', error);
|
||
throw error;
|
||
} finally {
|
||
this.loading = false;
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 获取订阅详情(按ID)
|
||
*/
|
||
async fetchDetail(id: number): Promise<AppSubscription> {
|
||
try {
|
||
const data = await getSubscriptionDetail(id);
|
||
this.currentSubscription = data;
|
||
return data;
|
||
} catch (error) {
|
||
console.error('获取订阅详情失败:', error);
|
||
throw error;
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 根据订阅编号查询详情(小程序入口用)
|
||
*/
|
||
async fetchDetailByNo(subscriptionNo: string): Promise<AppSubscription> {
|
||
try {
|
||
const data = await getSubscriptionDetailByNo(subscriptionNo);
|
||
this.currentSubscription = data;
|
||
return data;
|
||
} catch (error) {
|
||
console.error('根据订阅编号查询详情失败:', error);
|
||
throw error;
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 查询支付状态(前端轮询用,强制实时请求,不走缓存)
|
||
*/
|
||
async checkStatus(subscriptionNo: string): Promise<CheckStatusResult> {
|
||
return await checkSubscriptionStatus(subscriptionNo);
|
||
},
|
||
|
||
/**
|
||
* 检查是否已购买某应用(强制实时请求,不走缓存)
|
||
*/
|
||
async checkPurchased(productId: number): Promise<boolean> {
|
||
return await checkPurchased(productId);
|
||
},
|
||
|
||
/**
|
||
* 获取当前用户余额
|
||
*/
|
||
async fetchBalance(): Promise<number> {
|
||
try {
|
||
const data = await getBalance();
|
||
this.balance = data.balance;
|
||
return data.balance;
|
||
} catch (error) {
|
||
console.error('获取用户余额失败:', error);
|
||
throw error;
|
||
}
|
||
},
|
||
|
||
// ============================================================
|
||
// 订阅与支付操作
|
||
// ============================================================
|
||
|
||
/**
|
||
* 创建订阅
|
||
* 免费应用直接激活,付费应用创建待支付记录
|
||
*/
|
||
async subscribe(data: SubscribeParam): Promise<SubscribeResult> {
|
||
try {
|
||
const result = await subscribe(data);
|
||
// 订阅状态变化,失效缓存
|
||
this.invalidateCache();
|
||
return result;
|
||
} catch (error) {
|
||
console.error('创建订阅失败:', error);
|
||
throw error;
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 生成支付小程序码(同时创建订阅记录)
|
||
*/
|
||
async generatePayQrcode(
|
||
data: GeneratePayQrcodeParam
|
||
): Promise<GeneratePayQrcodeResult> {
|
||
try {
|
||
const result = await generatePayQrcode(data);
|
||
// 已创建订阅记录,失效缓存
|
||
this.invalidateCache();
|
||
return result;
|
||
} catch (error) {
|
||
console.error('生成支付小程序码失败:', error);
|
||
throw error;
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 发起支付
|
||
* @param id 订阅ID
|
||
* @param method 支付方式:balance-余额支付 / wechat-微信支付
|
||
* @param envVersion 小程序版本(微信支付时使用):develop/trial/release
|
||
*/
|
||
async pay(
|
||
id: number,
|
||
method: 'balance' | 'wechat' = 'wechat',
|
||
envVersion?: string
|
||
): Promise<PayResult | WechatNativePayResult> {
|
||
try {
|
||
const result = await paySubscription(id, method, envVersion);
|
||
// 支付成功后余额/订阅状态变化,失效缓存并刷新余额
|
||
this.invalidateCache();
|
||
if (method === 'balance') {
|
||
const payResult = result as PayResult;
|
||
if (typeof payResult.balance === 'number') {
|
||
this.balance = payResult.balance;
|
||
}
|
||
}
|
||
return result;
|
||
} catch (error) {
|
||
console.error('发起支付失败:', error);
|
||
throw error;
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 小程序 JSAPI 预支付下单
|
||
*/
|
||
async mpPrepay(id: number, openid: string): Promise<MpPrepayResult> {
|
||
try {
|
||
return await mpPrepay(id, { openid });
|
||
} catch (error) {
|
||
console.error('小程序预支付失败:', error);
|
||
throw error;
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 小程序支付成功确认
|
||
*/
|
||
async mpConfirm(
|
||
subscriptionNo: string,
|
||
transactionId?: string
|
||
): Promise<PayResult> {
|
||
try {
|
||
const result = await mpConfirm(subscriptionNo, { transactionId });
|
||
// 支付确认后订阅状态变化,失效缓存
|
||
this.invalidateCache();
|
||
return result;
|
||
} catch (error) {
|
||
console.error('小程序支付确认失败:', error);
|
||
throw error;
|
||
}
|
||
},
|
||
|
||
// ============================================================
|
||
// 订阅管理
|
||
// ============================================================
|
||
|
||
/**
|
||
* 续费:基于已有订阅创建续费订单并生成支付码(一步完成,对齐后端 /renew-pay)
|
||
* @param id 订阅ID
|
||
* @param period 周期:month/year
|
||
* @param method 支付方式:balance/wechat
|
||
* @param envVersion 小程序版本(微信支付时使用)
|
||
*/
|
||
async renew(
|
||
id: number,
|
||
period: 'month' | 'year' = 'month',
|
||
method: 'balance' | 'wechat' = 'wechat',
|
||
envVersion?: string
|
||
): Promise<PayResult | WechatNativePayResult> {
|
||
try {
|
||
const result = await renewSubscription(id, period, method, envVersion);
|
||
this.invalidateCache();
|
||
return result;
|
||
} catch (error) {
|
||
console.error('续费失败:', error);
|
||
throw error;
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 退订/取消
|
||
*/
|
||
async cancel(id: number): Promise<string> {
|
||
try {
|
||
const msg = await cancelSubscription(id);
|
||
this.invalidateCache();
|
||
return msg;
|
||
} catch (error) {
|
||
console.error('退订失败:', error);
|
||
throw error;
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 启用/禁用
|
||
*/
|
||
async toggleEnable(id: number, enabled: boolean): Promise<string> {
|
||
try {
|
||
const msg = await toggleEnable(id, enabled);
|
||
this.invalidateCache();
|
||
return msg;
|
||
} catch (error) {
|
||
console.error('启用/禁用失败:', error);
|
||
throw error;
|
||
}
|
||
},
|
||
|
||
// ============================================================
|
||
// 缓存控制
|
||
// ============================================================
|
||
|
||
/**
|
||
* 失效缓存(仅重置时间标记,保留已有数据,下次拉取时更新)
|
||
* 用于订阅/支付/管理操作后,确保下次查询获取最新数据
|
||
*/
|
||
invalidateCache() {
|
||
this.lastUpdateTime = null;
|
||
},
|
||
|
||
/**
|
||
* 清除缓存(清空数据并重置时间标记)
|
||
*/
|
||
clearCache() {
|
||
this.subscriptionList = [];
|
||
this.total = 0;
|
||
this.currentSubscription = null;
|
||
this.lastUpdateTime = null;
|
||
},
|
||
|
||
/**
|
||
* 强制刷新订阅列表
|
||
*/
|
||
async refresh() {
|
||
return await this.fetchMySubscriptions({ page: 1, limit: 10 }, true);
|
||
},
|
||
|
||
/**
|
||
* 设置缓存有效期
|
||
*/
|
||
setCacheExpiry(expiry: number) {
|
||
this.cacheExpiry = expiry;
|
||
}
|
||
}
|
||
});
|