chore: 首次提交 - 关联 GitHub fnvtk/MBTI_wang

Made-with: Cursor
This commit is contained in:
卡若
2026-03-17 12:39:38 +08:00
commit eb510304c0
259 changed files with 60464 additions and 0 deletions

View File

@@ -0,0 +1,109 @@
<?php
namespace app\model;
use think\Model;
use think\model\concern\SoftDelete;
/**
* AI服务商配置模型
*/
class AiProvider extends Model
{
use SoftDelete;
// 设置表名(不带前缀,前缀在数据库配置中设置)
protected $name = 'ai_providers';
// 软删除字段(驼峰命名,时间戳格式)
protected $deleteTime = 'deletedAt';
// 设置字段信息(匹配数据库字段命名:驼峰命名)
protected $schema = [
'id' => 'int',
'providerId' => 'string',
'name' => 'string',
'enabled' => 'int',
'visible' => 'int',
'apiKey' => 'string',
'apiEndpoint' => 'string',
'model' => 'string',
'organizationId' => 'string',
'maxTokens' => 'int',
'balanceAlertEnabled' => 'int',
'balanceAlertThreshold' => 'float',
'notes' => 'string',
'docUrl' => 'string',
'isFree' => 'int',
'supportsBalance' => 'int',
'lastBalance' => 'float',
'lastBalanceCurrency' => 'string',
'lastBalanceCheckedAt' => 'int',
'createdAt' => 'int',
'updatedAt' => 'int',
'deletedAt' => 'int',
'extraConfig' => 'string',
];
// 自动时间戳(使用驼峰命名,时间戳格式)
protected $autoWriteTimestamp = 'int';
// 时间戳字段名(驼峰命名,匹配数据库)
protected $createTime = 'createdAt';
protected $updateTime = 'updatedAt';
// 时间字段类型(时间戳格式)
protected $type = [
'lastBalanceCheckedAt' => 'integer',
'createdAt' => 'integer',
'updatedAt' => 'integer',
'deletedAt' => 'integer',
'extraConfig' => 'json',
];
// 注意API Key需要可逆读取用于API调用所以不隐藏但在获取器中脱敏
/**
* API Key 修改器存储原始值用于API调用
*/
public function setApiKeyAttr($value)
{
if (empty($value)) {
return null;
}
// 如果输入的是脱敏格式(包含****),不更新
if (strpos($value, '****') !== false) {
return null; // 返回null表示不更新此字段
}
// 直接存储原始值实际生产环境建议使用AES加密
return $value;
}
/**
* API Key 获取器(返回脱敏后的密钥)
* 注意如果需要原始密钥用于API调用使用 getRawApiKey() 方法
*/
public function getApiKeyAttr($value)
{
if (empty($value)) {
return '';
}
// 返回脱敏后的密钥显示前6位和后4位
if (strlen($value) > 10) {
return substr($value, 0, 6) . '****' . substr($value, -4);
}
return '****';
}
/**
* 获取原始API Key用于API调用
* @return string
*/
public function getRawApiKey()
{
// 直接从数据库读取原始值,绕过获取器
return \think\facade\Db::name('ai_providers')
->where('id', $this->id)
->value('apiKey') ?: '';
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace app\model;
use think\Model;
use think\model\concern\SoftDelete;
/**
* 数据库备份记录模型
*/
class BackupRecord extends Model
{
use SoftDelete;
// 设置表名(不带前缀,前缀在数据库配置中设置)
protected $name = 'backup_records';
// 软删除字段(驼峰命名,时间戳格式)
protected $deleteTime = 'deletedAt';
// 设置字段信息(匹配数据库字段命名:驼峰命名)
protected $schema = [
'id' => 'int',
'filename' => 'string',
'filepath' => 'string',
'fileSize' => 'int',
'ossUrl' => 'string',
'ossPath' => 'string',
'status' => 'string',
'deletedAt' => 'int',
'createdAt' => 'int',
'updatedAt' => 'int',
];
// 自动时间戳(使用驼峰命名,时间戳格式)
protected $autoWriteTimestamp = 'int';
// 时间戳字段名(驼峰命名,匹配数据库)
protected $createTime = 'createdAt';
protected $updateTime = 'updatedAt';
// 时间字段类型(时间戳格式)
protected $type = [
'deletedAt' => 'integer',
'createdAt' => 'integer',
'updatedAt' => 'integer',
];
}

View File

@@ -0,0 +1,51 @@
<?php
namespace app\model;
use think\Model;
use think\model\concern\SoftDelete;
/**
* 企业模型
*/
class Enterprise extends Model
{
use SoftDelete;
// 设置表名(不带前缀,前缀在数据库配置中设置)
protected $name = 'enterprises';
// 软删除字段(驼峰命名,时间戳格式)
protected $deleteTime = 'deletedAt';
// 设置字段信息(匹配数据库字段命名:驼峰命名)
protected $schema = [
'id' => 'int',
'name' => 'string',
'code' => 'string',
'contactName' => 'string',
'contactPhone' => 'string',
'contactEmail' => 'string',
'balance' => 'float',
'status' => 'string',
'trialExpireAt' => 'int',
'deletedAt' => 'int',
'createdAt' => 'int',
'updatedAt' => 'int',
];
// 自动时间戳(使用驼峰命名,时间戳格式)
protected $autoWriteTimestamp = 'int';
// 时间戳字段名(驼峰命名,匹配数据库)
protected $createTime = 'createdAt';
protected $updateTime = 'updatedAt';
// 时间字段类型(时间戳格式)
protected $type = [
'trialExpireAt' => 'integer',
'deletedAt' => 'integer',
'createdAt' => 'integer',
'updatedAt' => 'integer',
];
}

View File

@@ -0,0 +1,25 @@
<?php
namespace app\model;
use think\Model;
/**
* 企业版简历上传记录(仅记录上传,支持预览)
*/
class EnterpriseResumeUpload extends Model
{
protected $name = 'enterprise_resume_uploads';
protected $schema = [
'id' => 'int',
'userId' => 'int',
'enterpriseId' => 'int',
'fileUrl' => 'string',
'fileName' => 'string',
'is_default' => 'int',
'createdAt' => 'int',
];
protected $autoWriteTimestamp = 'int';
protected $createTime = 'createdAt';
}

View File

@@ -0,0 +1,117 @@
<?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();
}
}

