1 Commits

Author SHA1 Message Date
Ghost
810eba0a63 代码优化 2026-05-11 09:39:32 +08:00
18 changed files with 598 additions and 158 deletions

View File

@@ -281,21 +281,71 @@ class Distribution extends BaseController
$now = time();
foreach ($rows as $r) {
$exp = isset($r['expiresAt']) ? (int) $r['expiresAt'] : 0;
if ($exp <= 0 || $exp > $now) {
$code = trim((string) ($r['code'] ?? ''));
if ($code !== '') {
return success([
'code' => strtoupper($code),
'createdNew' => false,
]);
if ($exp > 0 && $exp <= $now) {
continue;
}
$numericCol = trim((string) ($r['code_numeric'] ?? ''));
$codeCol = trim((string) ($r['code'] ?? ''));
if ($numericCol === '' && $codeCol !== '' && preg_match('/^[0-9]+$/', $codeCol)) {
$numericCol = $codeCol;
}
$replenishedNumeric = false;
// 仅有老版或未迁移字段时:强制补发数字码(与 code_legacy 并存)
if ($numericCol === '') {
for ($try = 0; $try < 8; $try++) {
try {
$newNumeric = $this->generateUniqueNumericInviteCode();
$rowId = (int) ($r['id'] ?? 0);
if ($rowId <= 0) {
break;
}
Db::name('invite_codes')->where('id', $rowId)->update([
'code_numeric' => $newNumeric,
'code' => $newNumeric,
'updatedAt' => $now,
]);
$r['code_numeric'] = $newNumeric;
$r['code'] = $newNumeric;
$replenishedNumeric = true;
break;
} catch (\Throwable $e) {
Log::warning('myInviteCode replenish numeric try ' . $try . ': ' . $e->getMessage());
}
}
}
$payload = $this->buildInviteCodeApiPayload($r);
if (($payload['code'] ?? '') === '') {
continue;
}
$numOut = $payload['numericCode'] ?? null;
if ($numOut === null || $numOut === '') {
Log::warning('myInviteCode numeric still missing after replenish', [
'inviterId' => $inviterId,
'rowId' => (int) ($r['id'] ?? 0),
]);
return error('数字邀请码生成失败,请稍后重试', 503);
}
$out = array_merge($payload, [
'createdNew' => false,
]);
if ($replenishedNumeric) {
$out['replenishedNumeric'] = true;
}
return success($out);
}
try {
$newCode = $this->generateUniqueInviteCode();
$pair = $this->generateUniqueInvitePair();
Db::name('invite_codes')->insert([
'code' => $newCode,
'code' => $pair['numeric'],
'code_numeric' => $pair['numeric'],
'code_legacy' => $pair['legacy'],
'inviterId' => $inviterId,
'enterpriseId' => null,
'status' => 'active',
@@ -308,18 +358,50 @@ class Distribution extends BaseController
Log::warning('myInviteCode insert: ' . $e->getMessage());
return success([
'code' => null,
'createdNew' => false,
'hint' => '暂时无法生成邀请码,请稍后重试或联系管理员',
'code' => null,
'numericCode' => null,
'legacyCode' => null,
'createdNew' => false,
'hint' => '暂时无法生成邀请码,请稍后重试或联系管理员',
]);
}
return success([
'code' => $newCode,
'createdNew' => true,
'code' => $pair['numeric'],
'numericCode' => $pair['numeric'],
'legacyCode' => $pair['legacy'],
'createdNew' => true,
]);
}
/**
* @param array<string,mixed> $row invite_codes 一行
* @return array{code:string,numericCode:?string,legacyCode:?string}
*/
private function buildInviteCodeApiPayload(array $row): array
{
$numeric = trim((string) ($row['code_numeric'] ?? ''));
$legacy = trim((string) ($row['code_legacy'] ?? ''));
$codeCol = trim((string) ($row['code'] ?? ''));
if ($numeric === '' && $codeCol !== '' && preg_match('/^[0-9]+$/', $codeCol)) {
$numeric = $codeCol;
}
if ($legacy === '' && $codeCol !== '' && !preg_match('/^[0-9]+$/', $codeCol)) {
$legacy = strtoupper($codeCol);
}
$numericOut = $numeric !== '' ? $numeric : null;
$legacyOut = $legacy !== '' ? strtoupper($legacy) : null;
$display = $numericOut ?? $legacyOut ?? '';
return [
'code' => $display,
'numericCode' => $numericOut,
'legacyCode' => $legacyOut,
];
}
/**
* 过期待收款提现status=2 超过24小时未确认收款的自动退回余额并标记为已过期
*/
@@ -1658,32 +1740,101 @@ class Distribution extends BaseController
}
/**
* 活动邀请码 → inviterId需表 invite_codes见 database/add_invite_codes.sql
*
* @param string $raw
* @param int|null $requestEnterpriseId 请求里已带的 eid用于与码上企业校验
* @return array{inviterId:int, enterpriseId:int|null}|null
* 判断邀请码字符串是否已被占用(数字码 / 老版码 / 兼容字段 code 任一命中即占用
*/
private function inviteCodeTokenTaken(string $token): bool
{
$token = trim($token);
if ($token === '') {
return true;
}
$norm = strtoupper(preg_replace('/[^A-Za-z0-9]/', '', $token));
if ($norm === '') {
return true;
}
try {
$row = Db::name('invite_codes')
->where(function ($query) use ($norm) {
$query->where('code_numeric', $norm)
->whereOr('code_legacy', $norm)
->whereOr('code', $norm);
})
->find();
} catch (\Throwable $e) {
Log::warning('inviteCodeTokenTaken: ' . $e->getMessage());
return true;
}
return (bool) $row;
}
/**
* 生成不与 invite_codes.code 冲突的随机码(易辨认字符集
* 数字邀请码6 位 1000009999997 位 10000009999999以此类推用尽则加长位数上限 12 位
*/
private function generateUniqueInviteCode(): string
private function generateUniqueNumericInviteCode(): string
{
$maxLen = 12;
for ($len = 6; $len <= $maxLen; $len++) {
$min = (int) pow(10, $len - 1);
$max = (int) pow(10, $len) - 1;
$attemptsPerLen = $len <= 7 ? 96 : 48;
for ($attempt = 0; $attempt < $attemptsPerLen; $attempt++) {
$code = (string) random_int($min, $max);
if (!$this->inviteCodeTokenTaken($code)) {
return $code;
}
}
}
throw new \RuntimeException('invite numeric code exhausted');
}
/**
* 老版邀请码8 位易辨认字母数字(与原逻辑一致),与数字码分列存储、均可绑定
*
* @param string $excludeNumeric 同一用户本次已生成的数字码,避免与老版随机串完全相同
*/
private function generateUniqueLegacyInviteCode(string $excludeNumeric): string
{
$chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
$max = strlen($chars) - 1;
for ($attempt = 0; $attempt < 24; $attempt++) {
for ($round = 0; $round < 64; $round++) {
$code = '';
for ($i = 0; $i < 8; $i++) {
$code .= $chars[random_int(0, $max)];
}
$exists = Db::name('invite_codes')->where('code', $code)->find();
if (!$exists) {
return $code;
$norm = strtoupper($code);
if ($excludeNumeric !== '' && $norm === $excludeNumeric) {
continue;
}
if (!$this->inviteCodeTokenTaken($norm)) {
return $norm;
}
}
throw new \RuntimeException('invite code collision');
throw new \RuntimeException('invite legacy code collision');
}
/**
* @return array{numeric:string,legacy:string}
*/
private function generateUniqueInvitePair(): array
{
$numeric = $this->generateUniqueNumericInviteCode();
return [
'numeric' => $numeric,
'legacy' => $this->generateUniqueLegacyInviteCode($numeric),
];
}
/**
* 活动邀请码 → inviterId需表 invite_codes见 database/add_invite_codes.sql
* 支持填写 code_numeric、code_legacy 或兼容字段 code
*
* @return array{inviterId:int, enterpriseId:int|null}|null
*/
private function resolveInviterFromInviteCode(string $raw, ?int $requestEnterpriseId): ?array
{
$norm = strtoupper(preg_replace('/[^A-Za-z0-9]/', '', $raw));
@@ -1692,8 +1843,12 @@ class Distribution extends BaseController
}
try {
$row = Db::name('invite_codes')
->where('code', $norm)
->where('status', 'active')
->where(function ($query) use ($norm) {
$query->where('code_numeric', $norm)
->whereOr('code_legacy', $norm)
->whereOr('code', $norm);
})
->find();
} catch (\Throwable $e) {
Log::warning('invite_codes lookup failed: ' . $e->getMessage());

View File

@@ -3,7 +3,9 @@
CREATE TABLE IF NOT EXISTS `mbti_invite_codes` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`code` varchar(64) NOT NULL COMMENT '邀请码(仅存大写字母数字)',
`code` varchar(64) NULL DEFAULT NULL COMMENT '兼容旧字段,可与 code_numeric 同步',
`code_numeric` varchar(20) NULL DEFAULT NULL COMMENT '数字邀请码,如 6 位 100000-999999',
`code_legacy` varchar(64) NULL DEFAULT NULL COMMENT '老版字母数字邀请码,与数字码同一 inviter 可并存',
`inviterId` int unsigned NOT NULL COMMENT '推广员 wechat_users.id',
`enterpriseId` int unsigned NULL DEFAULT NULL COMMENT '企业上下文,与个人版并行时可空',
`status` varchar(20) NOT NULL DEFAULT 'active' COMMENT 'active|disabled',
@@ -12,6 +14,7 @@ CREATE TABLE IF NOT EXISTS `mbti_invite_codes` (
`createdAt` int unsigned NULL DEFAULT NULL,
`updatedAt` int unsigned NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_code` (`code`),
UNIQUE KEY `uk_code_numeric` (`code_numeric`),
UNIQUE KEY `uk_code_legacy` (`code_legacy`),
KEY `idx_inviterId` (`inviterId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='小程序填写邀请码解析用';

View File

@@ -0,0 +1,31 @@
-- 邀请码拆分为:数字码 code_numeric、老版码 code_legacy两列均可用于填写解析
-- 表名前缀与 api/config/database.php 中 prefix 对齐(默认 mbti_
-- 执行前请确认表名,如为 invite_codes 则把 mbti_invite_codes 替换为 {prefix}invite_codes
-- 1) 新列
ALTER TABLE `mbti_invite_codes`
ADD COLUMN `code_numeric` varchar(20) NULL DEFAULT NULL COMMENT '数字邀请码(如 100000-999999)' AFTER `code`,
ADD COLUMN `code_legacy` varchar(64) NULL DEFAULT NULL COMMENT '老版字母数字邀请码' AFTER `code_numeric`;
-- 2) 回填:原 code 视作老版展示码(用户填写的多为旧格式)
UPDATE `mbti_invite_codes`
SET `code_legacy` = UPPER(TRIM(`code`))
WHERE (`code_legacy` IS NULL OR `code_legacy` = '')
AND `code` IS NOT NULL AND TRIM(`code`) <> '';
-- 3) 若历史已是纯数字仅存于 code写入 code_numeric
UPDATE `mbti_invite_codes`
SET `code_numeric` = TRIM(`code`)
WHERE TRIM(`code`) REGEXP '^[0-9]+$'
AND (`code_numeric` IS NULL OR `code_numeric` = '');
-- 4) 去掉 code 唯一约束,改为数字码/老版码分别唯一(允许多列 NULL
ALTER TABLE `mbti_invite_codes` DROP INDEX `uk_code`;
ALTER TABLE `mbti_invite_codes`
ADD UNIQUE KEY `uk_code_numeric` (`code_numeric`),
ADD UNIQUE KEY `uk_code_legacy` (`code_legacy`);
-- 5) code 改为可空,仅作兼容旧读取
ALTER TABLE `mbti_invite_codes`
MODIFY `code` varchar(64) NULL DEFAULT NULL COMMENT '兼容旧字段,与新码同步时可等于数字码';

View File

@@ -6,7 +6,7 @@
class="icd-input"
type="text"
maxlength="{{maxLen}}"
placeholder="字母数字"
placeholder="100000-999999 数字码,或老版字母数字"
placeholder-class="icd-ph"
value="{{code}}"
bindinput="onInput"

View File

@@ -13,7 +13,9 @@ Page({
maintenanceMode: false,
reviewMode: false,
permFace: true,
showVersionPicker: false
showVersionPicker: false,
versionPickerCurrent: 'enterprise',
versionPickerCurrentLabel: '企业版'
},
_navigateByVersion(version) {
try {
@@ -32,7 +34,11 @@ Page({
},
openVersionPicker() {
this.setData({ showVersionPicker: true })
this.setData({
showVersionPicker: true,
versionPickerCurrent: 'enterprise',
versionPickerCurrentLabel: '企业版'
})
},
closeVersionPicker() {

View File

@@ -5,7 +5,7 @@
<view class="navbar-content">
<view class="switch-personal-btn" bindtap="switchToPersonal">
<text class="personal-icon">👤</text>
<text class="personal-text">个人版</text>
<text class="personal-text">企业版</text>
</view>
<view class="navbar-title">{{siteTitle || '神仙团队性格测试'}}</view>
<view class="navbar-placeholder"></view>
@@ -82,14 +82,12 @@
<view class="version-picker-close" bindtap="closeVersionPicker">×</view>
</view>
<view class="version-picker-title">请选择使用版本</view>
<view class="version-picker-current">当前入口:<text class="version-picker-current-em">{{versionPickerCurrentLabel}}</text></view>
<view class="version-picker-desc">当前企业支持三种版本,选择后将进入对应流程</view>
<view class="version-picker-actions">
<button class="version-btn version-btn-personal" data-version="personal" bindtap="selectVersion">个人版</button>
<button class="version-btn version-btn-enterprise" data-version="enterprise" bindtap="selectVersion">企业版</button>
<button class="version-btn version-btn-gaokao" data-version="gaokao" bindtap="selectVersion">高考版</button>
</view>
<view class="version-picker-foot">
<button class="version-cancel-btn" bindtap="closeVersionPicker">取消</button>
<button class="version-btn version-btn-personal {{versionPickerCurrent === 'personal' ? 'version-btn-is-current' : ''}}" data-version="personal" bindtap="selectVersion">个人版</button>
<button class="version-btn version-btn-enterprise {{versionPickerCurrent === 'enterprise' ? 'version-btn-is-current' : ''}}" data-version="enterprise" bindtap="selectVersion">企业版</button>
<button class="version-btn version-btn-gaokao {{versionPickerCurrent === 'gaokao' ? 'version-btn-is-current' : ''}}" data-version="gaokao" bindtap="selectVersion">高考版</button>
</view>
</view>
</view>

View File

@@ -98,17 +98,19 @@
.top-image-section {
width: 100%;
padding: 15rpx 40rpx 20rpx;
padding: 8rpx 40rpx 10rpx;
flex-shrink: 0;
box-sizing: border-box;
position: relative;
z-index: 1;
margin-top: 0;
margin-top: -20rpx;
}
.image-container {
position: relative;
width: 100%;
transform: translateY(-26rpx);
margin-bottom: -26rpx;
}
.image-wrapper {
@@ -163,7 +165,7 @@
}
.process-section {
padding: 15rpx 40rpx 20rpx;
padding: 8rpx 40rpx 20rpx;
flex-shrink: 0;
position: relative;
z-index: 1;
@@ -249,7 +251,7 @@
.version-picker-mask {
position: fixed;
inset: 0;
background: rgba(10, 15, 30, 0.52);
background: rgba(10, 15, 30, 0.44);
z-index: 20000;
display: flex;
align-items: center;
@@ -260,11 +262,12 @@
.version-picker-dialog {
width: 100%;
max-width: 640rpx;
background: #fff;
border-radius: 30rpx;
padding: 26rpx 30rpx 24rpx;
box-shadow: 0 24rpx 70rpx rgba(16, 24, 40, 0.28);
max-width: 620rpx;
background: #ffffff;
border-radius: 36rpx;
padding: 30rpx 30rpx 30rpx;
border: 1rpx solid rgba(99, 102, 241, 0.12);
box-shadow: 0 30rpx 80rpx rgba(15, 23, 42, 0.22);
}
.version-picker-head {
@@ -274,59 +277,96 @@
}
.version-picker-badge {
padding: 8rpx 18rpx;
padding: 8rpx 20rpx;
border-radius: 999rpx;
background: linear-gradient(135deg, rgba(79, 70, 229, 0.12) 0%, rgba(124, 58, 237, 0.16) 100%);
color: #4f46e5;
font-size: 22rpx;
font-weight: 600;
font-weight: 700;
}
.version-picker-close {
width: 48rpx;
height: 48rpx;
width: 52rpx;
height: 52rpx;
border-radius: 50%;
background: #f3f4f6;
background: #f1f5f9;
color: #6b7280;
display: flex;
align-items: center;
justify-content: center;
font-size: 34rpx;
font-size: 32rpx;
line-height: 1;
}
.version-picker-title {
text-align: center;
font-size: 36rpx;
font-size: 40rpx;
font-weight: 700;
color: #111827;
margin-top: 12rpx;
margin-top: 18rpx;
line-height: 1.25;
}
.version-picker-current {
margin-top: 16rpx;
text-align: center;
font-size: 24rpx;
color: #475569;
background: #f8fafc;
border-radius: 999rpx;
padding: 10rpx 18rpx;
}
.version-picker-current-em {
color: #4f46e5;
font-weight: 700;
}
.version-picker-desc {
margin-top: 12rpx;
margin-top: 14rpx;
text-align: center;
font-size: 24rpx;
font-size: 23rpx;
color: #6b7280;
line-height: 1.6;
line-height: 1.55;
}
.version-picker-actions {
margin-top: 28rpx;
margin-top: 24rpx;
display: flex;
flex-direction: column;
gap: 16rpx;
gap: 14rpx;
}
.version-btn::after,
.version-cancel-btn::after {
border: none;
}
.version-btn,
.version-cancel-btn {
display: flex;
align-items: center;
justify-content: center;
padding: 0;
margin: 0;
line-height: 1.25;
box-sizing: border-box;
}
.version-btn {
height: 84rpx;
line-height: 84rpx;
height: 82rpx;
width: 100%;
border-radius: 999rpx;
color: #fff;
font-size: 29rpx;
font-size: 30rpx;
font-weight: 600;
border: none;
box-shadow: 0 8rpx 22rpx rgba(79, 70, 229, 0.22);
box-shadow: 0 10rpx 20rpx rgba(79, 70, 229, 0.2);
}
.version-btn-is-current {
transform: translateY(-1rpx);
box-shadow: 0 0 0 4rpx #ffffff, 0 0 0 6rpx rgba(79, 70, 229, 0.45), 0 12rpx 24rpx rgba(79, 70, 229, 0.24);
}
.version-btn-personal { background: linear-gradient(135deg, #ef4444 0%, #f97316 100%); }
@@ -339,7 +379,6 @@
.version-cancel-btn {
height: 76rpx;
line-height: 76rpx;
border-radius: 999rpx;
background: #f3f4f6;
color: #6b7280;

View File

@@ -10,7 +10,9 @@ Page({
maintenanceMode: false,
reviewMode: false,
permFace: true,
showVersionPicker: false
showVersionPicker: false,
versionPickerCurrent: 'gaokao',
versionPickerCurrentLabel: '高考版'
},
_navigateByVersion(version) {
@@ -30,7 +32,11 @@ Page({
},
openVersionPicker() {
this.setData({ showVersionPicker: true })
this.setData({
showVersionPicker: true,
versionPickerCurrent: 'gaokao',
versionPickerCurrentLabel: '高考版'
})
},
closeVersionPicker() {

View File

@@ -84,14 +84,12 @@
<view class="version-picker-close" bindtap="closeVersionPicker">×</view>
</view>
<view class="version-picker-title">请选择使用版本</view>
<view class="version-picker-current">当前入口:<text class="version-picker-current-em">{{versionPickerCurrentLabel}}</text></view>
<view class="version-picker-desc">当前企业支持三种版本,选择后将进入对应流程</view>
<view class="version-picker-actions">
<button class="version-btn version-btn-personal" data-version="personal" bindtap="selectVersion">个人版</button>
<button class="version-btn version-btn-enterprise" data-version="enterprise" bindtap="selectVersion">企业版</button>
<button class="version-btn version-btn-gaokao" data-version="gaokao" bindtap="selectVersion">高考版</button>
</view>
<view class="version-picker-foot">
<button class="version-cancel-btn" bindtap="closeVersionPicker">取消</button>
<button class="version-btn version-btn-personal {{versionPickerCurrent === 'personal' ? 'version-btn-is-current' : ''}}" data-version="personal" bindtap="selectVersion">个人版</button>
<button class="version-btn version-btn-enterprise {{versionPickerCurrent === 'enterprise' ? 'version-btn-is-current' : ''}}" data-version="enterprise" bindtap="selectVersion">企业版</button>
<button class="version-btn version-btn-gaokao {{versionPickerCurrent === 'gaokao' ? 'version-btn-is-current' : ''}}" data-version="gaokao" bindtap="selectVersion">高考版</button>
</view>
</view>
</view>

View File

@@ -95,17 +95,19 @@
.top-image-section {
width: 100%;
padding: 15rpx 40rpx 20rpx;
padding: 8rpx 40rpx 10rpx;
flex-shrink: 0;
box-sizing: border-box;
position: relative;
z-index: 1;
margin-top: 0;
margin-top: -20rpx;
}
.image-container {
position: relative;
width: 100%;
transform: translateY(-26rpx);
margin-bottom: -26rpx;
}
.image-wrapper {
@@ -160,7 +162,7 @@
}
.process-section {
padding: 15rpx 40rpx 20rpx;
padding: 8rpx 40rpx 20rpx;
flex-shrink: 0;
position: relative;
z-index: 1;
@@ -273,7 +275,7 @@
.version-picker-mask {
position: fixed;
inset: 0;
background: rgba(10, 15, 30, 0.52);
background: rgba(10, 15, 30, 0.44);
z-index: 20000;
display: flex;
align-items: center;
@@ -284,11 +286,12 @@
.version-picker-dialog {
width: 100%;
max-width: 640rpx;
background: #fff;
border-radius: 30rpx;
padding: 26rpx 30rpx 24rpx;
box-shadow: 0 24rpx 70rpx rgba(16, 24, 40, 0.28);
max-width: 620rpx;
background: #ffffff;
border-radius: 36rpx;
padding: 30rpx 30rpx 30rpx;
border: 1rpx solid rgba(99, 102, 241, 0.12);
box-shadow: 0 30rpx 80rpx rgba(15, 23, 42, 0.22);
}
.version-picker-head {
@@ -298,64 +301,101 @@
}
.version-picker-badge {
padding: 8rpx 18rpx;
padding: 8rpx 20rpx;
border-radius: 999rpx;
background: #e8f0ff;
color: #1d4ed8;
background: linear-gradient(135deg, rgba(79, 70, 229, 0.12) 0%, rgba(124, 58, 237, 0.16) 100%);
color: #4f46e5;
font-size: 22rpx;
font-weight: 600;
font-weight: 700;
}
.version-picker-close {
width: 48rpx;
height: 48rpx;
width: 52rpx;
height: 52rpx;
border-radius: 50%;
background: #f3f4f6;
background: #f1f5f9;
color: #6b7280;
display: flex;
align-items: center;
justify-content: center;
font-size: 34rpx;
font-size: 32rpx;
line-height: 1;
}
.version-picker-title {
text-align: center;
font-size: 36rpx;
font-size: 40rpx;
font-weight: 700;
color: #111827;
margin-top: 12rpx;
margin-top: 18rpx;
line-height: 1.25;
}
.version-picker-current {
margin-top: 16rpx;
text-align: center;
font-size: 24rpx;
color: #475569;
background: #f8fafc;
border-radius: 999rpx;
padding: 10rpx 18rpx;
}
.version-picker-current-em {
color: #4f46e5;
font-weight: 700;
}
.version-picker-desc {
margin-top: 12rpx;
margin-top: 14rpx;
text-align: center;
font-size: 24rpx;
font-size: 23rpx;
color: #6b7280;
line-height: 1.6;
line-height: 1.55;
}
.version-picker-actions {
margin-top: 28rpx;
margin-top: 24rpx;
display: flex;
flex-direction: column;
gap: 16rpx;
gap: 14rpx;
}
.version-btn::after,
.version-cancel-btn::after {
border: none;
}
.version-btn,
.version-cancel-btn {
display: flex;
align-items: center;
justify-content: center;
padding: 0;
margin: 0;
line-height: 1.25;
box-sizing: border-box;
}
.version-btn {
height: 84rpx;
line-height: 84rpx;
height: 82rpx;
width: 100%;
border-radius: 999rpx;
color: #fff;
font-size: 29rpx;
font-size: 30rpx;
font-weight: 600;
border: none;
box-shadow: 0 6rpx 16rpx rgba(37, 99, 235, 0.16);
box-shadow: 0 10rpx 20rpx rgba(79, 70, 229, 0.2);
}
.version-btn-personal { background: #2563eb; }
.version-btn-enterprise { background: #2563eb; }
.version-btn-gaokao { background: #2563eb; }
.version-btn-is-current {
transform: translateY(-1rpx);
box-shadow: 0 0 0 4rpx #ffffff, 0 0 0 6rpx rgba(79, 70, 229, 0.45), 0 12rpx 24rpx rgba(79, 70, 229, 0.24);
}
.version-btn-personal { background: linear-gradient(135deg, #ef4444 0%, #f97316 100%); }
.version-btn-enterprise { background: linear-gradient(135deg, #2563eb 0%, #6366f1 100%); }
.version-btn-gaokao { background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%); }
.version-picker-foot {
margin-top: 14rpx;
@@ -363,7 +403,6 @@
.version-cancel-btn {
height: 76rpx;
line-height: 76rpx;
border-radius: 999rpx;
background: #f3f4f6;
color: #4b5563;

View File

@@ -12,7 +12,9 @@ Page({
aiAnalysisText: '分析',
reviewMode: true,
permFace: true,
showVersionPicker: false
showVersionPicker: false,
versionPickerCurrent: 'personal',
versionPickerCurrentLabel: '个人版'
},
onLoad(options) {
@@ -206,7 +208,11 @@ Page({
},
openVersionPicker() {
this.setData({ showVersionPicker: true })
this.setData({
showVersionPicker: true,
versionPickerCurrent: 'personal',
versionPickerCurrentLabel: '个人版'
})
},
// 切换版本2 个版本沿用原逻辑3 个版本弹出居中选择框

View File

@@ -5,7 +5,7 @@
<view class="navbar-content">
<view class="switch-enterprise-btn" wx:if="{{showEnterpriseEntry && !reviewMode}}" bindtap="switchToEnterprise">
<text class="enterprise-icon">🏢</text>
<text class="enterprise-text">企业版</text>
<text class="enterprise-text">个人版</text>
</view>
<view class="navbar-title">{{siteTitle || '神仙团队性格测试'}}</view>
<view class="navbar-placeholder"></view>
@@ -84,14 +84,12 @@
<view class="version-picker-close" bindtap="closeVersionPicker">×</view>
</view>
<view class="version-picker-title">请选择使用版本</view>
<view class="version-picker-current">当前入口:<text class="version-picker-current-em">{{versionPickerCurrentLabel}}</text></view>
<view class="version-picker-desc">当前企业支持三种版本,选择后将进入对应流程</view>
<view class="version-picker-actions">
<button class="version-btn version-btn-personal" data-version="personal" bindtap="selectVersion">个人版</button>
<button class="version-btn version-btn-enterprise" data-version="enterprise" bindtap="selectVersion">企业版</button>
<button class="version-btn version-btn-gaokao" data-version="gaokao" bindtap="selectVersion">高考版</button>
</view>
<view class="version-picker-foot">
<button class="version-cancel-btn" bindtap="closeVersionPicker">取消</button>
<button class="version-btn version-btn-personal {{versionPickerCurrent === 'personal' ? 'version-btn-is-current' : ''}}" data-version="personal" bindtap="selectVersion">个人版</button>
<button class="version-btn version-btn-enterprise {{versionPickerCurrent === 'enterprise' ? 'version-btn-is-current' : ''}}" data-version="enterprise" bindtap="selectVersion">企业版</button>
<button class="version-btn version-btn-gaokao {{versionPickerCurrent === 'gaokao' ? 'version-btn-is-current' : ''}}" data-version="gaokao" bindtap="selectVersion">高考版</button>
</view>
</view>
</view>

View File

@@ -254,7 +254,7 @@
.version-picker-mask {
position: fixed;
inset: 0;
background: rgba(10, 15, 30, 0.52);
background: rgba(10, 15, 30, 0.44);
z-index: 20000;
display: flex;
align-items: center;
@@ -265,11 +265,12 @@
.version-picker-dialog {
width: 100%;
max-width: 640rpx;
background: #fff;
border-radius: 30rpx;
padding: 26rpx 30rpx 24rpx;
box-shadow: 0 24rpx 70rpx rgba(16, 24, 40, 0.28);
max-width: 620rpx;
background: #ffffff;
border-radius: 36rpx;
padding: 30rpx 30rpx 30rpx;
border: 1rpx solid rgba(99, 102, 241, 0.12);
box-shadow: 0 30rpx 80rpx rgba(15, 23, 42, 0.22);
}
.version-picker-head {
@@ -279,59 +280,96 @@
}
.version-picker-badge {
padding: 8rpx 18rpx;
padding: 8rpx 20rpx;
border-radius: 999rpx;
background: linear-gradient(135deg, rgba(79, 70, 229, 0.12) 0%, rgba(124, 58, 237, 0.16) 100%);
color: #4f46e5;
font-size: 22rpx;
font-weight: 600;
font-weight: 700;
}
.version-picker-close {
width: 48rpx;
height: 48rpx;
width: 52rpx;
height: 52rpx;
border-radius: 50%;
background: #f3f4f6;
background: #f1f5f9;
color: #6b7280;
display: flex;
align-items: center;
justify-content: center;
font-size: 34rpx;
font-size: 32rpx;
line-height: 1;
}
.version-picker-title {
text-align: center;
font-size: 36rpx;
font-size: 40rpx;
font-weight: 700;
color: #111827;
margin-top: 12rpx;
margin-top: 18rpx;
line-height: 1.25;
}
.version-picker-current {
margin-top: 16rpx;
text-align: center;
font-size: 24rpx;
color: #475569;
background: #f8fafc;
border-radius: 999rpx;
padding: 10rpx 18rpx;
}
.version-picker-current-em {
color: #4f46e5;
font-weight: 700;
}
.version-picker-desc {
margin-top: 12rpx;
margin-top: 14rpx;
text-align: center;
font-size: 24rpx;
font-size: 23rpx;
color: #6b7280;
line-height: 1.6;
line-height: 1.55;
}
.version-picker-actions {
margin-top: 28rpx;
margin-top: 24rpx;
display: flex;
flex-direction: column;
gap: 16rpx;
gap: 14rpx;
}
.version-btn::after,
.version-cancel-btn::after {
border: none;
}
.version-btn,
.version-cancel-btn {
display: flex;
align-items: center;
justify-content: center;
padding: 0;
margin: 0;
line-height: 1.25;
box-sizing: border-box;
}
.version-btn {
height: 84rpx;
line-height: 84rpx;
height: 82rpx;
width: 100%;
border-radius: 999rpx;
color: #fff;
font-size: 29rpx;
font-size: 30rpx;
font-weight: 600;
border: none;
box-shadow: 0 8rpx 22rpx rgba(79, 70, 229, 0.22);
box-shadow: 0 10rpx 20rpx rgba(79, 70, 229, 0.2);
}
.version-btn-is-current {
transform: translateY(-1rpx);
box-shadow: 0 0 0 4rpx #ffffff, 0 0 0 6rpx rgba(79, 70, 229, 0.45), 0 12rpx 24rpx rgba(79, 70, 229, 0.24);
}
.version-btn-personal { background: linear-gradient(135deg, #ef4444 0%, #f97316 100%); }
@@ -344,7 +382,6 @@
.version-cancel-btn {
height: 76rpx;
line-height: 76rpx;
border-radius: 999rpx;
background: #f3f4f6;
color: #6b7280;

View File

@@ -87,6 +87,8 @@ Page({
profileRecoJump: null,
/** 拉取自 /api/distribution/my-invite-code与推广中心一致 */
myInviteCode: '',
myInviteCodeNumeric: '',
myInviteCodeLegacy: '',
myInviteCodeLoadState: 'idle',
myInviteCodeErrMsg: '',
myInviteCodeHint: '',
@@ -259,6 +261,8 @@ Page({
profileRecoSectionLabel: '',
profileRecoJump: null,
myInviteCode: '',
myInviteCodeNumeric: '',
myInviteCodeLegacy: '',
myInviteCodeLoadState: 'idle',
myInviteCodeErrMsg: '',
myInviteCodeHint: '',
@@ -290,6 +294,8 @@ Page({
if (!token || !this.data.permDistribution) {
this.setData({
myInviteCode: '',
myInviteCodeNumeric: '',
myInviteCodeLegacy: '',
myInviteCodeLoadState: 'idle',
myInviteCodeErrMsg: '',
myInviteCodeHint: ''
@@ -313,6 +319,8 @@ Page({
(typeof res.statusCode === 'number' ? `请求失败 (${res.statusCode})` : '加载失败')
this.setData({
myInviteCode: '',
myInviteCodeNumeric: '',
myInviteCodeLegacy: '',
myInviteCodeLoadState: 'error',
myInviteCodeErrMsg: msg,
myInviteCodeHint: ''
@@ -321,11 +329,22 @@ Page({
}
const data = payload.data || {}
const raw = data.code != null ? String(data.code).trim() : ''
const code = raw ? raw.toUpperCase() : ''
const code = raw ? (/^[0-9]+$/.test(raw) ? raw : raw.toUpperCase()) : ''
const numRaw =
data.numericCode != null && String(data.numericCode).trim() !== ''
? String(data.numericCode).trim()
: ''
const legRaw =
data.legacyCode != null && String(data.legacyCode).trim() !== ''
? String(data.legacyCode).trim().toUpperCase()
: ''
const hint = (data.hint && String(data.hint).trim()) || ''
if (code) {
if (code || numRaw || legRaw) {
this.setData({
myInviteCode: code,
/* 默认展示:数字码优先 */
myInviteCode: numRaw || legRaw || code,
myInviteCodeNumeric: numRaw,
myInviteCodeLegacy: legRaw,
myInviteCodeLoadState: 'ready',
myInviteCodeErrMsg: '',
myInviteCodeHint: ''
@@ -333,6 +352,8 @@ Page({
} else {
this.setData({
myInviteCode: '',
myInviteCodeNumeric: '',
myInviteCodeLegacy: '',
myInviteCodeLoadState: 'nodata',
myInviteCodeErrMsg: '',
myInviteCodeHint: hint || '暂无可用邀请码'
@@ -342,6 +363,8 @@ Page({
fail: () =>
this.setData({
myInviteCode: '',
myInviteCodeNumeric: '',
myInviteCodeLegacy: '',
myInviteCodeLoadState: 'error',
myInviteCodeErrMsg: '网络异常',
myInviteCodeHint: ''
@@ -358,11 +381,13 @@ Page({
},
copyMyInviteCode() {
const code = (this.data.myInviteCode || '').trim()
if (!code) return
const n = (this.data.myInviteCodeNumeric || '').trim()
const l = (this.data.myInviteCodeLegacy || '').trim()
const text = n || l || (this.data.myInviteCode || '').trim()
if (!text) return
wx.setClipboardData({
data: code,
success: () => wx.showToast({ title: '已复制邀请码', icon: 'success' })
data: text,
success: () => wx.showToast({ title: n ? '已复制数字码' : '已复制邀请码', icon: 'success' })
})
},

View File

@@ -105,7 +105,7 @@
<text class="invite-strip__label">我的邀请码</text>
<text wx:if="{{myInviteCodeLoadState === 'loading'}}" class="invite-strip__muted">加载中…</text>
<text wx:elif="{{myInviteCodeLoadState === 'error'}}" class="invite-strip__err">{{myInviteCodeErrMsg}}</text>
<text wx:elif="{{myInviteCodeLoadState === 'ready' && myInviteCode}}" class="invite-strip__code">{{myInviteCode}}</text>
<text wx:elif="{{myInviteCodeLoadState === 'ready' && (myInviteCodeNumeric || myInviteCode)}}" class="invite-strip__code">{{myInviteCodeNumeric || myInviteCode}}</text>
<text wx:else class="invite-strip__err">{{myInviteCodeHint}}</text>
</view>
<view class="invite-strip__side">

View File

@@ -34,6 +34,9 @@ Page({
withdrawFeeYuan: '0.00',
withdrawActualYuan: '0.00',
myInviteCode: '',
/** 与接口 numericCode / legacyCode 对应,便于分开展示与复制 */
myInviteCodeNumeric: '',
myInviteCodeLegacy: '',
myInviteCodeMeta: null,
/** loading | ready | error | nodata — 避免因接口失败整块不渲染 */
myInviteCodeLoadState: 'loading',
@@ -91,6 +94,8 @@ Page({
(typeof res.statusCode === 'number' ? `请求失败 (${res.statusCode})` : '加载失败')
this.setData({
myInviteCode: '',
myInviteCodeNumeric: '',
myInviteCodeLegacy: '',
myInviteCodeMeta: null,
myInviteCodeLoadState: 'error',
myInviteCodeErrMsg: msg,
@@ -100,11 +105,22 @@ Page({
}
const data = payload.data || {}
const raw = data.code != null ? String(data.code).trim() : ''
const code = raw ? raw.toUpperCase() : ''
const code = raw ? (/^[0-9]+$/.test(raw) ? raw : raw.toUpperCase()) : ''
const numRaw =
data.numericCode != null && String(data.numericCode).trim() !== ''
? String(data.numericCode).trim()
: ''
const legRaw =
data.legacyCode != null && String(data.legacyCode).trim() !== ''
? String(data.legacyCode).trim().toUpperCase()
: ''
const hint = (data.hint && String(data.hint).trim()) || ''
if (code) {
if (code || numRaw || legRaw) {
this.setData({
myInviteCode: code,
/* 默认展示与复制优先级:数字码 > 老版 > 接口 code */
myInviteCode: numRaw || legRaw || code,
myInviteCodeNumeric: numRaw,
myInviteCodeLegacy: legRaw,
myInviteCodeMeta: data,
myInviteCodeLoadState: 'ready',
myInviteCodeErrMsg: '',
@@ -113,6 +129,8 @@ Page({
} else {
this.setData({
myInviteCode: '',
myInviteCodeNumeric: '',
myInviteCodeLegacy: '',
myInviteCodeMeta: data,
myInviteCodeLoadState: 'nodata',
myInviteCodeErrMsg: '',
@@ -123,6 +141,8 @@ Page({
fail: () => {
this.setData({
myInviteCode: '',
myInviteCodeNumeric: '',
myInviteCodeLegacy: '',
myInviteCodeMeta: null,
myInviteCodeLoadState: 'error',
myInviteCodeErrMsg: '网络异常,请稍后重试',
@@ -137,11 +157,31 @@ Page({
},
copyMyInviteCode() {
const code = (this.data.myInviteCode || '').trim()
if (!code) return
const n = (this.data.myInviteCodeNumeric || '').trim()
const l = (this.data.myInviteCodeLegacy || '').trim()
const text = n || l || (this.data.myInviteCode || '').trim()
if (!text) return
wx.setClipboardData({
data: code,
success: () => wx.showToast({ title: '已复制邀请码', icon: 'success' })
data: text,
success: () => wx.showToast({ title: n ? '已复制数字码' : '已复制邀请码', icon: 'success' })
})
},
copyMyInviteCodeNumeric() {
const n = (this.data.myInviteCodeNumeric || '').trim()
if (!n) return
wx.setClipboardData({
data: n,
success: () => wx.showToast({ title: '已复制数字码', icon: 'success' })
})
},
copyMyInviteCodeLegacy() {
const l = (this.data.myInviteCodeLegacy || '').trim()
if (!l) return
wx.setClipboardData({
data: l,
success: () => wx.showToast({ title: '已复制老版码', icon: 'success' })
})
},

View File

@@ -35,7 +35,21 @@
<view class="my-invite-card">
<view class="my-invite-card__head">
<text class="my-invite-card__title">我的邀请码</text>
<view wx:if="{{myInviteCodeLoadState === 'ready' && myInviteCode}}" class="my-invite-card__copy" bindtap="copyMyInviteCode">复制</view>
<view
wx:if="{{myInviteCodeLoadState === 'ready' && myInviteCodeNumeric}}"
class="my-invite-card__copy"
bindtap="copyMyInviteCodeNumeric"
>复制邀请码</view>
<view
wx:elif="{{myInviteCodeLoadState === 'ready' && !myInviteCodeNumeric && myInviteCodeLegacy}}"
class="my-invite-card__copy"
bindtap="copyMyInviteCodeLegacy"
>复制老版码</view>
<view
wx:elif="{{myInviteCodeLoadState === 'ready' && myInviteCode}}"
class="my-invite-card__copy"
bindtap="copyMyInviteCode"
>复制</view>
<view wx:elif="{{myInviteCodeLoadState === 'error'}}" class="my-invite-card__retry" bindtap="retryMyInviteCode">重试</view>
</view>
<block wx:if="{{myInviteCodeLoadState === 'loading'}}">
@@ -45,9 +59,13 @@
<text class="my-invite-card__err">{{myInviteCodeErrMsg}}</text>
<text class="my-invite-card__tip">若提示功能未就绪,请在数据库执行 api/database/add_invite_codes.sql 建表后再试。</text>
</block>
<block wx:elif="{{myInviteCodeLoadState === 'ready' && myInviteCode}}">
<text class="my-invite-card__code" user-select="true">{{myInviteCode}}</text>
<text class="my-invite-card__tip">好友在付费解锁前填写此码,即可与你建立推广绑定</text>
<block wx:elif="{{myInviteCodeLoadState === 'ready' && (myInviteCode || myInviteCodeNumeric || myInviteCodeLegacy)}}">
<view wx:if="{{myInviteCodeNumeric}}" class="my-invite-card__numeric-hero">
<text class="my-invite-card__numeric-caption">我的邀请码</text>
<text class="my-invite-card__code" user-select="true">{{myInviteCodeNumeric}}</text>
</view>
<text wx:if="{{!myInviteCodeNumeric && myInviteCode}}" class="my-invite-card__code" user-select="true">{{myInviteCode}}</text>
<text class="my-invite-card__tip">好友在付费解锁前填写此邀请码,即可与你建立推广绑定。</text>
</block>
<block wx:else>
<text class="my-invite-card__err">{{myInviteCodeHint}}</text>

View File

@@ -47,6 +47,47 @@ page {
background: #fff1f2;
border-radius: 999rpx;
}
.my-invite-card__dual {
margin-bottom: 22rpx;
}
.my-invite-card__dual:last-of-type {
margin-bottom: 14rpx;
}
.my-invite-card__dual-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10rpx;
}
.my-invite-card__dual-label {
font-size: 24rpx;
font-weight: 600;
color: #64748b;
}
.my-invite-card__copy-sm {
font-size: 22rpx;
font-weight: 600;
color: #e11d48;
padding: 6rpx 18rpx;
background: #fff1f2;
border-radius: 999rpx;
}
.my-invite-card__dual-code {
display: block;
font-size: 34rpx;
font-weight: 800;
letter-spacing: 0.08em;
color: #0f172a;
}
.my-invite-card__numeric-hero {
margin-bottom: 8rpx;
}
.my-invite-card__numeric-caption {
display: block;
font-size: 22rpx;
color: #64748b;
margin-bottom: 10rpx;
}
.my-invite-card__code {
display: block;
font-size: 40rpx;