feat(appSubscription): 新增应用订阅模块及商城欢迎横幅Logo支持

- 新增 useAppSubscriptionStore,封装 websopy 特殊接口的应用订阅功能
- 实现订阅列表分页查询、详情获取、支付状态查询、订阅管理等接口调用
- 适配不同支付方式,支持余额支付、微信支付及小程序支付预支付流程
- 在商城 Dashboard 欢迎横幅左侧新增商城 Logo 显示,读取商城设置的 shopLogo 字段
- 使用图像压缩接口处理 Logo 大小和质量,优化加载体验
- 优化 Dashboard 待处理事项展示,恢复退款和优惠券使用项
- 统一订阅接口调用地址为独立域名,复用请求工具共享认证和错误处理
This commit is contained in:
2026-07-15 12:49:05 +08:00
parent e2dd8dbc18
commit 9e65007e65
5 changed files with 909 additions and 6 deletions

View File

@@ -0,0 +1,368 @@
/**
* 应用订阅 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;
}
},
// ============================================================
// 订阅管理
// ============================================================
/**
* 续费
* @param id 订阅ID
* @param period 周期month/year
*/
async renew(id: number, period: 'month' | 'year' = 'month'): Promise<string> {
try {
const msg = await renewSubscription(id, period);
this.invalidateCache();
return msg;
} 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;
}
}
});