文案配置化 + 支付开关

小程序
- 页面固定文案全部改读后台配置(购买、登录、资料、首页、分享、介绍页),代码里不再硬编码
- 已拥有课程时在当前页提示,不再跳购买页再提示
- 介绍页正文由代码常量改为读 settings.page.formula.content

服务端
- 课程相关报错文案(不存在 / 已下架 / 课时不存在 / 未购买 / 已过期 / 重复购买 / 未配置支付商品)改读配置,代码保留默认值兜底
- 新增支付总开关 pay.blocked:打开即拦截下单并下发配置里的提示;开关存在数据库里,开发库与生产库各一份(开发库已开,生产库未设)

后台
- 系统配置页按页面分组折叠,默认收起;认不出来的配置归入「其他」

配套数据(不在代码里):对应的 settings 行已写入生产库与开发库
This commit is contained in:
Yeomai committed 2026-10-01 11:28:29 +08:00
1 parent c45025fc86
commit 879e74ebda
21 files changed
+325 -103

No files matched your search

@@ -1,6 +1,7 @@
import { randomInt } from 'node:crypto';
import { Injectable } from '@nestjs/common';
import { AppError, ErrorCode } from '../../common/errors';
import { COURSE_TIP_KEYS, readFlag, readText, SITE_FLAG_KEYS } from '../../common/site-flags';
import { PrismaService } from '../../prisma/prisma.service';
import { OrderStatus, PaymentsService } from '../payments/payments.service';
import { WxPayService } from '../payments/wxpay.service';
@@ -34,9 +35,21 @@ export class OrdersService {
quantity = 1,
terminal: string | null = null,
): Promise<{ outTradeNo: string; payData: PayData }> {
// 支付配置跟着「小程序当前数据源」走:切到开发库就用开发那套(默认沙箱),不会误收真钱
// 支付配置跟着「小程序当前数据源」走:生产库用现网那套
const source = this.prisma.activeEnv;
const pay = this.wxpay.configFor(source);
// 支付总开关:打开就拦下单(临时停售、维护、或开发库防止误下单)。
// 开关存在数据库里,小程序读哪个库就受哪个库的开关控制,所以开发库和生产库可以各设各的。
// 放在「支付是否配置」之前判断,保证暂停期间永远是同一句提示。
if (await readFlag(this.prisma, SITE_FLAG_KEYS.payBlocked)) {
throw new AppError(
ErrorCode.PAY_NOT_ENABLED,
await readText(this.prisma, COURSE_TIP_KEYS.payUnavailable, '支付暂不可用'),
503,
);
}
if (!this.wxpay.enabledFor(source)) {
throw new AppError(ErrorCode.PAY_NOT_ENABLED, '支付尚未开通,请联系管理员', 503);
}
@@ -55,17 +68,26 @@ export class OrdersService {
}
const course = await this.prisma.active.course.findUnique({ where: { code: courseCode } });
if (!course || course.status !== 1) throw AppError.notFound('课程不存在或已下架');
if (!course || course.status !== 1) {
// 与课程详情用同一条配置,两处提示保持一致
throw AppError.notFound(await readText(this.prisma, COURSE_TIP_KEYS.gone, '课程不存在或已下架'));
}
const owned = await this.prisma.active.entitlement.findUnique({
where: { user_id_course_id: { user_id: userId, course_id: course.id } },
});
if (owned) throw AppError.conflict('你已拥有该课程');
if (owned) {
throw AppError.conflict(await readText(this.prisma, COURSE_TIP_KEYS.alreadyOwned, '你已拥有该课程'));
}
const product = await this.prisma.active.product.findFirst({
where: { course_id: course.id, status: 1 },
});
if (!product) throw AppError.notFound('该课程尚未配置支付商品');
if (!product) {
throw AppError.notFound(
await readText(this.prisma, COURSE_TIP_KEYS.noProduct, '该课程尚未配置支付商品'),
);
}
const outTradeNo = this.newOutTradeNo();
const amountFen = product.price_fen * quantity;