View File

@@ -0,0 +1,91 @@
<?php
namespace app\model;
use think\Model;
use think\model\concern\SoftDelete;
/**
* 题目模型
*/
class Question extends Model
{
use SoftDelete;
// 设置表名(不带前缀,前缀在数据库配置中设置)
protected $name = 'questions';
// 软删除字段(驼峰命名,时间戳格式)
protected $deleteTime = 'deletedAt';
// 设置字段信息(匹配数据库字段命名:驼峰命名)
protected $schema = [
'id' => 'int',
'type' => 'string',
'question' => 'string',
'options' => 'string',
'dimension' => 'string',
'enterpriseId' => 'int',
'sort' => 'int',
'status' => 'int',
'deletedAt' => 'int',
'createdAt' => 'int',
'updatedAt' => 'int',
];
// 自动时间戳(使用驼峰命名,时间戳格式)
protected $autoWriteTimestamp = 'int';
// 时间戳字段名(驼峰命名,匹配数据库)
protected $createTime = 'createdAt';
protected $updateTime = 'updatedAt';
// 时间字段类型(时间戳格式)
protected $type = [
'deletedAt' => 'integer',
'createdAt' => 'integer',
'updatedAt' => 'integer',
];
// JSON字段自动转换
protected $json = ['options'];
/**
* 选项修改器自动转换为JSON
*/
public function setOptionsAttr($value)
{
if (is_array($value)) {
return json_encode($value, JSON_UNESCAPED_UNICODE);
}
return $value;
}
/**
* 选项获取器自动解析JSON确保返回数组格式
*/
public function getOptionsAttr($value)
{
if (is_string($value)) {
$decoded = json_decode($value, true);
// 如果解码失败或返回null返回空数组
if ($decoded === null && json_last_error() !== JSON_ERROR_NONE) {
return [];
}
// 如果是对象格式(关联数组),转换为索引数组
if (is_array($decoded) && !empty($decoded) && !isset($decoded[0])) {
return array_values($decoded);
}
return $decoded ?: [];
}
// 如果是对象stdClass转换为数组
if (is_object($value)) {
$value = json_decode(json_encode($value), true);
}
// 如果已经是数组,确保是索引数组
if (is_array($value) && !empty($value) && !isset($value[0])) {
return array_values($value);
}
return is_array($value) ? $value : [];
}
}

View File

