开发文档

产品使用文档与部署教程,持续更新中

首页>文档>开发文档

云枢官网授权系统 · 开发文档

一、技术架构

  • 框架:ThinkPHP 8.1 多应用模式(index 前台 + admin 后台),PHP 8.1+ / MySQL 5.7+;
  • 双库结构:官网库(cms_ 前缀)+ 授权库(auth_ 前缀),配置在 .env 的 [DATABASE] 与 [AUTH] 段;
  • 主控 / 被控判定:以 auth_config.auth_siteurl 是否指向当前站点为界——主站直连授权库,客户站远程请求主站接口(打包时注入主站域名);
  • 开放接口:public/check.php(授权验证 + 广告下发)、/auth/api/market(插件市场)、/auth/xjkey(加密包密钥下发)、plugin_api.php / apidata.php / download.php(插件与更新分发),共用 public/xjboot.php 轻量引导;
  • 路由:route_complete_match=true 需显式路由;controller(true) 返回小写紧凑名(AdManage→ad_manage 视图目录)。

二、目录结构

程序根/
├── 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         # 主站注入配置(含密钥,绝不随包分发)

三、关键机制

3.1 打包注入(package.php)

构建买家版发布包时,把 package.conf 的三项(主站域名 / 官网程序 uid / 授权密钥)替换进 install/auth.sql 的占位符。客户安装后 auth_config 里天然带主站指向:auth_siteurl(主站域名)、xj_app_uid、xj_app_key。授权验证、插件市场、远程广告全部由这三个值驱动,客户零配置。

3.2 授权验证(LicenseService)

接口地址 = auth_siteurl + /check.php;uid/key 优先读手动配置与本地授权库,客户站读注入值。激活后状态存 cms_setting(license_* 键),24 小时自动复验,复验失败即锁定后台。

3.3 远程广告(AdService)

AdService::grouped() 统一取广告(按 position 分组):主站直连 auth_ad(展示「全部产品 + 官网程序」的广告),客户站带签名请求主站 check.php 解密取 ads。前台仪表盘 6 格(home_dash_1..6)由该服务驱动。主站不可达时优雅降级为「广告位招租」。

3.4 广告位声明

存主站 auth_app.ad_positions 字段(后台程序管理编辑),不随程序包分发。上传程序包时自动扫描源码中 xj_ads_render('标识') 调用识别广告位;「远程广告 → 广告位管理」按程序筛选、设置价格,客户可自助付费申请。

3.5 插件市场(Market)

接口地址 = auth_siteurl + /auth/api/market;主站自身直连 auth_market 库。插件数据(免费/付费、上下架、下载地址)全部主站控制,客户端只读。

3.6 在线客服(ChatService)

前后台共用 app/service/ChatService.php 一个业务层,前台接口在 app/index/controller/Chat.php,后台接待在 app/admin/controller/Chat.php;详细设计见文末「2026-09-26 开发记录」。

3.7 依赖自愈(AuthSchema / CmsSchema)

授权库用 AuthSchema、官网库用 CmsSchema:运行时探测缺失的表/列并自动补齐(CREATE TABLE IF NOT EXISTS + 版本标记短路),与安装向导、在线更新 SQL 组成三条建表通道,老站升级不易漏表。

四、开发注意事项(踩坑清单)

  1. think-template:{for start="1" end="N"} 不含 end(需 end="N+1");{:} 标签是原生 PHP(数组用 $vo['key']);模板文本中的 JSON 花括号会被当模板标签解析;{volist} 会丢失关联数组 key(先转 list 结构)。
  2. Query 对象复用会累积 where:每个查询必须用新的 Db::connect()->name(),忌复用对象。
  3. 中文路径:GD 打不开中文路径字体,需复制到纯英文路径。
  4. php -S 单线程:本机开发时服务端不可自请求(Market/AdService 均有本机直连库分支,勿删)。
  5. 模板缓存:Windows 下模板 mtime 秒级精度,改完视图务必清 runtime/admin/temp/。
  6. 广告渲染安全:text 必须转义、image 校验协议、链接仅 http(s);主控保存时已做 XSS 过滤(script/iframe/on* 剥离),SDK 渲染仍保留二次校验。
  7. {:$list|count} 会编译成 echo $list|count; PHP 把 count 当常量直接致命错误、页面从该行截断——模板里统计数组请写 {:count($list)}。

2026-09-07 开发记录

加密系统四项修复(影响所有档 B 加密包买家)

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 字段),双写与过期清理按程序分支处理。

2026-09-26 开发记录:在线客服(实时聊天)

1. 数据模型(官网库 cms_chat_*,4 张表)

  • 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 条。

