Skip to content

Mở rộng giới hạn số từ khóa (phiên bản release theo giai đoạn)

Tổng quan

MụcNội dung
Trạng thái🔵 Đề xuất
Issue-
Phụ trách-
Phiên bản trước#1-2 Bản sửa đổi

Nâng giới hạn từ khóa cho mỗi location từ 8 → 15 từ. Một user sở hữu nhiều cửa hàng nhưng không phải cửa hàng nào cũng tăng đồng loạt, nên thiết kế lấy giới hạn động theo từng cửa hàng (per-location) làm trục chính. Để giảm rủi ro, triển khai theo 3 giai đoạn:

  • Phase 1 (Release qua chỉnh sửa DB trực tiếp) — chỉ release phần "cơ chế cấu hình"; việc mở cho từng cửa hàng do ops thực hiện bằng SQL
  • Phase 2 (Release màn quản trị) — sales / CS tự thao tác trên UI để cài đặt giới hạn cho từng cửa hàng
  • Phase 3 (Workflow self-service: khách hàng gửi yêu cầu — admin duyệt) — khách hàng tự gửi "yêu cầu nâng giới hạn" từ tài khoản của mình, admin duyệt / từ chối

Nội dung đề xuất

Bối cảnh & vấn đề

  • Giới hạn từ khóa hiện tại là 8 (hard-code rải rác ở 7 chỗ: xem v2)
  • Nhu cầu khách hàng theo dõi từ 10 từ khóa trở lên ngày càng nhiều
  • Một user có nhiều cửa hàng (mappy_users 1:N mappy_gbp_locations) và việc nâng giới hạn tùy theo từng cửa hàng
  • Bản v2 đã đề xuất "mô hình DB hỗ trợ tính phí cả theo cửa hàng lẫn theo hợp đồng", nhưng nếu gộp cả UI quản trị thì thời gian release kéo dài
  • Phía sales muốn "ưu tiên mở 15 từ cho một vài cửa hàng trước"

→ Áp dụng release theo giai đoạn: hạ tầng cấu hình (DB + backend + UI khách hàng) release trong Phase 1, Phase 2 giảm công thao tác bằng UI quản trị

Giải pháp đề xuất

PhasePhạm viCách mở cho cửa hàngCông ước tính
Phase 1DB / backend / UI khách hàng / Export / scrapingOps chạy SQL để INSERT bản ghi giới hạn theo cửa hàng~7.5 người-ngày
Phase 2UI quản trị (chỉnh giới hạn theo cửa hàng) / ActivityLog / Admin APISales / CS thao tác trên màn quản trị~4.5 người-ngày
Phase 3UI khách hàng (gửi yêu cầu) / Queue admin / Email / Bảng requestKhách hàng request → admin duyệt → tự apply~6.0 người-ngày
Tổng cộng18.0 người-ngày

Điểm khác với v2:

  • Phase 1 chỉ vận hành scope_type='location' (scope contract giữ để dùng cho tương lai)
  • Phạm vi release tách thành 2 giai đoạn (v2 dự định release một lần)
  • Toàn bộ cải tạo UI khách hàng / Export / scraping gói trong Phase 1 (Phase 2 chỉ làm UI quản trị)

Mô hình dữ liệu

Dùng đúng schema của keyword-limit-v2, giữ tính hai giá trị của scope_type để sau này còn mở rộng được sang tính phí theo hợp đồng.

mappy_keyword_limit_settings
├─ id              BIGINT PK
├─ scope_type      ENUM('contract','location')   -- Phase 1 chỉ dùng 'location'
├─ scope_id        BIGINT                        -- gbp_location_id (hoặc mappy_user_id nếu contract)
├─ system_id       BIGINT                        -- Định danh tenant đã có
├─ max_keywords    TINYINT UNSIGNED              -- 8〜15
├─ effective_from  DATE                          -- Ngày bắt đầu hiệu lực
├─ effective_to    DATE NULLABLE                 -- NULL = không có hạn cuối
├─ memo            VARCHAR(255) NULLABLE         -- Ghi chú của sales (cơ sở tính phí)
├─ created_at / updated_at
└─ UNIQUE KEY (scope_type, scope_id, effective_from)