@@ -0,0 +1,63 @@
<?php
namespace app\model;
use think\Model;
/**
* 系统配置模型
*/
class SystemConfig extends Model
{
// 设置表名(不带前缀,前缀在数据库配置中设置)
protected $name = 'system_config';
// 设置字段信息(匹配数据库字段命名:驼峰命名)
protected $schema = [
'id' => 'int',
'key' => 'string',
'enterprise_id' => 'int',
'value' => 'string',
'description' => 'string',
'createdAt' => 'int',
'updatedAt' => 'int',
];
// 自动时间戳(使用驼峰命名,时间戳格式)
protected $autoWriteTimestamp = 'int';
// 时间戳字段名(驼峰命名,匹配数据库)
protected $createTime = 'createdAt';
protected $updateTime = 'updatedAt';
// 时间字段类型(时间戳格式)
protected $type = [
'createdAt' => 'integer',
'updatedAt' => 'integer',
];
// JSON字段自动转换
protected $json = ['value'];
/**
* 配置值修改器自动转换为JSON
*/
public function setValueAttr($value)
{
if (is_array($value)) {
return json_encode($value, JSON_UNESCAPED_UNICODE);
}
return $value;
}
/**
* 配置值获取器自动解析JSON
*/
public function getValueAttr($value)
{
if (is_string($value)) {
return json_decode($value, true);
}
return $value;
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace app\model;
use think\Model;
/**
* 上传文件记录(本地/OSS用于去重与 URL 查询
*/
class UploadFile extends Model
{
protected $name = 'upload_files';
protected $schema = [
'id' => 'int',
'path' => 'string',
'url' => 'string',
'driver' => 'string',
'hash' => 'string',
'size' => 'int',
'mimeType' => 'string',
'extension' => 'string',
'createdAt' => 'int',
'updatedAt' => 'int',
];
protected $autoWriteTimestamp = 'int';
protected $createTime = 'createdAt';
protected $updateTime = 'updatedAt';
}

76
api/app/model/User.php Normal file
View File

@@ -0,0 +1,76 @@
<?php
namespace app\model;
use think\Model;
use think\model\concern\SoftDelete;
/**
* 用户模型
*/
class User extends Model
{
use SoftDelete;
// 设置表名(不带前缀,前缀在数据库配置中设置)
protected $name = 'users';
// 软删除字段(驼峰命名,时间戳格式)
protected $deleteTime = 'deletedAt';
// 设置字段信息(匹配数据库字段命名:驼峰命名)
protected $schema = [
'id' => 'int',
'username' => 'string',
'password' => 'string',
'phone' => 'string',
'email' => 'string',
'role' => 'string',
'enterpriseId' => 'int',
'mbtiType' => 'string',
'region' => 'string',
'industry' => 'string',
'status' => 'int',
'lastLoginTime' => 'int',
'lastLoginIp' => 'string',
'deletedAt' => 'int',
'createdAt' => 'int',
'updatedAt' => 'int',
];
// 自动时间戳(使用驼峰命名,时间戳格式)
protected $autoWriteTimestamp = 'int';
// 时间戳字段名(驼峰命名,匹配数据库)
protected $createTime = 'createdAt';
protected $updateTime = 'updatedAt';
// 隐藏字段(不返回给前端)
protected $hidden = ['password'];
// 时间字段类型(时间戳格式)
protected $type = [
'lastLoginTime' => 'integer',
'deletedAt' => 'integer',
'createdAt' => 'integer',
'updatedAt' => 'integer',
];
/**
* 密码修改器(自动加密)
*/
public function setPasswordAttr($value)
{
return password_hash($value, PASSWORD_DEFAULT);
}
/**
* 验证密码
* @param string $password 明文密码
* @return bool
*/
public function verifyPassword($password)
{
return password_verify($password, $this->password);
}
}

View File

@@ -0,0 +1,162 @@
<?php
namespace app\model;
use think\Model;
use think\facade\Db;
/**
* 用户画像汇总模型
* 实际表名: 前缀 + user_profile (如 mbti_user_profile
*/
class UserProfile extends Model
{
protected $name = 'user_profile';
protected $schema = [
'id' => 'int',
'userId' => 'int',
'userType' => 'string',
'enterpriseId' => 'int',
'testsTotal' => 'int',
'testsMbti' => 'int',
'testsDisc' => 'int',
'testsPdp' => 'int',
'testsFace' => 'int',
'ordersTotal' => 'int',
'paidOrders' => 'int',
'totalPaidAmount' => 'int',
'lastTestResultId'=> 'int',
'lastTestType' => 'string',
'lastTestAt' => 'int',
'lastMbtiResultId'=> 'int',
'lastDiscResultId'=> 'int',
'lastPdpResultId' => 'int',
'lastFaceResultId'=> 'int',
'createdAt' => 'int',
'updatedAt' => 'int',
];
protected $autoWriteTimestamp = 'int';
protected $createTime = 'createdAt';
protected $updateTime = 'updatedAt';
/**
* 测试完成后更新用户画像统计与最近测试ID
*/
public static function recordTest(int $userId, string $testType, int $testResultId, ?int $enterpriseId = null, ?int $createdAt = null): void
{
if ($userId <= 0 || !$testType || $testResultId <= 0) {
return;
}
$now = $createdAt ?: time();
$userType = $enterpriseId ? 'enterprise' : 'personal';
[$data, $id] = self::loadOrInitRow($userId, $userType, $enterpriseId, $now);
$data['testsTotal']++;
switch ($testType) {
case 'mbti':
$data['testsMbti']++;
$data['lastMbtiResultId'] = $testResultId;
break;
case 'disc':
$data['testsDisc']++;
$data['lastDiscResultId'] = $testResultId;
break;
case 'pdp':
$data['testsPdp']++;
$data['lastPdpResultId'] = $testResultId;
break;
case 'face':
case 'ai':
$data['testsFace']++;
$data['lastFaceResultId'] = $testResultId;
break;
}
$data['lastTestResultId'] = $testResultId;
$data['lastTestType'] = $testType;
$data['lastTestAt'] = $now;
$data['updatedAt'] = $now;
self::upsertRow($data, $id);
}
/**
* 支付成功后更新订单统计与总支付金额
*
* @param int $userId
* @param int|null $enterpriseId
* @param int $amountFen 本次支付金额(分)
*/
public static function recordPayment(int $userId, ?int $enterpriseId, int $amountFen): void
{
if ($userId <= 0 || $amountFen <= 0) {
return;
}
$now = time();
$userType = $enterpriseId ? 'enterprise' : 'personal';
[$data, $id] = self::loadOrInitRow($userId, $userType, $enterpriseId, $now);
$data['ordersTotal'] = (int) ($data['ordersTotal'] ?? 0) + 1;
$data['paidOrders'] = (int) ($data['paidOrders'] ?? 0) + 1;
$currentTotal = (int) ($data['totalPaidAmount'] ?? 0);
$data['totalPaidAmount'] = $currentTotal + $amountFen;
$data['updatedAt'] = $now;
self::upsertRow($data, $id);
}
/**
* 读或初始化一行画像数据
*/
protected static function loadOrInitRow(int $userId, string $userType, ?int $enterpriseId, int $now): array
{
$where = [
'userId' => $userId,
'userType' => $userType,
'enterpriseId' => $enterpriseId,
];
$row = Db::name('user_profile')->where($where)->lock(true)->find();
$base = [
'testsTotal' => 0,
'testsMbti' => 0,
'testsDisc' => 0,
'testsPdp' => 0,
'testsFace' => 0,
'ordersTotal' => 0,
'paidOrders' => 0,
'totalPaidAmount' => 0,
'lastMbtiResultId'=> null,
'lastDiscResultId'=> null,
'lastPdpResultId' => null,
'lastFaceResultId'=> null,
];
if ($row) {
$data = array_merge($base, $row);
$id = (int) $row['id'];
} else {
$data = array_merge($base, $where, ['createdAt' => $now]);
$id = 0;
}
return [$data, $id];
}
/**
* 写入或更新一行画像数据
*/
protected static function upsertRow(array $data, int $id): void
{
if ($id > 0) {
Db::name('user_profile')->where('id', $id)->update($data);
} else {
Db::name('user_profile')->insert($data);
}
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace app\model;
use think\Model;
/**
* 微信小程序用户模型
*/
class WechatUser extends Model
{
protected $name = 'wechat_users';
protected $schema = [
'id' => 'int',
'openid' => 'string',
'unionid' => 'string',
'sessionKey' => 'string',
'nickname' => 'string',
'avatar' => 'string',
'phone' => 'string',
'gender' => 'int',
'country' => 'string',
'province' => 'string',
'city' => 'string',
'birthday' => 'string',
'status' => 'int',
'lastLoginAt' => 'int',
'lastLoginIp' => 'string',
'enterpriseId' => 'int',
'createdAt' => 'int',
'updatedAt' => 'int',
];
protected $autoWriteTimestamp = 'int';
protected $createTime = 'createdAt';
protected $updateTime = 'updatedAt';
protected $hidden = ['sessionKey', 'openid'];
protected $type = [
'lastLoginAt' => 'integer',
'createdAt' => 'integer',
'updatedAt' => 'integer',
];
/**
* 返回给前端的用户信息(不包含敏感字段)
*/
public function toApiArray(): array
{
$row = $this->toArray();
unset($row['sessionKey'], $row['openid']);
$row['avatarUrl'] = $row['avatar'] ?? '';
return $row;
}
}