Katalog firem#
Modul Comcat vznikl kopií z Inzerce, takže sdílí skoro celý model:
profil firmy je Advert s dynamickými parametry, kategorie je nested set, ceny za
zápis jsou na kategorii. Zápisek o vzniku je v docs/comcat-copy-from-bazaar.md.
Nejdřív si přečtěte Inzerci
Co platí tam o dynamických parametrech, denormalizovaných počítadlech, pravidlech zobrazení a nevratném propsání do podstromu, platí i tady. Tahle kapitola popisuje jen rozdíly.
Rozdíly proti Inzerci#
| Inzerce | Katalog firem | |
|---|---|---|
denormalizovaná price |
ano | ne |
příznak sold |
ano | ne |
sledující (AdvertFollower) |
ano | ne |
| hlídací pes | ano | ano |
| index nastavení | 5 |
6 |
Chybějící cena je záměr, ne opomenutí
Firma se neprodává. Ceny jsou za zápis do kategorie, a ty jsou na kategorii
(CategoryPrice), ne na profilu.
Rozdíl se propisuje i do generovaného sloupce active — má o jednu podmínku míň:
-- cms_mod_comcat_adverts.active (VIRTUAL)
active = CASE WHEN approved_at IS NOT NULL AND confirmed_at IS NOT NULL
AND paid = 1 AND banned = 0 AND deleted = 0 AND deactivated = 0
THEN 1 ELSE 0 END
Kopírovaný SQL dotaz z inzerce tu na sold narazí
Sloupec neexistuje. Kdo si přenese hotový dotaz, dostane chybu — což je ještě ta
lepší varianta; horší je přenesený PHP kód, který na sold sáhne přes getter
a dostane null. Přenesený filtr EqualFilter('sold', false) projde sestavením
i PHPStanem a spadne až za běhu na QueryException („has no field or association named
sold“), a to celou stránku, na které je výpis. Výchozí sada viditelnosti v
LastAdvertList proto sold nemá.
Sdíleno s Inzercí#
Tyto části nemají v katalogu vlastní kopii, kód je společný (traity v app/Core/Traits/):
| Část | Sdílený kód | Popis |
|---|---|---|
| zakázaná slova | Api System\BlacklistedWordManager, systémová tabulka cms_system_blacklisted_words |
modulová tabulka cms_mod_comcat_blacklisted_words už neexistuje; Inzerce |
| pravidla parametrů při Api zápisu | AdvertParameterPolicyTrait, ParameterRuleEvaluator, AdvertParameterRuleContextRepositoryTrait |
Inzerce |
| nahlášení profilu | ReportedFlagSyncTrait (Api), ReportModerationPresenterTrait (administrace) |
Inzerce |
| správa profilu hostem | ManageKeyTrait nad ManageKeyAwareInterface |
Odkaz s klíčem |
Profil nemá pevná pole#
Název, IČO, adresa, telefon, popis i otevírací doba jsou parametry, stejně jako u inzerátu.
Bez parametrů je formulář prázdný
Než se dá založit první firma, musí existovat parametry a musí být nacílené na kategorii. Formulář nevyhodí chybu — prostě nemá co vykreslit, takže to vypadá jako nedokončená šablona.
Cílení parametrů na kategorii, propsání do podkategorií (meta['setParametersToChildren'])
i propsání cen (meta['setPricesToChildren']) funguje jako
cílení v Inzerci: chybějící meta['parameterRels']
cílení nemění, prázdný seznam ho smaže. Katalog firem má vlastní kopii traitů
v app/UI/Api/Comcat/Models/Managers/Traits/CategoryManager/ a chování drží
tests/Unit/Comcat/Api/CategoryParameterRelsMetaTriStateTest.php.
Otevírací doba#
Vlastní typ pole: skryté pole s uloženou strukturou, které naplňuje JavaScript.
Otevírací doba se může tiše ztratit
Když se pole při uložení nevykreslí, kolekce parametrů se nahradí bez něj — tatáž mechanika jako u každé jiné kolekce (Hydrátory). Nic to neohlásí a v šabloně je to proto výslovně okomentované.
Formulář profilu na webu#
Front formulář firemního profilu funguje jako
formulář inzerátu na frontu: předvyplnění z účtu podle
klíče parametru přes getEntityValues() z LoadingTrait, parameterRels jen při aspoň jednom
vyplněném řádku, kategorie v meta['categoryIds']. Navíc proti inzerci předvyplní parametr
s klíčem contactName (jméno a příjmení v jednom poli).
Prázdné pole parametru se přeskočí. Zbyl-li aspoň jeden vyplněný řádek, kolekce se nahradí celá
a hodnoty vyprázdněných polí smaže orphanRemoval. Vyprázdnit z webu všechny parametry naráz
nejde: odeslání bez jediného vyplněného parametru kolekci neposílá a uložené hodnoty zůstanou.
Kategorie a ceny#
Nested set, matice cen region × platební brána, propsání do podstromu.
Zaškrtnuté „zdarma“ jen SKRYJE matici, ceny se uloží dál
Pole jsou schovaná stylem, ne vypnutá. Odškrtnutím „zdarma“ se objeví ceny, které tam admin nikdy vědomě nezadal.
Členství × zaplacený zápis#
Detail profilu v placené kategorii se dá podmínit členstvím návštěvníka — což je něco úplně jiného než zaplacený zápis firmy.
| Platba | Kdo platí | Co odemyká |
|---|---|---|
| zápis | firma | že profil v katalogu vůbec je |
| členství | návštěvník | že si detail profilu může zobrazit |
Dvě různé platby, dva různí plátci
Záměna je nejčastější nedorozumění v tomhle modulu. „Firma zaplatila, a stejně to není vidět" znamená, že chybí členství návštěvníka — ne že selhala platba firmy.
Řazení podle hodnocení#
Sortbox výpisu (Advert/list.latte) nabízí tři volby a prepareOrderBy() má pro každou
vlastní větev:
/o-N |
Volba | Řazení |
|---|---|---|
| 1 | Nejnovější | insertDatetime ↓ (i výchozí větev) |
| 2 | Nejčtenější | viewsDetail ↓ |
| 3 | Nejlépe hodnocené | ratingThread.ratingAvg ↓ |
Před volbou vždy stojí topované profily (topedUntil > NOW() ↓) a za ní id jako jednoznačný
druhý klíč (OrderByHelper::withIdTiebreak()), jinak by se shodné hodnoty stránkovaly náhodně.
Nabízená volba bez vlastní větve tiše řadí jako „Nejnovější“
Volba, kterou sortbox nabízí a switch v prepareOrderBy() nezná, propadne do default.
Uživatel si vybere řazení a nic se nezmění. Novou volbu přidávej do obou míst zároveň.
Výpis firem nabízí řazení „Nejlépe hodnocené“ (/o-3, ratingThread.ratingAvg sestupně,
AdvertListTrait::prepareOrderBy()); inzerce ho nemá. Ze skaláru ratingThreadId umí Doctrine
jen WHERE, řadit podle sloupce cizí tabulky jde jen přes asociaci. Api entita Advert proto
mapuje sloupec vlákna hodnocení podruhé, asociací $ratingThread, a ta nese
#[ExternalReadonly] stejně jako skalár (Atributy pro zápis sloupců).
Profil bez hodnocení z výpisu nevypadne, asociace se připojuje LEFT JOINem.
Asociace bez #[ExternalReadonly] otevře přepojení hodnocení
Externí klient by poslal data.ratingThread = {"id": X}, obešel zámek skaláru a připojil
profil k cizímu vláknu hodnocení, tedy k cizímu průměru a recenzím.
Odpovědi: příjemce a přečtení#
Odpovědi na firemní profil fungují stejně jako v Inzerci:
šablona CC_ADVERT_RESPONSE nese tlačítko {$markReadLink}, veřejná stránka
Front:Comcat:Response:markRead má id MARK_READ_SLUG = 2012, výpis LIST_SLUG = 208
a detail DETAIL_SLUG = 209 (dřív 159).
Nastavení#
Modul má v tabulce nastavení index 6 (inzerce má 5). Formulář
(app/UI/Admin/Comcat/Presenters/SettingPresenter.php) zapisuje tyto klíče:
| Klíč | Pole | Kdo ho čte |
|---|---|---|
advert.perPage, maxImageSize, advert.expiration.days |
číslo 1–100, 1–100 (MB), 1–3650 | výpis; limit uploaderu, když ho nemá parametr obrázků; výpočet expirace, když ji neurčí kategorie |
advert.approval.auto |
přepínač | schválení správcem při vložení |
advert.confirmation.required.logged, advert.confirmation.required.guest |
přepínač | potvrzení autorem odkazem z e-mailu, jen při vložení; výklad viz Inzerce |
geocoding.precise_address |
přepínač | AdvertRegionAddressTrait, viz Inzerce |
advert.statistics.enabled |
přepínač | Api AdvertManager; vypnuto = denní statistiky pro graf v editaci se nezapisují, počítadla zobrazení běží dál |
advert.expiration.noticeDays, advert.expiration.deleteDays |
0–365, 0–3650 (0 = vypnuto) | crony notice-expiration, delete-expired |
watchdog.perPage |
1–100 | front výpis hlídačů |
watchdog.email.mode |
0 vypnuto, 1 hned cronem send, 2 denní souhrn cronem digest, 3 hned při vložení bez cronu |
WatchdogManager, cron WatchdogEmailPresenter |
watchdog.email.maxAttempts, watchdog.email.retentionDays |
1–20, 1–3650 | rozesílání a úklid fronty hlídačů |
Api čte skupinu 6 napevno (getSettingContext()->get(6, …)). Konstanta MODULE = 6
v presenteru proto musí sedět, jinak formulář ukládá do skupiny, kterou nikdo nečte, a změny
nastavení se tiše neprojeví.
Klíče, které kód nečte, byly z formuláře odstraněny — nevracejte je
Rozměry obrázků, typ zmenšení, watermarkFilename, responsesPerPage ani
afterAddAdvertRedirectOn v nastavení katalogu nejsou, katalog je nečte:
varianty se čtou z DB (použití comcat-adverts, viz
Varianty obrázků) a vodoznak je globální watermarkPath.
Přiřazení variant se edituje v části Obrázky pod formulářem nastavení. Kdyby někdo
staré klíče do formuláře vrátil, první uložení je založí zpátky —
saveSettingForm() zapisuje všechny vyjmenované klíče.
Naplánované úlohy#
| Endpoint | Co dělá | Doporučený interval |
|---|---|---|
/cron/comcat/category/recount |
přepočet počtů firem v kategoriích | à 10–15 minut |
/cron/comcat/advert/notice-expiration |
upozorní před vypršením zápisu | denně |
/cron/comcat/advert/delete-expired |
označí deleted = 1 po expiraci |
denně |
/cron/comcat/watchdog-email/send |
rozešle hlídače | à 5–10 minut |
/cron/comcat/watchdog-email/digest |
souhrnný přehled | denně večer |
/cron/comcat/watchdog-email/purge-old |
smaže odeslané a zrušené řádky fronty hlídačů starší než watchdog.email.retentionDays (výchozí 90 dní), čekající nikdy; podrobně Inzerce |
denně |
/cron/comcat/elastic/sync |
průběžná synchronizace indexu | à 10 minut |
/cron/comcat/elastic/reindex |
úplný reindex | denně v noci |
/cron/comcat/sitemap/{category,advert} |
sitemapy | denně v noci |
Profil po expiraci zmizí z výpisu i z detailu hned, bez ohledu na crony: active expiraci
nezná, ale výpis i detail přidávají podmínku expiration_date >= dnes. Detail ho pak ukáže jen
majiteli s upozorněním „po expiraci“.
Bez cronů expirace firma nedostane upozornění a prošlý profil se neuklidí
Bez notice-expiration firma před vypršením zápisu nedostane e-mail a nemá jak včas
prodloužit. Bez delete-expired zůstane prošlý profil v databázi a v „Mých firmách“
navždy. Totéž nastane, když je advert.expiration.noticeDays nebo deleteDays 0.
Kam sáhnout#
| Chci | Kde |
|---|---|
| logiku profilu | app/UI/Api/Comcat/Models/Managers/AdvertManager.php |
| přepočet počtů | app/UI/Api/Comcat/Models/Managers/CategoryManager.php |
| řazení a filtry výpisu firem | app/UI/Front/Comcat/Presenters/Traits/Lists/AdvertListTrait.php |
| nastavení modulu | app/UI/Admin/Comcat/Presenters/SettingPresenter.php |
| formulář profilu na webu | app/UI/Front/Comcat/Components/AdvertFormComponent/ |
| cron úlohy | app/UI/Cron/Comcat/Presenters/ |
| zápisek o vzniku modulu | docs/comcat-copy-from-bazaar.md |
Navazující kapitoly: Inzerce · Elasticsearch · Naplánované úlohy