Logic tra cứu giới hạn

resolve_max_keywords(gbp_location_id, user_id, today)
  = bản ghi hiệu lực scope=location.max_keywords
  ?? bản ghi hiệu lực scope=contract.max_keywords
  ?? plan-default (permissions.js: user-standard-plan=8, free=1, meo-booster=8)

"Hiệu lực" = effective_from <= today AND (effective_to IS NULL OR today <= effective_to)

Phase 1 — Release qua chỉnh sửa DB trực tiếp

Phạm vi

  • [x] Tạo bảng mappy_keyword_limit_settings + migration Laravel
  • [x] Thêm method resolveMaxKeywords() cho model Keyword / GbpLocation
  • [x] Thay validation FormRequest sang dùng logic resolve mới
  • [x] Bỏ hard-code MAX_KEYWORDS_COUNT = 8 ở UI khách hàng (KeywordSettings.vue), lấy động từ API
  • [x] GbpLocation.php:209 .fillKeys(1, 8) chuyển sang dùng giới hạn động
  • [x] ExportController.php:2291, 2431 đổi header CSV/Excel sang dynamic
  • [x] PlacesAPI export_data [:8][:resolved_max]
  • [x] Sửa 3 chỗ hard-code phía scraping (repo scraping)
  • [x] Việc mở giới hạn cho từng cửa hàng do ops thực hiện bằng SQL vận hành (mục dưới)

Migration Laravel

