Files
wechatWeb/api/service/biz_usage_service.js
张成 1f4b39d576 1
2026-03-27 13:30:53 +08:00

158 lines
4.6 KiB
JavaScript
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.
const baseModel = require("../../middleware/baseModel");
const { op } = baseModel;
function currentStatMonth(d = new Date()) {
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, "0");
return `${y}-${m}`;
}
function num(v) {
if (v === null || v === undefined || v === "") return 0;
const n = Number(v);
return Number.isNaN(n) ? 0 : n;
}
/** 0 表示无限制;已用量超过 limit 则超限(允许用到等于 limit */
function quotaExceeded(used, limit) {
const li = num(limit);
if (li <= 0) return false;
return num(used) > li;
}
/**
* 取或创建当月用量行
*/
async function getOrCreateUsage(userId, planId, statMonth) {
const [row] = await baseModel.biz_usage_monthly.findOrCreate({
where: { user_id: userId, stat_month: statMonth },
defaults: {
user_id: userId,
plan_id: planId,
stat_month: statMonth,
msg_count: 0,
mass_count: 0,
friend_count: 0,
sns_count: 0,
active_user_count: 0,
api_call_count: 0,
},
});
if (num(row.plan_id) !== num(planId)) {
await row.update({ plan_id: planId });
}
return row;
}
async function applyDelta(userId, planId, statMonth, delta) {
const row = await getOrCreateUsage(userId, planId, statMonth);
const next = {
msg_count: num(row.msg_count) + num(delta.msg),
mass_count: num(row.mass_count) + num(delta.mass),
friend_count: num(row.friend_count) + num(delta.friend),
sns_count: num(row.sns_count) + num(delta.sns),
active_user_count: num(row.active_user_count) + num(delta.active_user),
};
await row.update(next);
return row.reload();
}
/**
* 校验「增量」后是否超限(与套餐额度对比)
* feature: msg | mass | friend | sns | active_user
*/
function checkQuotaAfterDelta(plan, usageRow, delta) {
const checks = [
["msg", "msg_count", "msg_quota"],
["mass", "mass_count", "mass_quota"],
["friend", "friend_count", "friend_quota"],
["sns", "sns_count", "sns_quota"],
["active_user", "active_user_count", "active_user_limit"],
];
for (const [key, uCol, pCol] of checks) {
const add = num(delta[key]);
if (add <= 0) continue;
const used = num(usageRow[uCol]) + add;
if (quotaExceeded(used, plan[pCol])) {
return { ok: false, error_code: "QUOTA_EXCEEDED", message: `额度不足: ${key}` };
}
}
return { ok: true };
}
/**
* 为当前月所有有效订阅补用量行(月结/初始化)
*/
async function ensureUsageRowsForCurrentMonth() {
const statMonth = currentStatMonth();
const now = new Date();
const subs = await baseModel.biz_subscription.findAll({
where: {
status: "active",
start_time: { [op.lte]: now },
end_time: { [op.gte]: now },
},
});
let n = 0;
for (const s of subs) {
await getOrCreateUsage(s.user_id, s.plan_id, statMonth);
n += 1;
}
return n;
}
/**
* 校验 API 调用次数是否超限
* @param {object} plan - 套餐记录(含 api_call_quota
* @param {object} usageRow - 当月用量记录(含 api_call_count
* @returns {{ ok: boolean, error_code?: string, message?: string }}
*/
function checkApiCallQuota(plan, usageRow) {
const quota = num(plan.api_call_quota);
if (quota <= 0) return { ok: true };
const used = num(usageRow.api_call_count) + 1;
if (used > quota) {
return { ok: false, error_code: "API_CALL_QUOTA_EXCEEDED", message: `当月 API 调用次数已达上限(${quota})` };
}
return { ok: true };
}
/**
* API 调用次数 +1
*/
async function incrementApiCallCount(userId, planId, statMonth) {
const row = await getOrCreateUsage(userId, planId, statMonth);
await row.update({ api_call_count: num(row.api_call_count) + 1 });
return row.reload();
}
/**
* 校验接口路径是否在套餐允许范围内
* @param {object} plan - 套餐记录(含 allowed_apis
* @param {string} apiPath - 当前请求的接口路径,如 /user/GetProfile
* @returns {{ ok: boolean, error_code?: string, message?: string }}
*/
function checkApiPathAllowed(plan, apiPath) {
const allowed = plan.allowed_apis;
if (allowed == null) return { ok: true };
let list = allowed;
if (typeof list === "string") {
try { list = JSON.parse(list); } catch { return { ok: true }; }
}
if (!Array.isArray(list) || list.length === 0) return { ok: true };
if (list.includes(apiPath)) return { ok: true };
return { ok: false, error_code: "API_NOT_ALLOWED", message: `当前套餐不支持该接口: ${apiPath}` };
}
module.exports = {
currentStatMonth,
getOrCreateUsage,
applyDelta,
checkQuotaAfterDelta,
ensureUsageRowsForCurrentMonth,
checkApiCallQuota,
incrementApiCallCount,
checkApiPathAllowed,
num,
};