feat: 企业计费服务与超管/管理端设置及定价规则文档
1、修复了财务、Analyze/Test 等与定价、账务相关的接口行为。 2、新增 EnterpriseBillingService 与定价企业账务规则说明文档。 3、优化超管 Settings、PricingConfig 及小程序 app 入口逻辑。 Made-with: Cursor
This commit is contained in:
@@ -602,6 +602,7 @@ const handleSave = async (section: string) => {
|
||||
maintenanceMode: !!systemConfig.maintenanceMode
|
||||
})
|
||||
if (response.code === 200) {
|
||||
await loadSettings()
|
||||
ElMessage.success(systemConfig.maintenanceMode ? '审核模式已开启,小程序将隐藏AI功能' : '审核模式已关闭,AI功能已恢复')
|
||||
saveSuccess.value = section
|
||||
setTimeout(() => { saveSuccess.value = null }, 3000)
|
||||
|
||||
125
api/app/common/service/EnterpriseBillingService.php
Normal file
125
api/app/common/service/EnterpriseBillingService.php
Normal file
@@ -0,0 +1,125 @@
|
||||
<?php
|
||||
namespace app\common\service;
|
||||
|
||||
use app\model\PricingConfig as PricingConfigModel;
|
||||
use think\facade\Db;
|
||||
|
||||
/**
|
||||
* 企业平台费账务服务
|
||||
*
|
||||
* 口径:
|
||||
* - 仅当 test_results 归属企业时才扣费
|
||||
* - 扣费金额读取超管全局 enterprise 定价
|
||||
* - 每条 test_results 仅扣一次,使用 finance_records 描述做幂等标记
|
||||
*/
|
||||
class EnterpriseBillingService
|
||||
{
|
||||
/**
|
||||
* 测评完成后,按超管全局企业版单价扣企业平台费。
|
||||
*/
|
||||
public static function chargePlatformFeeForTestResult(int $testResultId, string $testType, ?int $enterpriseId): void
|
||||
{
|
||||
$enterpriseId = (int) ($enterpriseId ?? 0);
|
||||
if ($testResultId <= 0 || $enterpriseId <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$amountFen = self::getPlatformFeeFen($testType);
|
||||
if ($amountFen <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$now = time();
|
||||
$description = self::buildPlatformFeeDescription($testType, $testResultId);
|
||||
|
||||
Db::startTrans();
|
||||
try {
|
||||
$enterprise = Db::name('enterprises')
|
||||
->where('id', $enterpriseId)
|
||||
->field('id, balance')
|
||||
->lock(true)
|
||||
->find();
|
||||
if (!$enterprise) {
|
||||
Db::rollback();
|
||||
return;
|
||||
}
|
||||
|
||||
$exists = Db::name('finance_records')
|
||||
->where('enterpriseId', $enterpriseId)
|
||||
->where('type', 'consume')
|
||||
->whereNull('orderId')
|
||||
->where('description', $description)
|
||||
->find();
|
||||
if ($exists) {
|
||||
Db::commit();
|
||||
return;
|
||||
}
|
||||
|
||||
$beforeFen = (int) ($enterprise['balance'] ?? 0);
|
||||
// 平台费独立记账,允许与企业测试收入分列后形成净额。
|
||||
$afterFen = $beforeFen - $amountFen;
|
||||
|
||||
Db::name('enterprises')
|
||||
->where('id', $enterpriseId)
|
||||
->update([
|
||||
'balance' => $afterFen,
|
||||
'updatedAt' => $now,
|
||||
]);
|
||||
|
||||
Db::name('finance_records')->insert([
|
||||
'enterpriseId' => $enterpriseId,
|
||||
'type' => 'consume',
|
||||
'amount' => $amountFen,
|
||||
'balanceBefore' => $beforeFen,
|
||||
'balanceAfter' => $afterFen,
|
||||
'description' => $description,
|
||||
'orderId' => null,
|
||||
'createdAt' => $now,
|
||||
]);
|
||||
|
||||
Db::commit();
|
||||
} catch (\Throwable $e) {
|
||||
Db::rollback();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从超管全局 enterprise 定价读取平台费(分)。
|
||||
*/
|
||||
public static function getPlatformFeeFen(string $testType): int
|
||||
{
|
||||
$config = PricingConfigModel::where('type', 'enterprise')->whereNull('enterpriseId')->find();
|
||||
if (!$config || empty($config->config)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$raw = $config->config;
|
||||
$pricing = is_array($raw) ? $raw : (array) $raw;
|
||||
$key = $testType === 'team_analysis' ? 'teamAnalysis' : $testType;
|
||||
if (!isset($pricing[$key])) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$yuan = (float) $pricing[$key];
|
||||
return $yuan > 0 ? (int) round($yuan * 100) : 0;
|
||||
}
|
||||
|
||||
private static function buildPlatformFeeDescription(string $testType, int $testResultId): string
|
||||
{
|
||||
return '平台扣费:' . self::getTestTypeLabel($testType) . '测试(testResultId=' . $testResultId . ')';
|
||||
}
|
||||
|
||||
private static function getTestTypeLabel(string $testType): string
|
||||
{
|
||||
$map = [
|
||||
'face' => '人脸',
|
||||
'mbti' => 'MBTI',
|
||||
'sbti' => 'SBTI',
|
||||
'disc' => 'DISC',
|
||||
'pdp' => 'PDP',
|
||||
'resume' => '简历',
|
||||
];
|
||||
|
||||
return $map[$testType] ?? strtoupper($testType);
|
||||
}
|
||||
}
|
||||
@@ -101,7 +101,7 @@ class Finance extends BaseController
|
||||
$direction = $type === 'consume' ? 'out' : 'in';
|
||||
$description = (string) ($row['description'] ?? '');
|
||||
$typeLabel = $type === 'consume'
|
||||
? '佣金扣减'
|
||||
? (strpos($description, '平台扣费:') === 0 ? '平台扣费' : '佣金扣减')
|
||||
: (strpos($description, '企业余额充值') !== false ? '余额充值' : ($orderId > 0 ? '测试收入' : '余额充值'));
|
||||
|
||||
return [
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
namespace app\controller\api;
|
||||
|
||||
use app\BaseController;
|
||||
use app\common\SystemDefaultEnterprise;
|
||||
use app\common\service\EnterpriseBillingService;
|
||||
use app\controller\api\Test as TestController;
|
||||
use app\model\AiProvider as AiProviderModel;
|
||||
use app\model\SystemConfig as SystemConfigModel;
|
||||
@@ -51,7 +51,7 @@ class Analyze extends BaseController
|
||||
// 三个变量各司其职(同 Test::submit 逻辑):
|
||||
// $enterpriseId —— 仅企业测试(请求体传入)才非 null,决定走 admin_enterprise 定价
|
||||
// $pricingEnterpriseId —— 个人测试时从 wechat_users 取,走 admin_personal + eid 定价
|
||||
// $writeEnterpriseId —— 写入 test_results.enterpriseId(企业测试 or 绑定企业都记录)
|
||||
// $writeEnterpriseId —— 写入 test_results.enterpriseId(仅企业测试或真实绑定企业时记录)
|
||||
$pricingEnterpriseId = $enterpriseId;
|
||||
$writeEnterpriseId = $enterpriseId;
|
||||
if ($enterpriseId === null && $earlyUserId > 0) {
|
||||
@@ -59,12 +59,6 @@ class Analyze extends BaseController
|
||||
if (!empty($boundEid)) {
|
||||
$pricingEnterpriseId = (int) $boundEid; // admin_personal + eid
|
||||
$writeEnterpriseId = (int) $boundEid; // 历史记录展示企业名
|
||||
} else {
|
||||
$defEid = SystemDefaultEnterprise::getId();
|
||||
if ($defEid !== null) {
|
||||
$pricingEnterpriseId = $defEid;
|
||||
$writeEnterpriseId = $defEid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,6 +183,11 @@ class Analyze extends BaseController
|
||||
]);
|
||||
}
|
||||
}
|
||||
try {
|
||||
EnterpriseBillingService::chargePlatformFeeForTestResult($testResultId, 'face', $writeEnterpriseId);
|
||||
} catch (\Throwable $e) {
|
||||
// 平台费扣款失败不阻断主流程
|
||||
}
|
||||
// 面相分析由本接口直接写入 test_results,也要补触发测试完成佣金
|
||||
try {
|
||||
\app\controller\api\Distribution::settleTestCommission($testResultId, $userId, 'face');
|
||||
@@ -376,6 +375,11 @@ class Analyze extends BaseController
|
||||
]);
|
||||
}
|
||||
}
|
||||
try {
|
||||
EnterpriseBillingService::chargePlatformFeeForTestResult($testResultId, 'resume', $enterpriseId > 0 ? $enterpriseId : null);
|
||||
} catch (\Throwable $e) {
|
||||
// 平台费扣款失败不阻断主流程
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('resumeAnalysis: save test_result failed ' . $e->getMessage());
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
namespace app\controller\api;
|
||||
|
||||
use app\BaseController;
|
||||
use app\common\SystemDefaultEnterprise;
|
||||
use app\common\PdpDiscResultText;
|
||||
use app\common\service\EnterpriseBillingService;
|
||||
use app\model\Enterprise as EnterpriseModel;
|
||||
use app\model\PricingConfig as PricingConfigModel;
|
||||
use app\model\Question as QuestionModel;
|
||||
@@ -668,7 +668,7 @@ class Test extends BaseController
|
||||
// 三个变量各司其职:
|
||||
// $enterpriseId —— 仅企业测试(请求体传入)才非 null,决定走 admin_enterprise 定价
|
||||
// $pricingEnterpriseId —— 个人测试时从 wechat_users 取,走 admin_personal + eid 定价
|
||||
// $writeEnterpriseId —— 写入 test_results.enterpriseId(企业测试 or 绑定企业都记录)
|
||||
// $writeEnterpriseId —— 写入 test_results.enterpriseId(仅企业测试或真实绑定企业时记录)
|
||||
$pricingEnterpriseId = $enterpriseId;
|
||||
$writeEnterpriseId = $enterpriseId;
|
||||
if ($enterpriseId === null) {
|
||||
@@ -676,13 +676,6 @@ class Test extends BaseController
|
||||
if (!empty($boundEid)) {
|
||||
$pricingEnterpriseId = (int) $boundEid; // admin_personal + eid
|
||||
$writeEnterpriseId = (int) $boundEid; // 历史记录展示企业名
|
||||
} else {
|
||||
// 主入口无参数且未绑定企业:回落超管配置的默认企业(与个人版 getEnterpriseIdForApiPayload 不传参一致,由服务端统一落库)
|
||||
$defEid = SystemDefaultEnterprise::getId();
|
||||
if ($defEid !== null) {
|
||||
$pricingEnterpriseId = $defEid;
|
||||
$writeEnterpriseId = $defEid;
|
||||
}
|
||||
}
|
||||
}
|
||||
$requiresPayment = $this->getRequiresPaymentByTestType($testType, $enterpriseId, $pricingEnterpriseId);
|
||||
@@ -715,6 +708,12 @@ class Test extends BaseController
|
||||
]);
|
||||
}
|
||||
}
|
||||
// 已绑定企业时,独立按超管企业版单价扣平台费,与用户实付分列记账。
|
||||
try {
|
||||
EnterpriseBillingService::chargePlatformFeeForTestResult($id, $testType, $writeEnterpriseId);
|
||||
} catch (\Throwable $e) {
|
||||
// 平台费扣款失败不阻断主流程
|
||||
}
|
||||
// 测试完成佣金结算(无需付款,异步不影响主流程)
|
||||
try {
|
||||
\app\controller\api\Distribution::settleTestCommission($id, $userId, $testType);
|
||||
|
||||
@@ -61,19 +61,19 @@ class Settings extends BaseController
|
||||
'defaultEnterpriseId' => null,
|
||||
];
|
||||
$systemOut = $systemDefault;
|
||||
// 仅当 system 行「未包含」maintenanceMode 键时,才用旧表 review_mode 回退(否则关闭审核后会被 review_mode.enabled 再次顶成「已开启」)
|
||||
$hasMaintenanceKeyInSystem = false;
|
||||
if ($systemConfig && $systemConfig->value !== null && $systemConfig->value !== '') {
|
||||
$raw = $systemConfig->value;
|
||||
$decoded = is_array($raw) ? $raw : (is_string($raw) ? json_decode($raw, true) : null);
|
||||
if (is_array($decoded)) {
|
||||
$hasMaintenanceKeyInSystem = array_key_exists('maintenanceMode', $decoded);
|
||||
$systemOut = array_merge($systemDefault, $decoded);
|
||||
}
|
||||
}
|
||||
// 审核模式唯一口径:system.maintenanceMode(布尔)。兼容旧版 review_mode.enabled
|
||||
$maint = false;
|
||||
if (isset($systemOut['maintenanceMode'])) {
|
||||
$maint = (bool) $systemOut['maintenanceMode'];
|
||||
}
|
||||
if (!$maint && $reviewModeConfig && !empty($reviewModeConfig->value)) {
|
||||
// 审核模式:system.maintenanceMode 为唯一口径;兼容旧数据仅在没有该键时读 review_mode.enabled
|
||||
$maint = (bool) ($systemOut['maintenanceMode'] ?? false);
|
||||
if (!$hasMaintenanceKeyInSystem && !$maint && $reviewModeConfig && !empty($reviewModeConfig->value)) {
|
||||
$rv = $reviewModeConfig->value;
|
||||
if (is_string($rv)) {
|
||||
$rv = json_decode($rv, true);
|
||||
@@ -187,6 +187,21 @@ class Settings extends BaseController
|
||||
$config->value = array_merge($oldArr, $data);
|
||||
$config->save();
|
||||
|
||||
// 与旧版 review_mode 行同步,避免库内 enabled=true 与 system.maintenanceMode=false 并存
|
||||
if (array_key_exists('maintenanceMode', $data)) {
|
||||
$rmRow = SystemConfigModel::where('key', 'review_mode')->where('enterprise_id', 0)->find();
|
||||
if ($rmRow) {
|
||||
$rv = $rmRow->value;
|
||||
$rv = is_array($rv) ? $rv : (is_string($rv) ? (json_decode($rv, true) ?: []) : []);
|
||||
if (!is_array($rv)) {
|
||||
$rv = [];
|
||||
}
|
||||
$rv['enabled'] = !empty($data['maintenanceMode']);
|
||||
$rmRow->value = $rv;
|
||||
$rmRow->save();
|
||||
}
|
||||
}
|
||||
|
||||
// 更新站点信息
|
||||
$this->updateSiteInfo($data);
|
||||
|
||||
@@ -528,6 +543,18 @@ class Settings extends BaseController
|
||||
$config->value = $oldArr;
|
||||
$config->save();
|
||||
|
||||
$rmRow = SystemConfigModel::where('key', 'review_mode')->where('enterprise_id', 0)->find();
|
||||
if ($rmRow) {
|
||||
$rv = $rmRow->value;
|
||||
$rv = is_array($rv) ? $rv : (is_string($rv) ? (json_decode($rv, true) ?: []) : []);
|
||||
if (!is_array($rv)) {
|
||||
$rv = [];
|
||||
}
|
||||
$rv['enabled'] = (bool) $on;
|
||||
$rmRow->value = $rv;
|
||||
$rmRow->save();
|
||||
}
|
||||
|
||||
return success([
|
||||
'maintenanceMode' => (bool) $on,
|
||||
'enabled' => (bool) $on,
|
||||
|
||||
@@ -1,117 +1,101 @@
|
||||
<?php
|
||||
namespace app\model;
|
||||
|
||||
use think\Model;
|
||||
|
||||
/**
|
||||
* 定价配置模型
|
||||
* 实际表名 = 数据库前缀 + pricing_config,例如 .env 中 DATABASE_PREFIX=mbti_ 时为 mbti_pricing_config
|
||||
*/
|
||||
class PricingConfig extends Model
|
||||
{
|
||||
// 表名(不含前缀);最终访问表 = config(database.prefix) + pricing_config
|
||||
protected $name = 'pricing_config';
|
||||
|
||||
// 设置字段信息(匹配数据库字段命名:驼峰命名)
|
||||
protected $schema = [
|
||||
'id' => 'int',
|
||||
'type' => 'string',
|
||||
'enterpriseId' => 'int',
|
||||
'config' => 'string',
|
||||
'createdAt' => 'int',
|
||||
'updatedAt' => 'int',
|
||||
];
|
||||
|
||||
// 自动时间戳(使用驼峰命名,时间戳格式)
|
||||
protected $autoWriteTimestamp = 'int';
|
||||
|
||||
// 时间戳字段名(驼峰命名,匹配数据库)
|
||||
protected $createTime = 'createdAt';
|
||||
protected $updateTime = 'updatedAt';
|
||||
|
||||
// 时间字段类型(时间戳格式)
|
||||
protected $type = [
|
||||
'createdAt' => 'integer',
|
||||
'updatedAt' => 'integer',
|
||||
];
|
||||
|
||||
// JSON字段自动转换
|
||||
protected $json = ['config'];
|
||||
|
||||
/**
|
||||
* 配置修改器(自动转换为JSON)
|
||||
*/
|
||||
public function setConfigAttr($value)
|
||||
{
|
||||
if (is_array($value)) {
|
||||
return json_encode($value, JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置获取器(自动解析JSON)
|
||||
*/
|
||||
public function getConfigAttr($value)
|
||||
{
|
||||
if (is_string($value)) {
|
||||
return json_decode($value, true);
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按类型与可选企业ID取定价配置
|
||||
*
|
||||
* personal(个人版)优先级:
|
||||
* 1. admin_personal + enterpriseId(企业专属管理端配置,有 eid 时)
|
||||
* 2. admin_personal + null(通用管理端配置)
|
||||
* 3. 任意一条 admin_personal(兜底:只要管理端配过就不走超管)
|
||||
* 4. personal + null(超管全局,仅在管理端完全未配置时使用)
|
||||
*
|
||||
* enterprise(企业版)优先级:
|
||||
* 1. admin_enterprise + enterpriseId(有 eid 时)
|
||||
* 2. admin_enterprise + null(通用管理端企业配置)
|
||||
* 3. 任意一条 admin_enterprise
|
||||
* 4. enterprise + null(超管全局兜底)
|
||||
*
|
||||
* @param string $type personal|enterprise|deep
|
||||
* @param int|null $enterpriseId 有则优先读该企业专属配置
|
||||
* @return \app\model\PricingConfig|null
|
||||
*/
|
||||
public static function getByTypeAndEnterprise(string $type, ?int $enterpriseId = null): ?self
|
||||
{
|
||||
if ($type === 'personal') {
|
||||
if (!empty($enterpriseId)) {
|
||||
$row = self::where('type', 'admin_personal')->where('enterpriseId', $enterpriseId)->find();
|
||||
if ($row) return $row;
|
||||
}
|
||||
// 通用管理端个人配置(admin_personal + null)
|
||||
$row = self::where('type', 'admin_personal')->whereNull('enterpriseId')->find();
|
||||
if ($row) return $row;
|
||||
// 任意管理端个人配置(兜底:管理端配过就不走超管)
|
||||
$row = self::where('type', 'admin_personal')->order('id', 'asc')->find();
|
||||
if ($row) return $row;
|
||||
// 超管全局个人定价(最后兜底,仅管理端完全未配置时使用)
|
||||
return self::where('type', 'personal')->whereNull('enterpriseId')->find();
|
||||
}
|
||||
if ($type === 'enterprise') {
|
||||
if (!empty($enterpriseId)) {
|
||||
$row = self::where('type', 'admin_enterprise')->where('enterpriseId', $enterpriseId)->find();
|
||||
if ($row) return $row;
|
||||
}
|
||||
// 通用管理端企业配置(admin_enterprise + null)
|
||||
$row = self::where('type', 'admin_enterprise')->whereNull('enterpriseId')->find();
|
||||
if ($row) return $row;
|
||||
// 任意管理端企业配置(兜底)
|
||||
$row = self::where('type', 'admin_enterprise')->order('id', 'asc')->find();
|
||||
if ($row) return $row;
|
||||
return self::where('type', 'enterprise')->whereNull('enterpriseId')->find();
|
||||
}
|
||||
if ($type === 'deep') {
|
||||
return self::where('type', 'deep')->whereNull('enterpriseId')->find();
|
||||
}
|
||||
return self::where('type', $type)->whereNull('enterpriseId')->find();
|
||||
}
|
||||
}
|
||||
|
||||
<?php
|
||||
namespace app\model;
|
||||
|
||||
use think\Model;
|
||||
|
||||
/**
|
||||
* 定价配置模型
|
||||
* 实际表名 = 数据库前缀 + pricing_config,例如 .env 中 DATABASE_PREFIX=mbti_ 时为 mbti_pricing_config
|
||||
*/
|
||||
class PricingConfig extends Model
|
||||
{
|
||||
// 表名(不含前缀);最终访问表 = config(database.prefix) + pricing_config
|
||||
protected $name = 'pricing_config';
|
||||
|
||||
// 设置字段信息(匹配数据库字段命名:驼峰命名)
|
||||
protected $schema = [
|
||||
'id' => 'int',
|
||||
'type' => 'string',
|
||||
'enterpriseId' => 'int',
|
||||
'config' => 'string',
|
||||
'createdAt' => 'int',
|
||||
'updatedAt' => 'int',
|
||||
];
|
||||
|
||||
// 自动时间戳(使用驼峰命名,时间戳格式)
|
||||
protected $autoWriteTimestamp = 'int';
|
||||
|
||||
// 时间戳字段名(驼峰命名,匹配数据库)
|
||||
protected $createTime = 'createdAt';
|
||||
protected $updateTime = 'updatedAt';
|
||||
|
||||
// 时间字段类型(时间戳格式)
|
||||
protected $type = [
|
||||
'createdAt' => 'integer',
|
||||
'updatedAt' => 'integer',
|
||||
];
|
||||
|
||||
// JSON字段自动转换
|
||||
protected $json = ['config'];
|
||||
|
||||
/**
|
||||
* 配置修改器(自动转换为JSON)
|
||||
*/
|
||||
public function setConfigAttr($value)
|
||||
{
|
||||
if (is_array($value)) {
|
||||
return json_encode($value, JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置获取器(自动解析JSON)
|
||||
*/
|
||||
public function getConfigAttr($value)
|
||||
{
|
||||
if (is_string($value)) {
|
||||
return json_decode($value, true);
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按类型与可选企业ID取定价配置
|
||||
*
|
||||
* personal(个人版)优先级:
|
||||
* 1. admin_personal + enterpriseId(企业专属管理端配置,有 eid 时)
|
||||
* 2. personal + null(超管全局,仅未绑定企业或该企业未配置时使用)
|
||||
*
|
||||
* enterprise(企业版)优先级:
|
||||
* 1. admin_enterprise + enterpriseId(有 eid 时)
|
||||
* 2. enterprise + null(超管全局兜底)
|
||||
*
|
||||
* @param string $type personal|enterprise|deep
|
||||
* @param int|null $enterpriseId 有则优先读该企业专属配置
|
||||
* @return \app\model\PricingConfig|null
|
||||
*/
|
||||
public static function getByTypeAndEnterprise(string $type, ?int $enterpriseId = null): ?self
|
||||
{
|
||||
if ($type === 'personal') {
|
||||
if (!empty($enterpriseId)) {
|
||||
$row = self::where('type', 'admin_personal')->where('enterpriseId', $enterpriseId)->find();
|
||||
if ($row) return $row;
|
||||
}
|
||||
// 超管全局个人定价(最后兜底,仅管理端完全未配置时使用)
|
||||
return self::where('type', 'personal')->whereNull('enterpriseId')->find();
|
||||
}
|
||||
if ($type === 'enterprise') {
|
||||
if (!empty($enterpriseId)) {
|
||||
$row = self::where('type', 'admin_enterprise')->where('enterpriseId', $enterpriseId)->find();
|
||||
if ($row) return $row;
|
||||
}
|
||||
return self::where('type', 'enterprise')->whereNull('enterpriseId')->find();
|
||||
}
|
||||
if ($type === 'deep') {
|
||||
return self::where('type', 'deep')->whereNull('enterpriseId')->find();
|
||||
}
|
||||
return self::where('type', $type)->whereNull('enterpriseId')->find();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
147
api/docs/定价与企业账务规则说明.md
Normal file
147
api/docs/定价与企业账务规则说明.md
Normal file
@@ -0,0 +1,147 @@
|
||||
# 定价与企业账务规则说明
|
||||
|
||||
> **文档用途**:供产品/运营阅读业务口径,供 AI 与研发对齐实现与测试用例。
|
||||
> **状态**:下述「目标账务口径」与线上代码可能不一致,落地前需对照 `Test` / `Payment` / `finance_records` 做改造与验收。
|
||||
|
||||
---
|
||||
|
||||
## 1. 一句话摘要
|
||||
|
||||
- **用户付多少**:主要由**企业端**「个人版 / 企业版」单价决定;**未绑定企业**时回落**超管个人版**单价。
|
||||
- **企业扣款(平台费)**:在**已绑定企业**且满足条件时,按**超管「企业版」**维度配置的单价(示例中 PDP **1 元**)从企业余额扣费。
|
||||
- **企业收入**:通常对应**用户实付**进入企业的部分(与「平台扣款」分列科目,净额可另做报表)。
|
||||
|
||||
---
|
||||
|
||||
## 2. 术语表
|
||||
|
||||
| 术语 | 含义 |
|
||||
|:---|:---|
|
||||
| **超管全局价** | 超级管理员在「全局定价」中维护的 `pricing_config`:`type = personal \| enterprise` 且 `enterpriseId` 为空。 |
|
||||
| **企业端价** | 企业在管理后台维护的:`admin_personal`、`admin_enterprise`(可按企业 ID 覆盖)。 |
|
||||
| **个人版测试** | 请求未带企业测试上下文,`test_results.testScope = personal`(用户从个人入口完成测评)。 |
|
||||
| **企业版测试** | 请求带企业测试上下文,`test_results.testScope = enterprise`。 |
|
||||
| **绑定企业** | 用户账号 `wechat_users.enterpriseId` 指向某企业(或业务上认可的等价判定)。 |
|
||||
| **用户付费** | 终端用户通过微信支付等方式支付的金额。 |
|
||||
| **企业扣款** | 从企业**余额**中扣减(`finance_records.type = consume` 等),表示平台向企业收取的费用。 |
|
||||
| **企业收入** | 企业因该笔业务获得的收入(常与用户实付入账对应;与扣款分列)。 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 配置分层(示例)
|
||||
|
||||
以下数值仅为**文档示例**,实际以数据库配置为准。
|
||||
|
||||
### 3.1 超管全局
|
||||
|
||||
| 维度 | PDP 单价(示例) |
|
||||
|:---|:---:|
|
||||
| 超管 · **个人版** | 0 元 |
|
||||
| 超管 · **企业版** | 1 元 |
|
||||
|
||||
### 3.2 某企业(企业端)
|
||||
|
||||
| 维度 | PDP 单价(示例) |
|
||||
|:---|:---:|
|
||||
| 企业端 · **个人版**(`admin_personal`) | 0.1 元 |
|
||||
| 企业端 · **企业版**(`admin_enterprise`) | 0 元 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 业务规则(目标口径)
|
||||
|
||||
下列规则按**优先级**书写,便于实现与单测。
|
||||
|
||||
### 4.1 用户侧应付金额
|
||||
|
||||
1. **已绑定企业**且走**个人版测试**:按该企业 **企业端个人版** 定价计算是否需付费及金额(示例 PDP **0.1 元**)。
|
||||
2. **已绑定企业**且走**企业版测试**:按该企业 **企业端企业版** 定价计算(示例 PDP **0 元** → 用户无需付费)。
|
||||
3. **未绑定企业**且走**个人版测试**:按 **超管个人版** 定价计算(示例 PDP **0 元** → 用户无需付费)。
|
||||
4. 若存在「小程序默认企业」等回落逻辑,是否视为绑定,**须单独在产品上定稿**(本文示例 **C** 按**未绑定**处理)。
|
||||
|
||||
### 4.2 企业侧平台扣款(示例中的「扣 1」)
|
||||
|
||||
1. 当用户**已绑定企业**,且业务要求对该笔测试收取**平台费**时:扣费金额取 **超管「企业版」** 中对应测试类型单价(示例 PDP **1 元**)。
|
||||
2. **个人版测试**与**企业版测试**只要满足「已绑定企业 + 触发扣费条件」,均可发生该企业扣款(示例 **A、B** 均扣 **1 元**)。
|
||||
3. **未绑定企业**(示例 **C**):不发生面向企业的平台扣款。
|
||||
|
||||
### 4.3 企业收入(与用户实付对应)
|
||||
|
||||
1. 用户有实付时,企业收入可按实付记账(示例 **A**:用户付 **0.1 元** → 企业收入 **0.1 元**)。
|
||||
2. 用户无实付时,企业收入为 **0**(示例 **B**)。
|
||||
3. 「企业扣款」与「企业收入」**分列**;是否在报表中汇总为**净额**,由财务/产品另定。
|
||||
|
||||
---
|
||||
|
||||
## 5. 场景对照表(文档示例)
|
||||
|
||||
**前提**:超管个人 0 / 企业 PDP 1;该企业端个人 0.1 / 企业 0。
|
||||
|
||||
| 编号 | 绑定企业 | 测试方式 | 用户付费 | 企业扣款(平台费,示例) | 企业收入(示例) |
|
||||
|:---:|:---:|:---|:---:|:---:|:---:|
|
||||
| **A** | 是 | 个人版 | 0.1 元 | 1 元 | 0.1 元 |
|
||||
| **B** | 是 | 企业版 | 0 | 1 元 | 0 |
|
||||
| **C** | 否 | 个人版 | 0 | — | — |
|
||||
|
||||
---
|
||||
|
||||
## 6. 机器可读摘要(供 AI / 自动化)
|
||||
|
||||
下列 YAML 与正文一致,便于检索与生成测试数据。
|
||||
|
||||
```yaml
|
||||
# 定价与企业账务规则说明 — 规则摘要(示例配置内嵌)
|
||||
version: 1
|
||||
currency: CNY
|
||||
test_type_example: pdp
|
||||
|
||||
super_admin_global:
|
||||
personal: { pdp: 0 }
|
||||
enterprise: { pdp: 1 }
|
||||
|
||||
enterprise_admin_example:
|
||||
admin_personal: { pdp: 0.1 }
|
||||
admin_enterprise: { pdp: 0 }
|
||||
|
||||
scenarios:
|
||||
- id: A
|
||||
user_bound_enterprise: true
|
||||
test_scope: personal
|
||||
user_pays_yuan: 0.1
|
||||
enterprise_platform_fee_yuan: 1
|
||||
enterprise_revenue_yuan: 0.1
|
||||
- id: B
|
||||
user_bound_enterprise: true
|
||||
test_scope: enterprise
|
||||
user_pays_yuan: 0
|
||||
enterprise_platform_fee_yuan: 1
|
||||
enterprise_revenue_yuan: 0
|
||||
- id: C
|
||||
user_bound_enterprise: false
|
||||
test_scope: personal
|
||||
user_pays_yuan: 0
|
||||
enterprise_platform_fee_yuan: null
|
||||
enterprise_revenue_yuan: null
|
||||
|
||||
rule_hints:
|
||||
user_price_source: enterprise_admin_when_bound_else_super_admin_personal
|
||||
enterprise_fee_source_super_admin: enterprise_tier_global
|
||||
note: 实现时需确认与现有 Payment 入账逻辑是否合并或拆分科目
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 与当前实现的差异(给研发)
|
||||
|
||||
- 现有逻辑中,**用户支付成功**常将金额记入企业余额(入账),**未必**存在「按超管企业版单价固定扣平台费」的独立流水。
|
||||
- 若完全实现本文 **4.2**,需在测试完成或支付闭环处增加**幂等**的企业扣款(`consume`)及与订单、分销的**顺序**约定。
|
||||
- 「企业收入 0.1」与「企业扣款 1」并存时,**余额净变化**需在财务口径上明确(避免与现网「只加不减」假设冲突)。
|
||||
|
||||
---
|
||||
|
||||
## 8. 修订记录
|
||||
|
||||
| 日期 | 说明 |
|
||||
|:---|:---|
|
||||
| 2026-04-13 | 初稿:整合超管/企业端示例与 A/B/C 场景及 YAML 摘要 |
|
||||
| 2026-04-13 | 文档文件名改为中文:`定价与企业账务规则说明.md` |
|
||||
@@ -47,8 +47,8 @@ App({
|
||||
// 超管配置的默认企业 ID(无 scene/eid 等入口参数时回落)
|
||||
defaultEnterpriseId: null,
|
||||
// API基础地址(开发时用本地,生产环境替换为实际域名)
|
||||
apiBase: 'https://mbtiapi.quwanzhi.com',
|
||||
//apiBase: 'http://mbti.com',
|
||||
//apiBase: 'https://mbtiapi.quwanzhi.com',
|
||||
apiBase: 'http://mbti.com',
|
||||
// VIP信息
|
||||
vipInfo: null,
|
||||
// 测试次数
|
||||
|
||||
Reference in New Issue
Block a user