From 810eba0a639356a68634d22499befe829e0a101d Mon Sep 17 00:00:00 2001 From: Ghost <106998207@qq.com> Date: Mon, 11 May 2026 09:39:32 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BB=A3=E7=A0=81=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/app/controller/api/Distribution.php | 209 +++++++++++++++--- api/database/add_invite_codes.sql | 7 +- .../add_invite_codes_numeric_and_legacy.sql | 31 +++ .../invite-code-dialog.wxml | 2 +- miniprogram/pages/enterprise/index.js | 10 +- miniprogram/pages/enterprise/index.wxml | 12 +- miniprogram/pages/enterprise/index.wxss | 93 +++++--- miniprogram/pages/gaokao/index.js | 10 +- miniprogram/pages/gaokao/index.wxml | 10 +- miniprogram/pages/gaokao/index.wxss | 103 ++++++--- miniprogram/pages/index/index.js | 10 +- miniprogram/pages/index/index.wxml | 12 +- miniprogram/pages/index/index.wxss | 85 +++++-- miniprogram/pages/profile/index.js | 39 +++- miniprogram/pages/profile/index.wxml | 2 +- miniprogram/pages/promo/index.js | 54 ++++- miniprogram/pages/promo/index.wxml | 26 ++- miniprogram/pages/promo/index.wxss | 41 ++++ 18 files changed, 598 insertions(+), 158 deletions(-) create mode 100644 api/database/migrations/add_invite_codes_numeric_and_legacy.sql diff --git a/api/app/controller/api/Distribution.php b/api/app/controller/api/Distribution.php index ed96c55..32ec22e 100644 --- a/api/app/controller/api/Distribution.php +++ b/api/app/controller/api/Distribution.php @@ -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 $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 位 100000–999999,7 位 1000000–9999999,以此类推;用尽则加长位数(上限 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()); diff --git a/api/database/add_invite_codes.sql b/api/database/add_invite_codes.sql index d9e9f33..7e40b18 100644 --- a/api/database/add_invite_codes.sql +++ b/api/database/add_invite_codes.sql @@ -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='小程序填写邀请码解析用'; diff --git a/api/database/migrations/add_invite_codes_numeric_and_legacy.sql b/api/database/migrations/add_invite_codes_numeric_and_legacy.sql new file mode 100644 index 0000000..c80569e --- /dev/null +++ b/api/database/migrations/add_invite_codes_numeric_and_legacy.sql @@ -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 '兼容旧字段,与新码同步时可等于数字码'; diff --git a/miniprogram/components/invite-code-dialog/invite-code-dialog.wxml b/miniprogram/components/invite-code-dialog/invite-code-dialog.wxml index 5729e83..4ac2305 100644 --- a/miniprogram/components/invite-code-dialog/invite-code-dialog.wxml +++ b/miniprogram/components/invite-code-dialog/invite-code-dialog.wxml @@ -6,7 +6,7 @@ class="icd-input" type="text" maxlength="{{maxLen}}" - placeholder="字母或数字" + placeholder="100000-999999 数字码,或老版字母数字码" placeholder-class="icd-ph" value="{{code}}" bindinput="onInput" diff --git a/miniprogram/pages/enterprise/index.js b/miniprogram/pages/enterprise/index.js index a37487a..dcb8e20 100644 --- a/miniprogram/pages/enterprise/index.js +++ b/miniprogram/pages/enterprise/index.js @@ -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() { diff --git a/miniprogram/pages/enterprise/index.wxml b/miniprogram/pages/enterprise/index.wxml index f71eb05..fc98697 100644 --- a/miniprogram/pages/enterprise/index.wxml +++ b/miniprogram/pages/enterprise/index.wxml @@ -5,7 +5,7 @@ 👤 - 个人版 + 企业版 {{siteTitle || '神仙团队性格测试'}} @@ -82,14 +82,12 @@ × 请选择使用版本 + 当前入口:{{versionPickerCurrentLabel}} 当前企业支持三种版本,选择后将进入对应流程 - - - - - - + + + diff --git a/miniprogram/pages/enterprise/index.wxss b/miniprogram/pages/enterprise/index.wxss index 1582f8e..3d09b7b 100644 --- a/miniprogram/pages/enterprise/index.wxss +++ b/miniprogram/pages/enterprise/index.wxss @@ -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; diff --git a/miniprogram/pages/gaokao/index.js b/miniprogram/pages/gaokao/index.js index b19b856..a825aa0 100644 --- a/miniprogram/pages/gaokao/index.js +++ b/miniprogram/pages/gaokao/index.js @@ -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() { diff --git a/miniprogram/pages/gaokao/index.wxml b/miniprogram/pages/gaokao/index.wxml index 1079d90..5562e9d 100644 --- a/miniprogram/pages/gaokao/index.wxml +++ b/miniprogram/pages/gaokao/index.wxml @@ -84,14 +84,12 @@ × 请选择使用版本 + 当前入口:{{versionPickerCurrentLabel}} 当前企业支持三种版本,选择后将进入对应流程 - - - - - - + + + diff --git a/miniprogram/pages/gaokao/index.wxss b/miniprogram/pages/gaokao/index.wxss index d14ecdb..e3d8244 100644 --- a/miniprogram/pages/gaokao/index.wxss +++ b/miniprogram/pages/gaokao/index.wxss @@ -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; diff --git a/miniprogram/pages/index/index.js b/miniprogram/pages/index/index.js index ac81424..0a7edfc 100644 --- a/miniprogram/pages/index/index.js +++ b/miniprogram/pages/index/index.js @@ -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 个版本弹出居中选择框 diff --git a/miniprogram/pages/index/index.wxml b/miniprogram/pages/index/index.wxml index c6b90d6..a1dbe31 100644 --- a/miniprogram/pages/index/index.wxml +++ b/miniprogram/pages/index/index.wxml @@ -5,7 +5,7 @@ 🏢 - 企业版 + 个人版 {{siteTitle || '神仙团队性格测试'}} @@ -84,14 +84,12 @@ × 请选择使用版本 + 当前入口:{{versionPickerCurrentLabel}} 当前企业支持三种版本,选择后将进入对应流程 - - - - - - + + + diff --git a/miniprogram/pages/index/index.wxss b/miniprogram/pages/index/index.wxss index f548068..198adc4 100644 --- a/miniprogram/pages/index/index.wxss +++ b/miniprogram/pages/index/index.wxss @@ -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; diff --git a/miniprogram/pages/profile/index.js b/miniprogram/pages/profile/index.js index efc0719..28db923 100644 --- a/miniprogram/pages/profile/index.js +++ b/miniprogram/pages/profile/index.js @@ -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' }) }) }, diff --git a/miniprogram/pages/profile/index.wxml b/miniprogram/pages/profile/index.wxml index bcd1105..79291e2 100644 --- a/miniprogram/pages/profile/index.wxml +++ b/miniprogram/pages/profile/index.wxml @@ -105,7 +105,7 @@ 我的邀请码 加载中… {{myInviteCodeErrMsg}} - {{myInviteCode}} + {{myInviteCodeNumeric || myInviteCode}} {{myInviteCodeHint}} diff --git a/miniprogram/pages/promo/index.js b/miniprogram/pages/promo/index.js index 297e1d3..bc07b3a 100644 --- a/miniprogram/pages/promo/index.js +++ b/miniprogram/pages/promo/index.js @@ -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' }) }) }, diff --git a/miniprogram/pages/promo/index.wxml b/miniprogram/pages/promo/index.wxml index d58abd2..9e22194 100644 --- a/miniprogram/pages/promo/index.wxml +++ b/miniprogram/pages/promo/index.wxml @@ -35,7 +35,21 @@ 我的邀请码 - 复制 + 复制邀请码 + 复制老版码 + 复制 重试 @@ -45,9 +59,13 @@ {{myInviteCodeErrMsg}} 若提示功能未就绪,请在数据库执行 api/database/add_invite_codes.sql 建表后再试。 - - {{myInviteCode}} - 好友在付费解锁前填写此码,即可与你建立推广绑定 + + + 我的邀请码 + {{myInviteCodeNumeric}} + + {{myInviteCode}} + 好友在付费解锁前填写此邀请码,即可与你建立推广绑定。 {{myInviteCodeHint}} diff --git a/miniprogram/pages/promo/index.wxss b/miniprogram/pages/promo/index.wxss index 86c97a2..603443c 100644 --- a/miniprogram/pages/promo/index.wxss +++ b/miniprogram/pages/promo/index.wxss @@ -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;