开发约定

阅读当前文档内容。

文档

bot-admin 常用写法

这份文档用于统一后台模块的常见实现。新增页面、接口、列表、编辑页时先按这里写,确实不适合再局部说明原因,避免每个模块各写一套。


1. 文件放置

  • 后端接口放在 routers/,按业务域拆分,例如 routers/group.jsrouters/point.js
  • 前端页面逻辑放在 public/app/,文件名与路由名一致,例如 point_products.js
  • 页面模板放在 templates/,文件名与路由名一致,例如 point_products.html
  • 应用内路由需要登记 routers/app.jsrouteModuleMap
  • 应用内路由需要在 public/js/menu.js 加菜单项。
  • 应用内路由需要有 templates/public/app/ 对应文件。
  • 通用数据处理优先放到当前业务 router 顶部的小函数。跨 router 复用两次以上,再放到 utils/

2. 后端 router 写法

2.1 基础结构


import Router from "koa-router";

import monk from "monk";

import db from "../db.js";



let router = new Router();



router.post("/token/ca/get_demo_items", async (ctx) => {

    const { reg_token } = ctx.request.body || {};

    const items = await db.DEMO.find({ reg_token }, { sort: { _id: -1 } });

    const total = await db.DEMO.count({ reg_token });

    ctx.body = { items, total };

});



export default router;

  • 使用 ES module import/export default
  • router 实例统一写 let router = new Router();
  • 后台管理接口优先使用 /token/ca/... 前缀。
  • body/query 一律加兜底:ctx.request.body || {}ctx.query || {}
  • 数据库查询条件先组装成 query,不要在多个地方手写同一段条件。

2.2 返回格式

  • 列表接口返回 { listName, total },例如 { users, total }{ point_rules, total }
  • 详情接口返回 { itemName },例如 { rule }{ product }
  • 保存、删除、复制这类无额外数据的接口返回字符串 "ok"
  • 给前端弹出明确原因时返回 { error: "错误信息" }
  • 不要同一个模块里混用 ok: true"ok"{ status: "success" },除非是外部 API 兼容接口。

2.3 错误处理


if (!name) {

    ctx.throw(400, "名称不能为空");

}



if (!item) {

    ctx.status = 404;

    ctx.body = { error: "数据不存在" };

    return;

}

  • 参数校验失败优先用 ctx.throw(400, "...")
  • 需要给前端结构化错误时,设置 ctx.statusreturn
  • 不在每个接口外层重复包 try/catch。只有需要降级、清理资源、隐藏第三方错误时才局部捕获。
  • 捕获错误时不要吞掉上下文,至少保留 err.message

2.4 ID 与时间


const item = await db.DEMO.findOne({

    reg_token,

    _id: monk.id(id),

});



item.createTime = item._id.getTimestamp();

  • Mongo _id 查询统一使用 monk.id(id)
  • 列表展示创建时间时,用 _id.getTimestamp()createTime
  • 前端展示时间再用 dayjs 格式化,后端不混入展示文案。

2.5 账号范围

  • 接口必须带 reg_token 过滤,不能只按 _id 查。
  • 涉及子账号权限时,像积分模块一样先封装 getCurrentAccount(ctx)scopedRuleQuery(ctx, extra) 这类小函数。
  • owner/sub 的权限判断集中到函数里,不在每个接口散写 ctx.session?.account?.role

3. 前端页面写法

3.1 基础结构


BOT_ROUTES["demo_items"] = {

    el: "#bot-app",

    data() {

        return {

            list: [],

            total: 0,

            loading: false,

            dialogVisible: false,

            editing: defaultDemoItem(),

        };

    },

    async created() {

        await this.refresh();

    },

    methods: {

        async refresh() {

            const { data } = await axios.post(

                BOT_HOST + "/token/ca/get_demo_items",

                {

                    reg_token: window.tk,

                }

            );

            this.list = data.items || [];

            this.total = data.total || 0;

        },

    },

};