2. 三条建表通道

  1. 新装:install/schema.sql 已含 4 张表;
  2. 在线更新:install/update/chat_20260926.sql,主控在「程序管理 → 编辑程序 → 高级设置」把 sql=1、sqlfile 指向它,客户站更新时在官网库逐条执行(全部 CREATE TABLE IF NOT EXISTS,可重复执行;文件内不能出现会切碎语句的分号与行注释);
  3. 运行时自愈:CmsSchema::ensureChat(),探测 + 补建 + 写 chat_schema_ver 短路标记(后台接待页 initialize 时调用)。

3. 为什么用轮询而不是 WebSocket

站点经香港反代(审计绕行链路),WebSocket 升级头不透传,SSE 也会被缓冲,因此全程 HTTP 轮询:前台访客面板自适应间隔(对话活跃 0.8 秒 → 静默 30 秒后 3 秒 → 3 分钟后 5 秒),面板关闭或 document.hidden 时不轮询;后台接待页 1.5 / 3 / 5 秒,全局未读角标 15 秒。前台「客服回复 → 访客看到」实测约 0.7 秒。

4. 接口

  • 前台(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' => [...]])——写后台接口时按前者读返回值。

5. 关键实现约定

  • 访客身份一律服务端反查(cookie → user → 名下会话),不信任前端传来的 session_id;
  • 未读只有 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);
  • 限流按 IP 落文件锁:发送 1 条/秒、20 条/分、300 条/天;拉取 1 秒 3 次;上传 10 次/分;
  • 超时双层:惰性(open/send/pull/feed 顺手判 last_message_at + chat_timeout 立即关)+ 兜底计划任务 GET /chat/cron?key=chat_cron_key(宝塔每 5 分钟;key 存 cms_setting.chat_cron_key,后台设置弹窗可复制);cron 放行规则 = key 正确 / 后台已登录且未带 key / 尚未配置 key;
  • 降级:4 张表任一缺失或建表失败(主机无 CREATE 权限等)→ 前台浮窗自动退回原留言表单,接口返回 degraded=1,后台给出友好提示,绝不白屏。

6. 本次踩坑

  1. hidden 属性会被 CSS 的 display:flex 盖掉(浮窗面板一加载就显示)——对应选择器要补 [hidden] { display: none !important };
  2. {:$list|count} 在模板里会编译成 echo $list|count; 导致 PHP 致命错误、页面从该行截断(「自动回复」规则列表因此长期空白);
  3. 改前台/后台 JS、CSS 后必须升版本号(chat.js?v=、style.css?v=、admin.js?v=),否则浏览器用缓存会误判成功能没生效;
  4. 本机 php -S 单线程,自测并发请求会排队,别把排队当成「消息丢了」。

2026-09-26 开发记录(二):导航二级菜单富下拉

1. 数据模型

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 (...) 种子行不用改也不会报错。

2. 前台渲染

二级菜单不再用小的下拉卡片,改为 富下拉面板(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 子元素造出新的包含块,面板位置会跑偏。

3. 后台

「网站设置 → 导航管理」表单新增「菜单介绍」输入(200 字内,Nav::save() 写入 intro);手机端(≤768px)富下拉退回「缩进列表」形态,卡片拍平、左栏隐藏。

2026-09-26 开发记录(三):源码市场(把文章当商品卖)

1. 数据模型

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 (...) 种子行必须补够列数(见下「踩坑」)。

2. 权限判定(下载只给买家)

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,不会多查库。

3. 页面与后台

前台新增 /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)、购买链接、下载地址四个字段。

4. 后台「源码市场」菜单与两级分类

后台左侧新增一级菜单「源码市场」(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)、卡片网格(付费/免费角标、分类、标题、日期与浏览量、橙色价格)。

5. 本次踩坑

  1. setting() 的第二个参数必须是字符串(签名是 setting(string $name, string $default = ''))——写 setting('market_cat_id', 0) 直接 TypeError,要写 setting('market_cat_id', '0') 再 (int)。
  2. 给表加列后,install/schema.sql 里按位置的 REPLACE INTO <表> VALUES (...) 种子行必须补足列数(MySQL 强制值个数=列数,和 INSERT 可省尾列不一样),否则新装直接 1136 报错;改完务必跑「整份 schema.sql 模拟安装」。
  3. 本机验证 curl 时注意 Git-Bash 的路径改写:带 MSYS_NO_PATHCONV=1 时 -o /tmp/x 会被 curl(Windows 程序)写到 C:\tmp\x,要和 bash 读到同一个文件就用 Windows 路径写 -o。