产品使用文档与部署教程,持续更新中
程序根/ ├── public/ # Web 根(入口 index.php、check.php、xjboot.php、静态资源) ├── app/ │ ├── index/ # 前台(控制器+视图+路由) │ ├── admin/ # 后台(含授权中心、远程广告、插件市场、客服中心) │ └── service/ # 核心服务(LicenseService / AuthFs / AdService / AuthService / │ # AuthSchema / CmsSchema / ChatService / PromoService) ├── install/ │ ├── schema.sql # 官网库结构(安装向导导入,含演示文档 cms_doc 种子) │ ├── auth.sql # 授权库结构 + 主站注入占位符(XJ_MASTER_URL/XJ_APP_UID/XJ_APP_KEY) │ ├── update/*.sql # 在线更新随包执行的增量 SQL(必须可重复执行) │ └── sdk/authcode.php # 客户程序 SDK 模板(授权验证 + 远程广告) ├── runtime/packages/ # 程序包存储(app/xxx 发布包、plugins/xxx 插件包、backup/ 明文备份) ├── package.php # 发布包构建脚本(读取 package.conf 注入主站信息) └── package.conf # 主站注入配置(含密钥,绝不随包分发)
构建买家版发布包时,把 package.conf 的三项(主站域名 / 官网程序 uid / 授权密钥)替换进 install/auth.sql 的占位符。客户安装后 auth_config 里天然带主站指向:auth_siteurl(主站域名)、xj_app_uid、xj_app_key。授权验证、插件市场、远程广告全部由这三个值驱动,客户零配置。
接口地址 = auth_siteurl + /check.php;uid/key 优先读手动配置与本地授权库,客户站读注入值。激活后状态存 cms_setting(license_* 键),24 小时自动复验,复验失败即锁定后台。
AdService::grouped() 统一取广告(按 position 分组):主站直连 auth_ad(展示「全部产品 + 官网程序」的广告),客户站带签名请求主站 check.php 解密取 ads。前台仪表盘 6 格(home_dash_1..6)由该服务驱动。主站不可达时优雅降级为「广告位招租」。
存主站 auth_app.ad_positions 字段(后台程序管理编辑),不随程序包分发。上传程序包时自动扫描源码中 xj_ads_render('标识') 调用识别广告位;「远程广告 → 广告位管理」按程序筛选、设置价格,客户可自助付费申请。
接口地址 = auth_siteurl + /auth/api/market;主站自身直连 auth_market 库。插件数据(免费/付费、上下架、下载地址)全部主站控制,客户端只读。
前后台共用 app/service/ChatService.php 一个业务层,前台接口在 app/index/controller/Chat.php,后台接待在 app/admin/controller/Chat.php;详细设计见文末「2026-09-26 开发记录」。
授权库用 AuthSchema、官网库用 CmsSchema:运行时探测缺失的表/列并自动补齐(CREATE TABLE IF NOT EXISTS + 版本标记短路),与安装向导、在线更新 SQL 组成三条建表通道,老站升级不易漏表。
{:count($list)}。1. 混淆器空格 bug:instanceof \Exception 被压缩成 instanceof\Exception,PHP 词法器合并为单个命名空间 token 导致语法错误——现在「单词后紧跟反斜杠限定名」必定留空格。
2. PHP 8 中 :: 是 T_DOUBLE_COLON 数组 token(不是字符串),静态属性保护判断失效导致 App::$debug 被改名——已兼容数组 token。
3. 核心加密壳 eval 吞掉配置文件的 return [...]——壳模板改为 return eval(...),配置数组可穿透 include。
4. 解密执行的代码里 declare(strict_types=1); 若位于文件头注释之后(ThinkPHP 文件惯例)会致命错误——eval 前无条件剥离该声明(仅影响被加密副本的类型严格性,语义安全)。
以上修复后,官方/支付/付费进群三个程序的商店源码已解密归一化为单层新壳并重新打包。
演示中心现支持三种演示库结构:官网系(auth_user+cms_admin)、付费进群(web_users+web_auth_group_access)、支付系统(epay_admin)。每程序在 demo_info 行内配置独立演示库连接(db_config 字段),双写与过期清理按程序分支处理。
cms_chat_user:访客档案。身份凭 cookie xj_chat_id(32 位 hex),登录会员额外记 uid/account 并同步资料;含 remark(客服备注)、tag、status(1 正常 / 2 拉黑);cms_chat_session:会话。唯一键 session_id;status = waiting / active / closed;未读与已读水位 unread_admin / unread_user + admin_read_id / user_read_id;update_time 同时充当「列表变更版本」(feed 增量判定);cms_chat_message:消息。唯一键 message_id,另有 (session_id, client_msg_id) 唯一键做发送幂等;sender_type = user / admin / system;type = text / image / file,metadata 存 JSON(文件名、大小、宽高);cms_chat_quick:快捷回复短语(title / content / category / sort / status / hit_count),首次建表播种 5 条。install/schema.sql 已含 4 张表;install/update/chat_20260926.sql,主控在「程序管理 → 编辑程序 → 高级设置」把 sql=1、sqlfile 指向它,客户站更新时在官网库逐条执行(全部 CREATE TABLE IF NOT EXISTS,可重复执行;文件内不能出现会切碎语句的分号与行注释);CmsSchema::ensureChat(),探测 + 补建 + 写 chat_schema_ver 短路标记(后台接待页 initialize 时调用)。站点经香港反代(审计绕行链路),WebSocket 升级头不透传,SSE 也会被缓冲,因此全程 HTTP 轮询:前台访客面板自适应间隔(对话活跃 0.8 秒 → 静默 30 秒后 3 秒 → 3 分钟后 5 秒),面板关闭或 document.hidden 时不轮询;后台接待页 1.5 / 3 / 5 秒,全局未读角标 15 秒。前台「客服回复 → 访客看到」实测约 0.7 秒。
app/index/route/app.php 显式路由):POST /chat/open|send|pull|upload|profile|close、GET /chat/cron;/admin/chat(接待页)、feed(会话与消息增量、清未读、写客服在线心跳)、send / upload / close / visitor / badge / setting / history;Base::success() 把附加字段平铺在顶层(无 data 包裹),前台控制器才返回 json([... 'data' => [...]])——写后台接口时按前者读返回值。ChatService::appendMessage() 一个写入点,单条原子 UPDATE(unread_x+1、message_count+1、last_message 摘要、update_time);清零用水位条件更新(SET admin_read_id=GREATEST(admin_read_id,:maxId), unread_admin=0 WHERE ... AND admin_read_id<:maxId),幂等且不会误清刚到的新消息;admin_id=0(还没客服接过);命中关键词优先,未命中用 keyword 为空的默认规则,最后才发欢迎语;落库为 system 消息,metadata 带 {"auto":1,"rule_id":N};cms_bad_word(只读),命中不落库并返回提示;聊天消息不写 cms_message,留言列表不受影响;upload_image()(白名单 + 真图校验 + 随机名),其他文件单独白名单(pdf/zip/rar/7z/doc(x)/xls(x)/ppt(x)/txt,≤10MB);last_message_at + chat_timeout 立即关)+ 兜底计划任务 GET /chat/cron?key=chat_cron_key(宝塔每 5 分钟;key 存 cms_setting.chat_cron_key,后台设置弹窗可复制);cron 放行规则 = key 正确 / 后台已登录且未带 key / 尚未配置 key;degraded=1,后台给出友好提示,绝不白屏。hidden 属性会被 CSS 的 display:flex 盖掉(浮窗面板一加载就显示)——对应选择器要补 [hidden] { display: none !important };{:$list|count} 在模板里会编译成 echo $list|count; 导致 PHP 致命错误、页面从该行截断(「自动回复」规则列表因此长期空白);chat.js?v=、style.css?v=、admin.js?v=),否则浏览器用缓存会误判成功能没生效;php -S 单线程,自测并发请求会排队,别把排队当成「消息丢了」。cms_nav 新增 intro(varchar 200):二级菜单在前台下拉卡片里显示的介绍文字。老站交由运行时自愈补齐 —— CmsSchema::ensureNavIntro()(SHOW COLUMNS 探测 + ALTER TABLE,无权限时静默跳过),前台取导航前调用;新装随 install/schema.sql 建表带上;另附增量 SQL install/update/nav_intro_20260926.sql。**intro 必须放在建表语句末尾**,这样 install/schema.sql 里既有那种「按位置插入、少写尾列」的 REPLACE INTO cms_nav VALUES (...) 种子行不用改也不会报错。
二级菜单不再用小的下拉卡片,改为 富下拉面板(app/index/view/common/header.html + public/static/css/style.css):面板 80% 宽、满屏高、白底,从头部下沿铺开;左栏 25% 是本菜单名(浅蓝胶囊 #ebf1fd/#3860f4 + 右箭头),右栏 75%(#f8f8f8)是卡片网格;每张卡片显示子项名称(#16213a 16px)与 intro(rgba(34,34,34,.5) 14px/25px),悬停标题变 #1664ff。版式尺寸 1:1 复刻 xtuidc.cn「解决方案」下拉(卡片圆角 15px、2px 白边、8px 8px 20px + -8px -8px 20px 双向阴影、网格 repeat(auto-fill, minmax(328px, 1fr)))。
定位要点:面板以 .site-header(position:absolute)为定位基准,所以 .nav-item.has-dropdown 必须设成 position:static,再用 left:0; top:100%; width:80% 对齐视口左边与头部下沿;面板仍是 .nav-item 的后代,纯 CSS :hover 就能保持展开(鼠标移到面板里不会收起)。别用 position:fixed:头部滚动后带 backdrop-filter,会给 fixed 子元素造出新的包含块,面板位置会跑偏。
「网站设置 → 导航管理」表单新增「菜单介绍」输入(200 字内,Nav::save() 写入 intro);手机端(≤768px)富下拉退回「缩进列表」形态,卡片拍平、左栏隐藏。
cms_article 新增 4 列:price(decimal(10,2),价格)、buy_url(购买链接)、download_url(下载地址)、app_uid(关联程序)。老站由 CmsSchema::ensureMarketCols() 运行时自愈补列(逐列 SHOW COLUMNS + ALTER TABLE,失败静默),新装随 install/schema.sql,另附增量 SQL install/update/market_20260926.sql。列同样加在建表语句末尾,且 REPLACE INTO cms_article VALUES (...) 种子行必须补够列数(见下「踩坑」)。
MarketService(app/service/MarketService.php):member() 取当前登录会员(session('user_uid') → auth 库 auth_user,其 user 字段即 auth_site.uid);owns($appUid) 查授权库 auth_site:uid=会员账号 AND app_uid=程序 AND active=1 AND (endtime IS NULL OR endtime='' OR endtime > now) —— 与用户中心「我的授权」同一套语义。文章模型上挂了三个访问器(模板里直接 {$vo.can_download} / {$vo.price_text} / {$vo.in_market}):有关联程序 → 走 owns();没关联程序但有下载地址 → 视为免费资源直接可下。新闻类文章 app_uid 为空且无下载地址 → owns() 直接返回 false,不会多查库。
前台新增 /market(app/index/controller/Market.php + app/index/view/market/index.html,样式在 style.css 的 .market-*):按分类列文章,卡片显示价格与「立即购买 / 下载源码」(已购买带「已购买」角标)。分类取 ?cid=,其次 setting('market_cat_id'),再次名为「源码市场」的分类。文章详情页(article/detail.html)对有价格/下载地址的文章同样显示购买/下载条。后台「文章管理」表单新增价格、关联程序(下拉取 auth 库 auth_app)、购买链接、下载地址四个字段。
后台左侧新增一级菜单「源码市场」(Base.php 里硬编码的菜单组,两个子项的 key 分别是 market / marketcat,必须与控制器名小写一致才能高亮):
app/admin/controller/Market.php + view/market/index.html):列市场分类及其子分类下的文章,支持按子分类 / 关键词筛选;行操作 = 编辑(跳文章表单)、改价(prompt 输入 → Market/price)、上下架(Market/toggle)、看前台。MarketCat.php + view/market_cat/index.html):维护「源码市场」下的子分类(名称/排序/启停/删除,分类下有源码时拦下不删)。cms_article_category 新增 parent_id(两级分类):老站由 CmsSchema::ensureArticleCatParent() 运行时自愈补列,新装随 install/schema.sql,增量 SQL install/update/market_cat_20260926.sql;后台文章表单的分类下拉改成一二级缩进( └ 子分类)。前台 /market 重做成参考站(xtuzy.cn「主题模板」分类页)的版式:深色横幅 + 搜索框、筛选条(分类 / 价格 / 排序,参数 cid / price / sort / kw)、卡片网格(付费/免费角标、分类、标题、日期与浏览量、橙色价格)。
setting() 的第二个参数必须是字符串(签名是 setting(string $name, string $default = ''))——写 setting('market_cat_id', 0) 直接 TypeError,要写 setting('market_cat_id', '0') 再 (int)。install/schema.sql 里按位置的 REPLACE INTO <表> VALUES (...) 种子行必须补足列数(MySQL 强制值个数=列数,和 INSERT 可省尾列不一样),否则新装直接 1136 报错;改完务必跑「整份 schema.sql 模拟安装」。MSYS_NO_PATHCONV=1 时 -o /tmp/x 会被 curl(Windows 程序)写到 C:\tmp\x,要和 bash 读到同一个文件就用 Windows 路径写 -o。