function defaultDemoItem() {

    return {

        title: "",

        active: true,

        sort: 0,

    };

}

  • BOT_ROUTES["route_name"] 必须与模板、菜单、routers/app.js 路由名一致。
  • 页面初始化统一放 created()refresh()
  • 列表数据字段统一叫 list,总数叫 total。除非模板已有明确业务名。
  • 新建默认值抽成 defaultXxx(),新增和编辑都复用,避免字段漏填。

3.2 请求写法


const { data } = await axios.post(BOT_HOST + "/token/ca/set_demo_item", {

    reg_token: window.tk,

    form: this.editing,

});

  • 后台接口统一带 reg_token: window.tk
  • 保存接口表单统一放 form 字段。
  • 页面跳转使用 barba.go("route?key=value")window.navigateAppLink(),不要直接拼完整 /app/${window.tk}/...
  • 读取 URL 参数使用 new URLSearchParams(location.search).get("key") || ""

3.3 常用交互


async remove(item) {

    if (!confirm("确认删除该数据?")) return;

    await axios.post(BOT_HOST + "/token/ca/rm_demo_item", {

        reg_token: window.tk,

        id: item._id,

    });

    await this.refresh();

},

async save() {

    await axios.post(BOT_HOST + "/token/ca/set_demo_item", {

        reg_token: window.tk,

        form: this.editing,

    });

    this.dialogVisible = false;

    await this.refresh();

},

  • 删除前确认,删除后 refresh()
  • 保存成功后先关闭弹窗,再 refresh()
  • 开关类操作直接提交完整业务对象加变更字段,保存后刷新。
  • 需要防重复点击时加 loadingsaving,在 finally 里恢复。

3.4 前端错误提示


try {

    await this.saveData();

    this.$message?.success?.("保存成功");

} catch (err) {

    const message = err?.response?.data?.error || err?.response?.data || "保存失败";

    if (this.$message?.error) {

        this.$message.error(message);

    } else {

        alert(message);

    }

}

  • 用户可处理的失败要提示,不只 console.error
  • 后端返回 { error } 时优先展示 error
  • 只有调试信息才写 console.error

4. 命名约定

  • 列表接口:get_xxxs
  • 详情接口:get_xxx
  • 保存接口:set_xxx
  • 删除接口:rm_xxx
  • 复制接口:copy_xxx
  • 执行接口:run_xxxsend_xxx
  • 同步接口:sync_xxx
  • 前端列表页:xxxs
  • 前端编辑页:xxx_edit

示例:

  • get_group_replys
  • get_group_reply
  • set_group_reply
  • rm_group_reply
  • copy_group_reply
  • group_replys
  • group_reply_edit

5. 模板约定

  • templates/*.html 只放当前页面结构,不写大量业务脚本。
  • 页面脚本放 public/app/*.js
  • 复用选择器使用已有公共模板,例如 selector_usersselector_groupsselector_messages
  • 列表页保留空数据状态,按钮文案与接口动作一致。
  • 编辑页的字段名尽量与后端 form 字段一致,不做二次映射。

6. 新增模块检查清单

  • 已新增 routers/<module>.js 或追加到对应业务 router。
  • 已在 routers/index.js 导出并在 bot-admin.js 挂载。
  • 已新增 templates/<route>.html
  • 已新增 public/app/<route>.js
  • 已在 routers/app.js 登记 routeModuleMap
  • 已在 public/js/menu.js 加菜单入口。
  • 接口查询都带 reg_token
  • 保存接口使用 form
  • 删除/详情接口同时校验 reg_token_id
  • 前端新增、编辑共用 defaultXxx()
  • 不新增与现有模块重复但命名不同的 helper。

7. 什么时候抽公共 helper

  • 同一段数据归一化逻辑出现第二次,抽到当前 router 顶部函数。
  • 同一段逻辑跨两个 router 出现,抽到 utils/
  • 只被一个接口使用的一次性逻辑,留在接口附近即可。
  • 不为了“看起来通用”提前抽象;后台模块更需要稳定一致,少需要复杂继承。