php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration {
    public function up(): void
    {
        Schema::create('mappy_keyword_limit_settings', function (Blueprint $table) {
            $table->id();
            $table->enum('scope_type', ['contract', 'location'])
                  ->comment('Phạm vi áp dụng: location=theo cửa hàng / contract=theo hợp đồng user');
            $table->unsignedBigInteger('scope_id')
                  ->comment('scope_type=location → gbp_locations.id, contract → mappy_users.id');
            $table->unsignedBigInteger('system_id')->comment('Định danh tenant');
            $table->unsignedTinyInteger('max_keywords')->comment('8〜15');
            $table->date('effective_from');
            $table->date('effective_to')->nullable();
            $table->string('memo', 255)->nullable()->comment('Ghi chú sales / cơ sở tính phí');
            $table->timestamps();

            $table->unique(
                ['scope_type', 'scope_id', 'effective_from'],
                'uk_scope_effective'
            );
            $table->index(['scope_type', 'scope_id'], 'idx_scope');
            $table->index('system_id', 'idx_system');
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('mappy_keyword_limit_settings');
    }
};

Template SQL cho ops

Dự kiến đặt tại docs/operations/keyword-limit-release.sql.

sql
-- =========================================================
-- Phase 1 - SQL vận hành: mở 15 từ khóa cho cửa hàng chỉ định
-- =========================================================
-- Kiểm tra trước khi chạy:
--   1. Đã nhận danh sách gbp_location_id từ marketing / CS (có văn bản)
--   2. system_id tra từ mappy_gbp_locations.system_id
--   3. Người thực thi · cửa hàng · lý do tính phí đã post lên Slack #releases
--   4. BẮT BUỘC kiểm thử trước trên STAGING
-- =========================================================

START TRANSACTION;

-- Xác nhận cửa hàng đối tượng
SELECT id AS gbp_location_id, system_id, name
FROM mappy_gbp_locations
WHERE id IN (/* điền location_id ở đây */);

-- INSERT (thêm dòng nếu xử lý nhiều cửa hàng cùng lúc)
INSERT INTO mappy_keyword_limit_settings
  (scope_type, scope_id, system_id, max_keywords,
   effective_from, effective_to, memo,
   created_at, updated_at)
VALUES
  ('location',
   /* gbp_location_id */,
   /* system_id */,
   15,
   CURDATE(),
   NULL,
   'Sales: dự án ◯◯ / Ticket: TICKET-XXXX',
   NOW(), NOW());

-- Xác nhận kết quả
SELECT s.scope_type, s.scope_id, l.name, s.max_keywords,
       s.effective_from, s.effective_to, s.memo
FROM mappy_keyword_limit_settings s
JOIN mappy_gbp_locations l ON l.id = s.scope_id
WHERE s.scope_type = 'location'
  AND s.scope_id IN (/* điền location_id ở đây */);

-- Nếu OK thì:
COMMIT;
-- Nếu rollback:
-- ROLLBACK;

Quy trình rollout

  1. Kiểm tra trên STAGING: mở 1 cửa hàng lên 15, chạy toàn bộ đường: UI khách hàng / Export / scraping / PlacesAPI
  2. Release production: migration → deploy code (xoá hard-code đồng thời)
  3. Mở cửa hàng: dùng danh sách location_id CS gửi để chạy SQL vận hành → COMMIT
  4. Xác nhận hoạt động: đăng nhập tài khoản khách hàng của cửa hàng đối tượng, mở màn KeywordSettings, kiểm tra slot 9〜15 đã có thể nhập

Công của Phase 1

#Hạng mụcCông (người-ngày)Phụ trách
1Xác nhận yêu cầu & tạo tài liệu1.0Thiết kế
2Thiết kế DB / API / logic resolve1.0Thiết kế
3migration + model + FormRequest1.0Sản xuất
4Cải tạo UI khách hàng (lấy qua API + ô nhập động)1.0Sản xuất
5Cải tạo PlacesAPI export_data + scraping1.0Sản xuất
6CSV header + hiển thị ranking động1.0Sản xuất
7Kiểm thử tích hợp (PlacesAPI / Export / UI khách hàng)1.0Sản xuất
8Deploy + chuẩn bị template SQL + lần mở đầu tiên0.5Sản xuất
Phase 1 tổng cộng7.5

Phase 2 — Tự thao tác qua màn quản trị

Phạm vi

  • Thêm màn cài đặt giới hạn theo cửa hàng dưới admin guard hiện có (auth:admin_api)
  • Thêm tab thứ 3 "Giới hạn từ khóa" vào page /admin/users/:userId/details (MappyUserDetail.vue) — không tạo route mới, không thêm sidebar
  • Mở rộng tối thiểu API /api/mappy/admin/users/{user}/locations-and-groups đã có sẵn để response thêm max_keywords (resolved + override)
  • Bảng danh sách cửa hàng của user → thêm cột max_keywords (input) + nút lưu
  • Khi lưu, ghi ActivityLog (ai · khi nào · cửa hàng nào · đổi sang bao nhiêu từ) làm cơ sở tính phí
  • Validation: 8 <= max_keywords <= 15
  • Phản ánh ngay sang UI khách hàng (invalidate cache)

Hành vi màn quản trị

Thiết kế API

MethodPathMục đích
GET/api/mappy/admin/users/{user}/locations-and-groupsAPI đã có. Mở rộng tối thiểu để include max_keywords (resolved + override)
PATCH/api/mappy/admin/locations/{gbp_location}/keyword-limitUpsert giới hạn theo cửa hàng (body: { max_keywords, memo }) — Mới
DELETE/api/mappy/admin/locations/{gbp_location}/keyword-limitBỏ override cho cửa hàng (trở về plan default) — Mới

Công của Phase 2

#Hạng mụcCông (người-ngày)Phụ trách
1Thiết kế UI (wireframe + mock admin)0.5Thiết kế
2Cài đặt Admin API (GET / PATCH / DELETE)1.0Sản xuất
3UI màn quản trị (thêm tab vào UserDetail + UserKeywordLimitTab.vue mới)1.5Sản xuất
4Tích hợp ActivityLog + cache invalidate0.5Sản xuất
5Kiểm thử tích hợp + deploy1.0Sản xuất
Phase 2 tổng cộng4.5

Phase 3 — Workflow self-service request

Sau khi Phase 2 release, khách hàng có thể tự gửi "yêu cầu nâng giới hạn" từ chính tài khoản của họ. Mô hình admin tiếp nhận thủ công của Phase 2 sẽ tiến hóa thành workflow xuất phát từ khách hàng.

Phạm vi

  • Bảng request mới mappy_keyword_limit_requests
  • UI khách hàng (KeywordSettings.vue): thêm nút "Yêu cầu nâng giới hạn" + modal nhập yêu cầu
  • UI khách hàng: badge trạng thái (申請中 / 却下) ngay trong KeywordSettings
  • Khách hàng được hủy request khi đang ở trạng thái pending
  • Admin có màn queue /admin/keyword-limit-requests mới (thêm 1 mục sidebar "申請承認")
  • Khi approve: tự INSERT vào mappy_keyword_limit_settings + ghi ActivityLog + gửi email cho khách hàng
  • Khi reject: lưu review_note + gửi email cho khách hàng
  • Email gửi tới danh sách trong NotificationSettings của user

Mô hình dữ liệu (Phase 3 thêm)

mappy_keyword_limit_requests
├─ id                   BIGINT PK
├─ mappy_user_id        BIGINT          -- Người gửi
├─ gbp_location_id      BIGINT          -- Cửa hàng đối tượng
├─ current_max          TINYINT         -- Giá trị resolved tại thời điểm submit (lịch sử)
├─ requested_max        TINYINT (9〜15)
├─ reason               VARCHAR(500)    -- Lý do user nhập
├─ status               ENUM('pending','approved','rejected','cancelled')
├─ requested_at         DATETIME
├─ reviewed_by          BIGINT NULL     -- ID admin đã duyệt
├─ reviewed_at          DATETIME NULL
├─ review_note          VARCHAR(500) NULL  -- Memo của admin (lý do reject…)
├─ applied_setting_id   BIGINT NULL     -- FK tới settings được tạo khi approve
└─ created_at / updated_at

INDEX (mappy_user_id, status)         -- User xem request của mình
INDEX (status, requested_at)          -- Queue admin (pending cũ trước)

State machine

Flow gửi — duyệt đầy đủ

Thiết kế API (Phase 3 thêm)

MethodPathMục đíchAuth
POST/api/mappy/keyword-limit-requestsKhách hàng gửi requestauth:api (khách hàng)
GET/api/mappy/keyword-limit-requestsKhách hàng xem request của mìnhauth:api (khách hàng)
DELETE/api/mappy/keyword-limit-requests/{id}Khách hàng hủy request pendingauth:api (khách hàng)
GET/api/mappy/admin/keyword-limit-requestsAdmin lấy queue (filter status)auth:admin_api
PATCH/api/mappy/admin/keyword-limit-requests/{id}/approveDuyệtauth:admin_api
PATCH/api/mappy/admin/keyword-limit-requests/{id}/rejectTừ chốiauth:admin_api

UI khách hàng cần sửa

Vị tríSửa gì
Header KeywordSettings.vueCạnh hiển thị resolved max → thêm badge trạng thái request
Dưới ô nhậpNút "Yêu cầu nâng giới hạn" (chỉ hiển thị khi resolved max < 15)
KeywordLimitRequestModal.vue ★ MớiForm (slider requested_max / textarea lý do / nút gửi)
Khi đang pendingSlot 9 〜 requested_max hiển thị xám "申請中"

Màn quản trị (Phase 3 mới)

Thêm mục sidebar

resources/js/mappy/_navigationAdmin.js chỉ thêm 1 mục:

js
{ text: '申請承認', value: 'KeywordLimitRequests', icon: '/images/...', badge: 'pending_count' }

badge: 'pending_count' hiển thị số request chưa duyệt (đỏ) — dùng pattern có sẵn.

Màn queue /admin/keyword-limit-requests

Thành phầnNội dung
FilterStatus (pending / approved / rejected / all) / Khoảng thời gian / Tìm user
BảngThời điểm submit / loginId / tên shop / giá trị hiện tại / giá trị yêu cầu / lý do / status
Click dòngSidepane mở chi tiết → nút "Duyệt" / "Từ chối (lý do bắt buộc)"
Bulk actionCheckbox chọn → duyệt / từ chối hàng loạt (cùng review_note)

Email notification

Tạo 2 Laravel Mailable:

ClassTriggerSubjectNội dung chính
KeywordLimitRequestApprovedMailapprove API【MAPPY】Yêu cầu nâng giới hạn từ khóa đã được duyệtNội dung request + ngày duyệt + giới hạn mới
KeywordLimitRequestRejectedMailreject API【MAPPY】Về yêu cầu nâng giới hạn từ khóaNội dung request + lý do từ chối (review_note)

Gửi đến: toàn bộ mailAddresses đã set trong NotificationSettings (ít nhất 1 email chính khi ký hợp đồng).

Công của Phase 3

#Hạng mụcCông (người-ngày)Phụ trách
1Xác nhận yêu cầu + tài liệu thiết kế (gồm state machine)1.0Thiết kế
2migration (bảng requests) + Model + state machine0.5Sản xuất
33 API khách hàng (POST / GET / DELETE)0.5Sản xuất
43 API admin (GET / approve / reject) + auto INSERT0.5Sản xuất
52 Email Mailable + template0.5Sản xuất
6UI khách hàng (modal + badge + slot 申請中)1.0Sản xuất
7Màn queue admin + thêm sidebar1.0Sản xuất
8Kiểm thử tích hợp (email / state / auto apply)0.5Sản xuất
9Deploy + xác nhận0.5Sản xuất
Phase 3 tổng cộng6.0

Mock màn hình

Phía khách hàng (Phase 1 sẽ cải tạo)

キーワード管理

登録キーワード8 / 8件
1MEO対策
2美容室 渋谷
3ヘアサロン
4カット 安い
5縮毛矯正
6トリートメント
7ヘッドスパ
8カラー 渋谷
上限に達しています。上限撤廃モードを有効にしてください。

変更内容

項目現行変更後
キーワード上限8件無制限(推奨10件以上)
スクレイピング8件固定登録数に応じて動的
表示固定レイアウトスクロール対応

Phía quản trị (Phase 2 sẽ làm mới)

Thiết kế UI chi tiết xem Thiết kế màn hình: Quản trị — Giới hạn từ khóa theo cửa hàng.

アカウント情報

user-standard-planプラン上限:8 件契約:2025-04-01 〜 2026-03-31(12ヶ月)所属店舗数:5
このユーザ配下の店舗ごとに 8〜15 件の範囲でキーワード上限を設定できます。
対象ユーザ配下の店舗一覧
店舗名プラン値現在の上限新しい上限メモ(課金根拠)有効期間操作
渋谷本店
location_id: 8801
815
2026-06-01
〜 無期限
新宿西口店
location_id: 8802
88
〜 無期限
池袋東口店
location_id: 8803
812
2026-05-15
〜 2026-12-31
横浜西口店
location_id: 8804
88
〜 無期限
川崎駅前店
location_id: 8805
810
2026-06-01
〜 無期限
上限の範囲は 8〜15 件。8 にすると override が解除され、プランデフォルト適用に戻ります。

呼び出し API(参考)

MethodPath役割
GET/api/mappy/admin/users/{user}/locations-and-groups店舗一覧取得(既存 API を拡張:resolved + override を含める)
PATCH/api/mappy/admin/locations/{loc}/keyword-limit店舗別上限を upsert(新設)
DELETE/api/mappy/admin/locations/{loc}/keyword-limitoverride 解除(新設)

Flow request phía khách hàng (Phase 3 mới)

キーワード管理

申請中 (申請: 12 件)
登録キーワード8 / 8 件
1
2
3
4
5
6
7
8
9
申請中
10
申請中
11
申請中
12
申請中
13
🔒 ロック
14
🔒 ロック
15
🔒 ロック

Queue duyệt phía admin (Phase 3 mới)

キーワード上限引き上げ申請

未承認 3 件本日 2 件承認待ち最古:2026-05-28 16:45
申請日時申請者対象店舗現在希望理由ステータス
2026-05-29 10:32
田中 太郎
tanaka_taro_001
渋谷本店812新メニュー導入により追加で 4 キーワードを追跡したい未承認
2026-05-29 09:18
佐藤 花子
sato_hanako_023
横浜駅前店815競合分析強化のため上限まで申請未承認
2026-05-28 16:45
鈴木 一郎
suzuki_ichi_007
池袋東口店1215冬季キャンペーン用に+3未承認

Các vấn đề thiết kế (kế thừa từ v2)

Ảnh hưởng phía PlacesAPI

  • Mỗi location tăng 8 → 15 từ → lượng gọi API tăng +87%
  • Cấu hình 3 GCP project key hiện tại (QPM 1800) → QPM cần là 940, còn dư (không cần thêm project)
  • Phí Basic SKU = miễn phí
  • Thời gian: 200 giây → ~375 giây (batch ban đêm vẫn dư)
  • Trong Phase 1 số cửa hàng đối tượng còn ít, tải tăng rất hạn chế

Ảnh hưởng phía scraping

  • Chỉ còn ~33 cửa hàng → toàn bộ chuyển sang 15 từ cũng chỉ tăng ~39 phút
  • Hard-code phải sửa chỉ có 3 chỗ

Phương châm tích hợp hệ thống tính phí

mappy nhận thông báo thanh toán từ nền tảng bên ngoài (KUCHIKOMIONE / G-COR / pipit), bản thân không có engine tính phí. Case này giới hạn đến "có thể cài đặt thủ công trên màn quản trị", còn việc thêm SKU vào master sản phẩm và mở rộng payload thông báo phía nền tảng tính phí tách sang case sau.

Ước tính công (tiền đề có AI)

Cơ cấu nhân sự

Vai tròSố ngườiNội dung phụ trách
Thiết kế1 ngườiXác nhận yêu cầu → Chỉ thị AI tạo tài liệu → Review → Chỉ thị sản xuất
Sản xuất1 ngườiChỉ thị AI tạo theo ISSUE → Code review → Kiểm thử → Deploy

Tổng

PhaseCông
Phase 1 (Release qua DB trực tiếp)7.5 người-ngày
Phase 2 (Release màn quản trị)4.5 người-ngày
Phase 3 (Workflow self-service)6.0 người-ngày
Tổng cộng18.0 người-ngày

Điều kiện tiên quyết & ràng buộc

  • Giới hạn cứng = 15, tối thiểu = 8 (giữ nguyên cho hợp đồng hiện hữu)
  • Ngay sau release Phase 1, ops mở bằng SQL; Phase 2 sẽ chuyển sang UI
  • Tích hợp nền tảng tính phí nằm ngoài phạm vi case này
  • 3 key PlacesAPI hiện tại đủ hấp thụ lượng request tăng khi nâng lên 15 từ (không cần thêm project)

Lịch trình

タスク担当日数6/16/26/36/46/56/66/76/86/96/106/116/126/136/146/156/166/176/186/196/206/216/226/236/246/256/26
P1 Yêu cầu & tài liệuThiết kế1d
P1 Thiết kế DB · APIThiết kế1d
P1 migration + modelSản xuất1d
P1 Cải tạo UI khách hàngSản xuất1d
P1 PlacesAPI/scrapingSản xuất1d
P1 CSV/ranking độngSản xuất1d
P1 Kiểm thử tích hợpSản xuất1d
P1 Deploy + mở lần đầuSản xuất1d
P2 Thiết kế UIThiết kế1d
P2 Admin APISản xuất1d
P2 UI màn quản trịSản xuất2d
P2 ActivityLog + tích hợp + deploySản xuất1d
P3 Xác nhận yêu cầu + thiết kếThiết kế1d
P3 Bảng request + ModelSản xuất1d
P3 API (khách + admin) + MailableSản xuất2d
P3 UI khách hàng (modal + badge)Sản xuất1d
P3 Màn queue duyệt adminSản xuất1d
P3 Kiểm thử tích hợp + deploySản xuất1d