├── .gitignore ├── FULLTEXT.md ├── README.assets ├── apijson_function.png ├── image-20210801223053319.png ├── image-20210801223117628.png ├── image-20210801223138414.png ├── image-20210801223203817.png └── image-20210801223225240.png ├── README.md ├── apijson-demo.iml ├── demo.postman_collection.json ├── initdb_final.sql ├── pom.xml └── src └── main ├── java └── apijson │ └── demo │ ├── DemoApplication.java │ ├── config │ ├── DemoFunctionParser.java │ ├── DemoSQLConfig.java │ └── JSONWebConfig.java │ ├── controller │ └── DemoController.java │ └── model │ ├── Credential.java │ ├── Todo.java │ └── User.java └── resources └── application.properties /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/java,java-web,intellij 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=java,java-web,intellij 4 | 5 | ### Intellij ### 6 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 7 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 8 | 9 | # User-specific stuff 10 | .idea/**/workspace.xml 11 | .idea/**/tasks.xml 12 | .idea/**/usage.statistics.xml 13 | .idea/**/dictionaries 14 | .idea/**/shelf 15 | 16 | # AWS User-specific 17 | .idea/**/aws.xml 18 | 19 | # Generated files 20 | .idea/**/contentModel.xml 21 | 22 | # Sensitive or high-churn files 23 | .idea/**/dataSources/ 24 | .idea/**/dataSources.ids 25 | .idea/**/dataSources.local.xml 26 | .idea/**/sqlDataSources.xml 27 | .idea/**/dynamic.xml 28 | .idea/**/uiDesigner.xml 29 | .idea/**/dbnavigator.xml 30 | 31 | # Gradle 32 | .idea/**/gradle.xml 33 | .idea/**/libraries 34 | 35 | # Gradle and Maven with auto-import 36 | # When using Gradle or Maven with auto-import, you should exclude module files, 37 | # since they will be recreated, and may cause churn. Uncomment if using 38 | # auto-import. 39 | # .idea/artifacts 40 | # .idea/compiler.xml 41 | # .idea/jarRepositories.xml 42 | # .idea/modules.xml 43 | # .idea/*.iml 44 | # .idea/modules 45 | # *.iml 46 | # *.ipr 47 | 48 | # CMake 49 | cmake-build-*/ 50 | 51 | # Mongo Explorer plugin 52 | .idea/**/mongoSettings.xml 53 | 54 | # File-based project format 55 | *.iws 56 | 57 | # IntelliJ 58 | out/ 59 | 60 | # mpeltonen/sbt-idea plugin 61 | .idea_modules/ 62 | 63 | # JIRA plugin 64 | atlassian-ide-plugin.xml 65 | 66 | # Cursive Clojure plugin 67 | .idea/replstate.xml 68 | 69 | # Crashlytics plugin (for Android Studio and IntelliJ) 70 | com_crashlytics_export_strings.xml 71 | crashlytics.properties 72 | crashlytics-build.properties 73 | fabric.properties 74 | 75 | # Editor-based Rest Client 76 | .idea/httpRequests 77 | 78 | # Android studio 3.1+ serialized cache file 79 | .idea/caches/build_file_checksums.ser 80 | 81 | ### Intellij Patch ### 82 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 83 | 84 | # *.iml 85 | # modules.xml 86 | # .idea/misc.xml 87 | # *.ipr 88 | 89 | # Sonarlint plugin 90 | # https://plugins.jetbrains.com/plugin/7973-sonarlint 91 | .idea/**/sonarlint/ 92 | 93 | # SonarQube Plugin 94 | # https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin 95 | .idea/**/sonarIssues.xml 96 | 97 | # Markdown Navigator plugin 98 | # https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced 99 | .idea/**/markdown-navigator.xml 100 | .idea/**/markdown-navigator-enh.xml 101 | .idea/**/markdown-navigator/ 102 | 103 | # Cache file creation bug 104 | # See https://youtrack.jetbrains.com/issue/JBR-2257 105 | .idea/$CACHE_FILE$ 106 | 107 | # CodeStream plugin 108 | # https://plugins.jetbrains.com/plugin/12206-codestream 109 | .idea/codestream.xml 110 | 111 | ### Java ### 112 | # Compiled class file 113 | *.class 114 | 115 | # Log file 116 | *.log 117 | 118 | # BlueJ files 119 | *.ctxt 120 | 121 | # Mobile Tools for Java (J2ME) 122 | .mtj.tmp/ 123 | 124 | # Package Files # 125 | *.jar 126 | *.war 127 | *.nar 128 | *.ear 129 | *.zip 130 | *.tar.gz 131 | *.rar 132 | 133 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 134 | hs_err_pid* 135 | 136 | ### Java-Web ### 137 | ## ignoring target file 138 | target/ 139 | 140 | # End of https://www.toptal.com/developers/gitignore/api/java,java-web,intellij -------------------------------------------------------------------------------- /FULLTEXT.md: -------------------------------------------------------------------------------- 1 | # APIJSON Todo Demo 2 | 3 | 一个试图让 APIJSON 上手更简单一些的尝试。 4 | 5 | 本示例项目是一个基于 APIJSON 实现的 todo 系统,在官方示例项目(APIJSON-Demo)的基础上进一步简化了数据库和代码,完整实现了对一个业务表的单独/批量 CRUD 操作,并描述了如何用远程函数实现一个简单的自定义鉴权逻辑。 6 | 7 | :warning: 本项目文本最后更新于 2021 年 8 月(APIJSON 版本 4.7.2),后续维护只是跟随官方更新版本号,文中的文字描述和技术原理可能已经发生变更。如果在阅读时注意到不一致之处,欢迎提出 issue 和 pull request,我会在力所能及的范围内尽量修复。 8 | 9 | :warning: 部分内容为个人猜测,不保证正确性,如有错漏,还请指正! 10 | 11 | ## 引言 12 | 13 | APIJSON 是一个很有趣的框架,但是官方文档分散在各处(README,issues,code),加之官方示例中数据库内数据较多,各个功能代码庞杂,作为新手想要上路还是有一些困难的。官方文档对如何使用各个操作符完成查询着墨较多,但是对于如何从零开始搭建一个系统讲的比较少。本项目想要尝试补全这一空白。 14 | 15 | 对于一个 CRUD 系统,最常见的 hello world 项目莫过于一个简单的 todo (待办事项) 系统。通常而言,能够自己实现一个这样的系统,基本就对这个系统入了门,可以开始将这个系统为我所用了。本 Repo 中已经完成了这个系统。你可以自己尝试从头实现,用这个项目作为参考,也可以基于这个项目扩展功能。 16 | 17 | 18 | 19 | ## 初始化这个项目 20 | 21 | 1. 把整个项目 clone 到本地 22 | 23 | 2. 用根目录下的 initdb_final.sql 初始化数据库 24 | 25 | 3. 用 IDEA 打开项目,进入 pom.xml ,右键 Maven - Reload 26 | 27 | > 如果遇到了 Maven 报错提示找不到 APIJSON 包,请参考「杂项」一节中[「Maven 报错无法找到 APIJSON 包」](https://github.com/jerrylususu/apijson_todo_demo/blob/master/FULLTEXT.md#maven-报错无法找到-apijson-包) 28 | 29 | 4. 进入 apijson/demo/config/DemoSQLConfig.java 修改数据库配置,包括数据库类型,默认 schema 名,数据库版本号,JDBC 连接字符串,数据库用户名和密码 30 | 31 | 5. (推荐)打开 Postman,导入根目录下的 demo.postman_collection.json,其中含有一些预先准备好的请求 32 | 33 | 6. 进入 DemoApplication,右键 Run 'DemoApplication' 34 | 35 | 36 | 37 | 如果你想要更深入的探究框架的内部工作机制,推荐你在 pom.xml 中禁用 apijson-framework 和 APIJSON 两个依赖项,然后手动去 [APIJSON/APIJSONORM](https://github.com/Tencent/APIJSON/tree/master/APIJSONORM) 和 [APIJSON/apijson-framework](https://github.com/APIJSON/apijson-framework) 把这两个依赖的源代码下载回来,然后放置到 src 文件夹下。这一步完成后,你的 src/main/java/apijson 下应该有 demo, framework, orm 三个文件夹,以及一些从 APIJSONORM 复制过来的一些文件。这样做的好处是当你断点/搜索的时候,可以更容易的访问到框架内部代码,也可以修改内部代码,观察会发生什么变化。 38 | 39 | 40 | 41 | ## 项目场景简介 42 | 43 | 本项目中我们实现了一个自定义权限的待办事项系统。 44 | 45 | 在这个系统中,每个用户有一个自己的 friends 列表,而每个 todo item 都有一个自己的 helper 列表。todo 对所有用户可见,但是只有其创建者可以删除。当前登录的用户,可以创建新的 todo,增加/删除自己的 friend,也可以增加/删除自己所创建的 todo item 中的 helper 列表。比较复杂的是对 todo 的修改操作,你可以想象成一个简单的共享模型。一个用户可以修改某个 todo,如果 46 | 47 | * 这个用户是这个 todo 的创建者,或者 48 | * 这个用户在这个 todo 的创建者的 friend 列表中,或者 49 | * 这个用户在这个 todo 的 helper 列表中。 50 | 51 | 52 | 53 | ## 框架相关 54 | 55 | ### 必备参考链接 56 | 57 | 设计规范:https://github.com/Tencent/APIJSON/blob/master/Document.md#3 58 | 59 | 提问前必看:https://github.com/Tencent/APIJSON/issues/36 60 | 61 | 实现原理:https://github.com/Tencent/APIJSON/wiki#%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86 62 | 63 | 「详细的说明文档」:https://github.com/Tencent/APIJSON/blob/master/%E8%AF%A6%E7%BB%86%E7%9A%84%E8%AF%B4%E6%98%8E%E6%96%87%E6%A1%A3.md 64 | 65 | 文档网站:https://vincentcheng.github.io/apijson-doc/zh/newinterface.html#%E5%90%8E%E5%8F%B0%E6%B7%BB%E5%8A%A0%E6%95%B0%E6%8D%AE%E8%A1%A8 66 | 67 | 路线图:https://github.com/Tencent/APIJSON/blob/3c9084479876748055ebe048d388098ef05c3e23/Roadmap.md 68 | 69 | QQ 技术群: 734652054(新)、607020115(旧) 70 | 71 | ### 访问方法、请求体与 Request 72 | 73 | APIJSON 支持以下的访问方法: 74 | 75 | * GET 获取 76 | * HEAD 计数 77 | * GETS 安全/限制性获取 78 | * HEADS 安全/限制性计数 79 | * POST 新增 80 | * PUT 修改 81 | * DELETE 删除 82 | 83 | 这些访问方法又被分为两类: 84 | 85 | * GET / HEAD 是开放请求,可以随意组合嵌套 86 | * 其他方法是私密/非开放请求,需要符合预先定义好的安全规则才能调用 87 | 88 | > 注:GETS 不支持返回多条记录(参见 https://github.com/Tencent/APIJSON/issues/273) 89 | 90 | 对于私密/非开放请求,其安全规则在 Request 表中定义,包含以下几个部分(见 「设计规范」 3.1 节底部注释) 91 | 92 | * method,即这条规则所适用的访问方法 93 | * tag,某条规则的名字/标识,大部分情况下为操作的表的名字。(但是也有一些例外,见后文) 94 | * version,版本号。不传,为 null 或 <=0 都会使用相同 method + 相同 tag 的版本号最高的规则,如果制定了版本则使用对应版本的规则 95 | * structure,结构,是一个描述请求体需要满足的结构的 JSON 96 | 97 | 以下是一条规则的示例。 98 | 99 | | version | method | tag | structure | 100 | | ------- | ------ | ---- | ------------------------------------------------------------ | 101 | | 1 | POST | Todo | {"MUST": "title", "UPDATE": {"@role": "OWNER"}, "REFUSE": "id"} | 102 | 103 | 这条规则规定了如果想要用 POST 方法在 Todo 表中添加一行数据,请求体中必须要有 `"tag":"Todo"`,且请求体中必须有 title,不能有 id,而且权限(`@role`)会被覆盖为 OWNER。以下是一个满足该规则的请求示例。关于规则中可以使用的关键词,可以参见 apijson.orm.Operation 中的注释。 104 | 105 | ```json 106 | { 107 | "Todo": { 108 | "title": "write doc", 109 | "note": "apijson quickstart" 110 | }, 111 | "tag": "Todo" 112 | } 113 | ``` 114 | 115 | 在 APIJSON,对于一个业务表(在我们的项目中是 `Todo` 表)的 CRUD 操作,查询已经由 GET / HEAD 帮我们代办了,我们需要完成的就是在 Request 表中补全对应表的 POST/PUT/DELETE 操作规则。这三个操作都有单独操作和批量操作两个变种,PUT的批量操作也有统一设置和单独设置两种形式(见「设计规范」3.1节),因此对于一个业务表,通常需要在 Request 表中增加七条规则。 116 | 117 | 在上面的 POST 请求示例中,你可能会注意到,我们的请求体似乎只有在 `"Todo"` 内才满足 structure 要求,但是就整个 JSON 而言并不满足。这就引出了 tag 为表名时的特殊作用:如果 tag 是表名,APIJSON 会自动帮我们把规则中的 structure 外面用表名包上一层。(https://github.com/Tencent/APIJSON/issues/115#issuecomment-565733254) 118 | 119 | * 判定 tag 是否是表名,是根据其开头首字母是否是大写来判断的 (见 apijson.JSONObject.isTableKey) 120 | 121 | 换句话说,对于同一个 structure,进行校验的时候,实际上是 122 | 123 | ```json 124 | {"Todo": {"MUST": "title", "UPDATE": {"@role": "OWNER"}, "REFUSE": "id"}} 125 | ``` 126 | 127 | 这样看来就和我们的请求能对上了。 128 | 129 | 与之对应的,如果 tag 不是表名(即首字母不是大写),那么就必须要在 structure 内写出完整的 JSON 结构要求。一般有两种情况会需要设定 tag 不是表名的规则。一是多表操作,例如注册,需要同时在 User 表(用户公开信息)和 Credential 表(用户隐私信息,官方示例中为 Privacy 表)中进行插入。以下是一个用于注册的规则示例。 130 | 131 | | version | method | tag | structure | 132 | | ------- | ------ | ------------ | ------------------------------------------------------------ | 133 | | 1 | POST | api_register | {"User": {"MUST": "username,realname", "REFUSE": "id", "UNIQUE": "username"}, "Credential": {"MUST": "pwdHash", "UPDATE": {"id@": "User/id"}}} | 134 | 135 | 这条规则看起来复杂了很多,但是可以从外到内分析。首先最外层的两个 Key,分别定义了这个 POST 请求要操作的两张表。然后每个表内部,又定义了针对这张特定的表所需要的输入要求。UNIQUE 要求在 User 表中 username 唯一,UPDATE 中 id@ 则是一个引用赋值,指在 User 表插入完成后,用 User 表中的 id 作为 Credential 表的 id。(APIJSON 默认使用毫秒精度的当前时间戳作为主键,但可以修改为自增主键,见「提问前必看 11.如何使用自增主键?」)以下是一个满足该要求的请求体。 136 | 137 | ```json 138 | { 139 | "User": { 140 | "username": "user", 141 | "realname": "apijson" 142 | }, 143 | "Credential": { 144 | "pwdHash": "2333" 145 | }, 146 | "tag": "api_register" 147 | } 148 | ``` 149 | 150 | 请注意,请求体中的相对顺序十分重要!在 Structure 中我们让 Credential 引用了 User/Id,那么在请求体中我们也必须要让 Credential 在 User 之后,不然这个引用赋值不会成功,而且返回的结果帮不上什么忙。(见 [建议:在输入引用赋值顺序错误时给出更明显的提示 · Issue #275 · Tencent/APIJSON (github.com)](https://github.com/Tencent/APIJSON/issues/275))。 151 | 152 | 另一种需要 tag 非表名的情况是对某些特定列的操作,例如以下规则,可以在某条 todo 上增加一个 helper。(也可以参见官方示例 APIJSONBoot 的 balance+, balance- 两条规则。) 153 | 154 | | version | method | tag | structure | 155 | | ------- | ------ | ------- | ------------------------------------------------------------ | 156 | | 1 | PUT | helper+ | {"Todo": {"MUST": "id,helper+", "INSERT": {"@role": "OWNER"}}} | 157 | 158 | 满足此规则的一个请求体如下。 159 | 160 | ```json 161 | { 162 | "Todo": { 163 | "id": 1627565018422, 164 | "helper+": [1627508518581] 165 | }, 166 | "tag": "helper+" 167 | } 168 | ``` 169 | 170 | 其中,+号代表「增加或扩展」功能符(见「设计规范 3.2」),当然也有-号,代表「减少或去除」。在这个例子里,helper 是一个 `List`,序列化存入 DB 后是一个 `JSONArray`。不幸的是,因为 structure 中 key 部分要求完全相等,即 `helper+` 无法匹配 `helper-`,所以需要还需要一条规则来在某条 todo 上减少一个 helper。 171 | 172 | 顺带一提,在前文提到的批量 POST/PUT/DELETE 中,structure 也可以正常使用,只需要把需要判断的部分用数组包起来,就可以应用到包含数组的请求体中了。请见下面这个规则,用于一次新建多条 todo。注意用于单个 todo 的 MUST 和 REFUSE 规则现在被包在一个数组里。 173 | 174 | | version | method | tag | structure | 175 | | ------- | ------ | ------- | ------------------------------------------------------------ | 176 | | 1 | POST | Todo:[] | {"Todo[]": [{"MUST": "title", "REFUSE": "id"}], "UPDATE": {"@role": "OWNER"}} | 177 | 178 | 这个 tag 名看起来可能有些怪异(为什么后面有 `:[]`?)。其实这是一个 APIJSON 框架的约定,代表这是一个批量操作,其中每个对象被独立描述,然后被收集在一个数组中。以下是一个满足该规则的请求体。 179 | 180 | ```json 181 | { 182 | "Todo[]": [ 183 | { 184 | "title":"hi there", 185 | "note": "apijson" 186 | }, 187 | { 188 | "title": "today is good", 189 | "note": "thanks to apijson" 190 | } 191 | ], 192 | "tag": "Todo:[]" 193 | } 194 | ``` 195 | 196 | 为了勾起读者的好奇心,以下是一个删除单一 todo 的规则。请猜猜看 REFUSE 中的 `!` 代表的含义。 197 | 198 | | version | method | tag | structure | 199 | | ------- | ------ | ---- | ----------------------------------------------------------- | 200 | | 1 | DELETE | Todo | {"MUST": "id", "REFUSE": "!", "INSERT": {"@role": "OWNER"}} | 201 | 202 | (答案:拒绝除了 MUST 中声明的 key 之外的其他所有 key。参见官方示例 APIJSONDemo Request 表 id=8) 203 | 204 | ### 访问控制、角色和 Access 表 205 | 206 | > 在早期的 APIJSON 版本中,需要需要用 `@MethodAccess` 注解来标注某个类对应的表的访问权限,但是现在的版本中直接改 Access 表就可以了。 207 | 208 | 权限系统是现代服务中绕不过的一个话题,APIJSON 也针对这一问题交出了自己的答卷。Access 表就是 APIJSON 的权限中心,其中定义了对于数据库中的每张表,哪些角色(RequestRole)可以调用什么请求方法。 209 | 210 | APIJSON 中的默认角色有以下几类:(参见 https://github.com/Tencent/APIJSON/issues/67#issuecomment-453428079,具体实现参见 AbstractVerifier.verifyAccess 方法) 211 | 212 | - UNKNOWN:用户未登录时的默认角色(用户登录用的是服务端的 HttpSession,这里即 userId <=0 或为 null) 213 | 214 | - LOGIN:用户登录后的默认角色(userId > 0) 215 | 216 | - OWNER:请求对象的创建者是当前用户(userId = $currentUserId) 217 | 218 | - CONTACT:请求对象的创建者在当前用户的好友列表中(userId IN( $currentContactIdList ) ) 219 | 220 | - CIRCLE:请求对象的创建者在 当前用户的好友列表加上当前用户自己 得到的列表中 221 | 222 | (userId IN( $currentCircleIdList ) // currentCircleIdList = currentContactIdList.add(currentUserId)) 223 | 224 | * ADMIN:管理员权限,默认不支持,需要手动重载 verifyAdmin 方法实现。 225 | 226 | 其中: 227 | 228 | * UNKNOWN,LOGIN 是默认角色,会根据用户的登陆与否自动分配 229 | * 其他角色需要用户在请求体中手动用类似于 `"@role" = "OWNER"` 的形式声明 230 | 231 | APIJSON 的这个权限模型应该是从微信朋友圈获得的灵感,毕竟在刷朋友圈的时候可以看到自己+联系人的动态。这也难怪为什么官方示例的数据库是一个类似于朋友圈的项目了。不幸的是,对于许多其他项目,这一权限模型可能不是很合适。例如在我们这个示例项目的场景中,要判定一个 todo 的能否修改,不仅要判断创建者的 friend,还要判断 todo 本身的 helper。另外,即使我们忽略 helper 这个需求,我们需要判定的是「当前用户」是否在「todo 创建者的 friend」列表中,而不是「todo 创建者」是否在「当前用户的 friend」列表中,因此 CIRCLE 角色对我们是没有帮助的。 232 | 233 | 了解了角色之后,Access 表也就很简单了:每一行是一张表,每一列是一个访问方法,每一格则是角色数组。只有在这个数组内的角色才能用这个访问方法访问这张表。但是具体设定起来,可能还是会有些头大。以下是一个可能的设置方法: 234 | 235 | * 读可以放开,写尽量谨慎。 236 | 237 | * 对于用户公开信息(User)、用户隐私信息(Credential/Privacy)、远程函数(Function)表,参考 APIJSONDemo 中的设定方式。 238 | * 对于业务表,如果资源登陆后可见则不设定 UNKNOWN。每个操作最好都允许 ADMIN。POST 一般 LOGIN 就足够了。DELETE 一般需要 OWNER。PUT 视情况而定,通常是 OWNER。如果有自定义鉴权逻辑的需求,则需要加上 LOGIN,因为请求中不带 `@role` 的时候登陆后默认身份就是 LOGIN。 239 | 240 | 需要注意的是,Access 表的权限设定是只对外部请求生效的。如果在后端代码内手动构造并发起 APIJSON 请求,是可以绕过内置的权限控制的,如下代码所示,只需要设定 needVerify 为 false 即可。 241 | 242 | ```json 243 | // APIJSONParser(RequestMethod method, boolean needVerify) 244 | JSONObject response = new APIJSONParser(POST,false).parseResponse(request); 245 | ``` 246 | 247 | > 注:此处还有一些其他的设定选项,如 setNeedVerifyLogin(验证登录态,内部调用需要设定 false), setNeedVerifyContent(验证请求是否满足 Request 中 Structure 定义) 248 | 249 | 此外,APIJSON 还支持隐藏特定列,只需要在数据库中将列名开头设定为下划线,则该列完全不会出现在 APIJSON 的返回中,无论是内部调用还是外部调用都如此。这一方面的示例可以参见官方示例 APIJSONBoot 中 `login` 方法的实现。在那个实例中,用户密码存储在列名为 `_password` 的列中,登陆的时候后端构造一个带有用户 id 和密码的 HEAD 请求(两个条件之间是 AND),用 APIJSON 调用后查看返回的 count 是否为 1,以此来判断用户 id 和密码是否正确。这样的确可以实现信息的隐藏,但稍有不足之处是无法再使用 BCryptEncrypter 等需要密码哈希的密码验证器,因为无法拿回数据库中存储的密码哈希,只能通过 HEAD 间接验证相等。 250 | 251 | 以下是对本节的一个简要总结。 252 | 253 | | 控制粒度 | 实现 | APIJSON 内部可绕过 | 254 | | -------- | ---------------------------- | ------------------------------- | 255 | | 表级 | 角色:UNKNOWN, LOGIN, ADMIN | 可以 | 256 | | 行级 | 角色:OWNER, CONTACT, CIRCLE | 可以 | 257 | | 列级 | 数据库:列名前加下划线 | APIJSON 内不可,直接执行 SQL 可 | 258 | 259 | ### 远程函数和 Function 表 260 | 261 | APIJSON 大部分操作都是直接在数据库层面完成的,那么如何在 APIJSON 中调用业务代码呢?答案是远程函数。(个人理解)与传统开发中业务代码为主体不同,在 APIJSON 的视角中,现在大部分公司实际上做的都是 CRUD 的活,因此可以把业务代码抽象为一种 CRUD 的副作用。在 APIJSON 的请求中,如果某个请求的 key 后有 `()`,那么就会被解析为一个远程函数调用请求,从而触发对应的远程函数,并且把结果以相同的 key 返回。从这个层面上看,远程函数几乎类似于数据库中的一个虚拟的列了。 262 | 263 | 要实现一个远程函数,不仅需要写函数本身的代码,也需要在 Function 表中注册这个函数。以一个最简单的 `sayHello` 远程函数为例子(在本项目中位于 apijson.demo.config.DemoFunctionParser#sayHello)。首先,我们需要 extend APIJSONFunctionParser。在远程函数调用的时候,实际上是从 AbstractParser - APIJSONFunctionParser - DemoFunctionParser 一路找下来的。 264 | 265 | ```java 266 | public class DemoFunctionParser extends APIJSONFunctionParser {} 267 | ``` 268 | 269 | 然后可以实现具体的方法体。(需要注意这里的 name 并不是参数值,而是参数值所在的 key。) 270 | 271 | 远程函数的返回值一般都是 Object,参数一般是一个 JSONObject 跟着 0 个到多个 String。对远程函数而言,如果返回 null,则只会调用函数,而不会在响应中显示。如果抛出异常,则会中止当前请求。如果返回一个特定的对象,则会被序列化为 JSON,并在响应中显示出来。 272 | 273 | ```java 274 | public Object sayHello(@NotNull JSONObject current, @NotNull String name) throws Exception{ 275 | // 注意这里参数 name 是 key,不是 value 276 | Object obj = current.get(name); 277 | if (obj == null ){ 278 | throw new IllegalArgumentException(); 279 | } 280 | if (!(obj instanceof String)){ 281 | throw new IllegalArgumentException(); 282 | } 283 | return "Hello, " + obj.toString(); 284 | } 285 | ``` 286 | 287 | 还要在 Function 表中注册函数。(这里填写的 demo 会在 APIJSONApplication.init 方法中用于远程函数的测试,在应用启动的时候会被调一次。) 288 | 289 | 注意 name 需要完全等于 Java 中的方法名,arguments 的顺序需要完全和 Java 中的方法参数顺序一致(逗号连接,省去第一个 current)。 290 | 291 | | name | arguments | demo | 292 | | -------- | --------- | ---------------- | 293 | | sayHello | name | {"name": "test"} | 294 | 295 | 以下是一个简单的调用示例。(POST /get) 296 | 297 | ```json 298 | { 299 | "name": "jerry", 300 | "ref()": "sayHello(name)" 301 | } 302 | ``` 303 | 304 | 返回值也正如预期。 305 | 306 | ```json 307 | { 308 | "name": "jerry", 309 | "ref": "Hello, jerry", 310 | "ok": true, 311 | "code": 200, 312 | "msg": "success" 313 | } 314 | ``` 315 | 316 | 317 | 318 | 然后来看看在远程函数中如何和数据库交互。传统开发中,后端本身用 JPA / Mybatis 之类的工具和数据库交互,前端则是被动地使用后端建立好的各种接口。但是在使用 APIJSON 时,前后端面对的数据库交互界面都是 APIJSON,并没有格式和表达能力上的区分,区别只是后端可以手动跳过验证罢了。这两种开发方式的区别如下图所示。 319 | 320 | apijson_function 321 | 322 | 以这个项目的特殊权限需求为例。对于 PUT todo 的请求,需要得到被操作的 todo 的 id,以及当前登录用户的 userId。随后再从数据库中查出这条 todo 的 helper 列表及创建者的 friend 列表。首先,我们可以用 `this.session` 在远程函数中拿到当前的用户 session,进而拿到用户 id。其次,我们可以利用 Request 的 Structure 规则在 PUT 请求体中插入一个远程调用请求,并拿到 todo 的 id。最后,在我们的远程函数内部,我们可以请求数据库,拿到 todo 的的 helper 列表及创建者的 friend 列表。 323 | 324 | 首先看看在 Function 中函数的注册信息。(demo 中的 id 其实可以随便写,因为远程函数测试的时候是没有 session 的,测了也没有作用,只要不抛异常就行。) 325 | 326 | | name | arguments | demo | 327 | | ---------------- | --------- | --------------- | 328 | | isUserCanPutTodo | todoId | {"todoId": 123} | 329 | 330 | 具体的函数实现,请参考 apijson.demo.config.DemoFunctionParser#isUserCanPutTodo。其中我们先构造了一个 APIJSON 请求,根据 todoid 同时得到对应的 User 和 Todo 对象。(不用 APIJSON 的话很可能需要两个请求。)注意,这里的顺序也很重要,否则引用赋值可能失败。 331 | 332 | ```java 333 | JSONObject todoRequest = new JSONRequest(); 334 | todoRequest.put(TODO_CLASS_NAME, new apijson.JSONObject(new Todo().setId(TodoId)).setJson("helper")); 335 | JSONObject userRequest = new JSONRequest().fluentPut("id@", "/" + TODO_CLASS_NAME + "/userId").fluentPut("@json", "friends"); 336 | todoRequest.put(DemoController.USER_CLASS_NAME, userRequest); 337 | ``` 338 | 339 | 构造出的请求如下,其中指定了 Todo.id,并用 Todo 的 userId 赋值给 user。(`@json` 是转为 JSON 格式返回,如果不开启即使数据库中存的是 JSONArray / JSONObject,得到的也是字符串。见「设计文档 3.2 节 对象关键词 8」) 340 | 341 | ```json 342 | { 343 | "Todo": { 344 | "id": 1627761702477, 345 | "@json": "helper" 346 | }, 347 | "User": { 348 | "id@": "/Todo/userId", 349 | "@json": "friends" 350 | } 351 | } 352 | ``` 353 | 354 | 然后从后端发起请求,并用 `needVerify=false` 跳过验证。 355 | 356 | ```java 357 | JSONObject response = new APIJSONParser(GET, false).parseResponse(todoRequest); 358 | ``` 359 | 360 | 拿到结果后就可以反序列化成对象,进行判断了。 361 | 362 | ```java 363 | JSONResponse todoResponse = new JSONResponse(response); 364 | Todo todo = todoResponse.getObject(Todo.class); 365 | User user = todoResponse.getObject(User.class); 366 | 367 | if (todo.getUserId().equals(uid)) { 368 | // current user is creator 369 | continue; 370 | } else if (user.getFriends().contains(uid)) { 371 | // current user in creator's friend list 372 | continue; 373 | } else if (todo.getHelper() != null && todo.getHelper().contains(uid)) { 374 | // current user in todo's helper list 375 | continue; 376 | } 377 | 378 | // 以上验证都没有通过 379 | throw new IllegalAccessException("user don't have permission to put todo!"); 380 | ``` 381 | 382 | 鉴权函数完成后,为了能让 PUT 请求都能触发鉴权函数,还需要在 Request 表中 PUT 项内加上对应的调用。这样即使请求体内没有显式调用鉴权函数,在经过处理后也会调用。(这里的处理指的是 Parser.ParseCorrectRequest,会检查输入是否满足 Structure 并引用变换。) 383 | 384 | | version | method | tag | structure | 385 | | ------- | ------ | ---- | ------------------------------------------------------------ | 386 | | 1 | PUT | Todo | {"Todo":{ "MUST":"id","REFUSE": "userId", "UPDATE": {"checkCanPut-()": "isUserCanPutTodo(id)"}} } | 387 | 388 | 其中 UPDATE 是不存在则插入,存在则覆盖。`()` 前的 `-` 则制定了这个远程函数的执行优先级,在解析当前对象之前。(见「设计文档 3.2 节 远程调用函数」) 389 | 390 | 对于批量 PUT 请求,还需要在函数内部判断输入的是单个 id 还是一个 id 的 JSONArray。 391 | 392 | 前文提及,在 APIJSONApplication 启动的时候会对远程函数进行测试,实际上就是把 demo 列中的作为输入传入远程函数。如果不设定 `APIJSONApplication.init(false)` (即不在测试失败的时候关闭服务器),`APIJSONFunctionParser.test` 方法会测试以下四个函数:countArray, isContain, getFromArray, getFromObject,因此在本项目的 Function 表中有这四项,并标注为「框架启动自检需要」。此外,`APIJSONFunctionParser` 中还内建了一部分函数,有 393 | 394 | * getFromArray, getFromObject 395 | * isArrayEmpty, isObjectEmpty, isContain, isContainKey, isContainValue 396 | * countArray, countObject 397 | * removeIndex, removeKey 398 | 399 | 如果需要使用,只需要在 Function 表中注册即可。 400 | 401 | 本项目的 DemoFunctionParser 中还有两个远程函数: 402 | 403 | * getNoteCountAPI:展示如何在远程函数中分页请求数据库,以及如何从 JSONResponse 转换回 List 404 | * rawSQLAPI:展示如何在远程函数中直接用 SQL 操作数据库(可能有的时候的确需要这样的自由度) 405 | 406 | ### 新加一个表 407 | 408 | 现在你对这个 APIJSON 框架和这个示例项目应该有了一些大概的了解了。现在你可能想知道的是:我想加一个新的表,应该怎样做? 409 | 410 | APIJSON 中的业务表一般有以下几个基础列:(名字, 类型) 411 | 412 | * 主键:id, bigint 413 | * 用户外键:userId, bigint (只是逻辑上的外键,并不需要真的建立 FOREIGN KEY) 414 | * 创建日期:date, timestamp, default=current_timestamp 415 | 416 | 其他的列可以依业务需求设立。 417 | 418 | 如果只是用 APIJSON 查询,不需要在 Java 业务代码中处理的话: 419 | 420 | 1. 创建业务表 421 | 2. 更新 Access 表 422 | 3. (如果有非公开接口需求)更新 Request 表 423 | 4. 重启 App 或 reload (reload 可在不重启后端的情况下更新系统表(Access, Function, Request)信息,参见 APIJSONBoot.DemoContoller.reload) 424 | 425 | 如果还需要在业务代码中处理的话: 426 | 427 | 5. 创建一个 Model 类,extend BaseModel 428 | 6. 如果类内有嵌套类型(如 `List`),使用 APIJSON 请求时,须在对应层级加上 `@json` ,否则会解析失败 429 | 430 | ### 从零开始 431 | 432 | 如果你想完全从零开始搭建,你需要这样做: 433 | 434 | 0. 所有表名都是第一个字母大写,后面小写。 435 | 436 | 1. 系统表(Access, Function, Request)可以先从本项目中导入,然后用 TRUNCATE 清空数据(这样可以保留表定义。本项目中表定义来源于官方示例项目 APIJSONDemo) 437 | 2. 用户表一般分为两个,一个存储用户的公开信息(User),一个存储用户的私密信息(Credential / Privacy),密码一般存储在后者。建立这两个表,主键名 id,类型 bigint。 438 | 3. 在 Java 中新建两个 Class,分别对应公开信息和私密信息。类名和表名应该相同。公开信息类需要 `implement Visitor`。 439 | 440 | 4. 建立 Controller,extend APIJSONController,创建 7 个访问对象端点。 441 | 5. 建立登录、登出、注册逻辑。 442 | 6. 从上一节「新加一个表」开始。 443 | 444 | 445 | 446 | ## 杂项 447 | 448 | ### 时区问题 449 | 450 | 如果遇到存入数据库的时间,取出来的时候少了 8 小时,请确保在 JDBC 连接字符串中设定了 `serverTimezone=GMT%2B8`,以及在数据库设定了 global time zone。如果用 Docker 镜像部署数据库,默认时区是 UTC,需要执行如下 SQL 进行设定 451 | 452 | ```sql 453 | SET global time_zone = "+8:00"; FLUSH PRIVILEGES; SELECT NOW(); 454 | ``` 455 | 456 | ### 启动时的 debug 信息 457 | 458 | APIJSONApplication 启动的时候,会进行一系列测试,位于 APIJSONApplication.init,包括: 459 | 460 | 权限校验 APIJSONVerifier.initAccess、 461 | 远程函数配置 APIJSONFunctionParser.init、 462 | 远程函数 APIJSONFunctionParser.test()、 463 | 请求结构校验配置 APIJSONVerifier.initRequest、 464 | req/resp的数据结构校验 APIJSONVerifier.testStructure(); 465 | 466 | 如果不希望显示,可以在 DemoController.main 中设定 Log.DEBUG = false, APIJSONParser.IS_PRINT_BIG_LOG = false 467 | 468 | ### FastJSON 构造请求的 Key 顺序问题 469 | 470 | 有的时候可能发生 FastJSON 的 JSONObject,按照顺序放 Key 进去,取出来的顺序就乱掉了,导致引用赋值失败。这个时候可以用 `JSONObject.toJSONString(req, SerializerFeature.MapSortField)`。 参见 apijson.demo.config.DemoFunctionParser#getNoteCountAPI。 471 | 472 | ```java 473 | JSONResponse todoResponse = new JSONResponse(new APIJSONParser(GET,false).parseResponse(JSONObject.toJSONString(todoRequest, SerializerFeature.MapSortField))); 474 | ``` 475 | 476 | ### 返回不是 application/json 477 | 478 | 参见 apijson.demo.config.JSONWebConfig。设定一个 `defaultContentType` 就好了。 479 | 480 | ```java 481 | @Configuration 482 | @EnableWebMvc 483 | public class JSONWebConfig extends WebMvcConfigurerAdapter { 484 | 485 | @Override 486 | public void configureContentNegotiation(ContentNegotiationConfigurer configurer) { 487 | // 设定各个接口默认返回 application/json,以便 Postman 展示 488 | configurer.defaultContentType(MediaType.APPLICATION_JSON); 489 | } 490 | } 491 | ``` 492 | 493 | ### Maven 报错无法找到 APIJSON 包 494 | 495 | 这一问题的核心原因,似乎是 APIJSON 已经从自己在 Maven 上发行包,转换到了使用 jitpack.io 的服务自动从 Github 拉取最新版本打包。如果遇到此问题,请升级 IDEA 和 Maven 至最新版本,并勾选 IDEA 中 Maven 配置页的「Use plugin registry」和「Always update snapshots」后,重启 IDEA 再试。 496 | 497 | 作者在 IDEA 2021.1, Maven 3.6.1 下遇到了此问题,更新至 IDEA 2022.1, Maven 3.8.6 后问题解决。 498 | 499 | 相关 issue:[maven 阿里云镜像站无法找到 com.github.Tencent APIJSON · Issue #11 · APIJSON/APIJSON-Demo](https://github.com/APIJSON/APIJSON-Demo/issues/11) -------------------------------------------------------------------------------- /README.assets/apijson_function.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/APIJSON/apijson_todo_demo/2a578a394cc148d0e81892a9fc9725401747b13c/README.assets/apijson_function.png -------------------------------------------------------------------------------- /README.assets/image-20210801223053319.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/APIJSON/apijson_todo_demo/2a578a394cc148d0e81892a9fc9725401747b13c/README.assets/image-20210801223053319.png -------------------------------------------------------------------------------- /README.assets/image-20210801223117628.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/APIJSON/apijson_todo_demo/2a578a394cc148d0e81892a9fc9725401747b13c/README.assets/image-20210801223117628.png -------------------------------------------------------------------------------- /README.assets/image-20210801223138414.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/APIJSON/apijson_todo_demo/2a578a394cc148d0e81892a9fc9725401747b13c/README.assets/image-20210801223138414.png -------------------------------------------------------------------------------- /README.assets/image-20210801223203817.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/APIJSON/apijson_todo_demo/2a578a394cc148d0e81892a9fc9725401747b13c/README.assets/image-20210801223203817.png -------------------------------------------------------------------------------- /README.assets/image-20210801223225240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/APIJSON/apijson_todo_demo/2a578a394cc148d0e81892a9fc9725401747b13c/README.assets/image-20210801223225240.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # APIJSON Todo Demo 2 | 3 | 一个试图让 APIJSON 上手更简单一些的尝试。 4 | 5 | 本示例项目是一个基于 APIJSON 实现的 todo 系统,在官方示例项目(APIJSON-Demo)的基础上进一步简化了数据库和代码,完整实现了对一个业务表的单独/批量 CRUD 操作,并描述了如何用远程函数实现一个简单的自定义鉴权逻辑。 6 | 7 | :warning: 本项目文本最后更新于 2021 年 8 月(APIJSON 版本 4.7.2),后续维护只是跟随官方更新版本号,文中的文字描述和技术原理可能已经发生变更。如果在阅读时注意到不一致之处,欢迎提出 issue 和 pull request,我会在力所能及的范围内尽量修复。 8 | 9 | [阅读全文](https://github.com/jerrylususu/apijson_todo_demo/blob/master/FULLTEXT.md) 10 | 11 | ## 展示 12 | 13 | 简化、易于理解、开箱即用的数据库 14 | 15 | ![image-20210801223117628](README.assets/image-20210801223117628.png) 16 | 17 | ![image-20210801223138414](README.assets/image-20210801223138414.png) 18 | 19 | 完整的 CRUD 实现 20 | 21 | ![image-20210801223203817](README.assets/image-20210801223203817.png) 22 | 23 | 自带接口调试文件(基于 Postman) 24 | 25 | ![image-20210801223225240](README.assets/image-20210801223225240.png) 26 | 27 | 详尽的代码注释 28 | 29 | ![image-20210801223053319](README.assets/image-20210801223053319.png) -------------------------------------------------------------------------------- /apijson-demo.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /demo.postman_collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "_postman_id": "f3b82e3a-2635-486b-b7e7-10c7be9c1b28", 4 | "name": "demo", 5 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" 6 | }, 7 | "item": [ 8 | { 9 | "name": "login jerry", 10 | "request": { 11 | "method": "POST", 12 | "header": [], 13 | "body": { 14 | "mode": "raw", 15 | "raw": "{\r\n \"username\": \"jerry\",\r\n \"password\": \"123456\"\r\n}", 16 | "options": { 17 | "raw": { 18 | "language": "json" 19 | } 20 | } 21 | }, 22 | "url": { 23 | "raw": "http://localhost:8080/login", 24 | "protocol": "http", 25 | "host": [ 26 | "localhost" 27 | ], 28 | "port": "8080", 29 | "path": [ 30 | "login" 31 | ] 32 | } 33 | }, 34 | "response": [] 35 | }, 36 | { 37 | "name": "login neko", 38 | "request": { 39 | "method": "POST", 40 | "header": [], 41 | "body": { 42 | "mode": "raw", 43 | "raw": "{\r\n \"username\": \"neko\",\r\n \"password\": \"233233\"\r\n}", 44 | "options": { 45 | "raw": { 46 | "language": "json" 47 | } 48 | } 49 | }, 50 | "url": { 51 | "raw": "http://localhost:8080/login", 52 | "protocol": "http", 53 | "host": [ 54 | "localhost" 55 | ], 56 | "port": "8080", 57 | "path": [ 58 | "login" 59 | ] 60 | } 61 | }, 62 | "response": [] 63 | }, 64 | { 65 | "name": "login random guy", 66 | "request": { 67 | "method": "POST", 68 | "header": [], 69 | "body": { 70 | "mode": "raw", 71 | "raw": "{\r\n \"username\": \"randomguy\",\r\n \"password\": \"654321\"\r\n}", 72 | "options": { 73 | "raw": { 74 | "language": "json" 75 | } 76 | } 77 | }, 78 | "url": { 79 | "raw": "http://localhost:8080/login", 80 | "protocol": "http", 81 | "host": [ 82 | "localhost" 83 | ], 84 | "port": "8080", 85 | "path": [ 86 | "login" 87 | ] 88 | } 89 | }, 90 | "response": [] 91 | }, 92 | { 93 | "name": "login doge", 94 | "request": { 95 | "method": "POST", 96 | "header": [], 97 | "body": { 98 | "mode": "raw", 99 | "raw": "{\r\n \"username\": \"doge\",\r\n \"password\": \"666666\"\r\n}", 100 | "options": { 101 | "raw": { 102 | "language": "json" 103 | } 104 | } 105 | }, 106 | "url": { 107 | "raw": "http://localhost:8080/login", 108 | "protocol": "http", 109 | "host": [ 110 | "localhost" 111 | ], 112 | "port": "8080", 113 | "path": [ 114 | "login" 115 | ] 116 | } 117 | }, 118 | "response": [] 119 | }, 120 | { 121 | "name": "logout", 122 | "request": { 123 | "method": "POST", 124 | "header": [], 125 | "body": { 126 | "mode": "raw", 127 | "raw": "{}", 128 | "options": { 129 | "raw": { 130 | "language": "json" 131 | } 132 | } 133 | }, 134 | "url": { 135 | "raw": "http://localhost:8080/logout", 136 | "protocol": "http", 137 | "host": [ 138 | "localhost" 139 | ], 140 | "port": "8080", 141 | "path": [ 142 | "logout" 143 | ] 144 | } 145 | }, 146 | "response": [] 147 | }, 148 | { 149 | "name": "register wrapped", 150 | "request": { 151 | "method": "POST", 152 | "header": [], 153 | "body": { 154 | "mode": "raw", 155 | "raw": "{\r\n \"username\": \"neko\",\r\n \"realname\": \"nekonull\",\r\n \"password\": \"1234\"\r\n}", 156 | "options": { 157 | "raw": { 158 | "language": "json" 159 | } 160 | } 161 | }, 162 | "url": { 163 | "raw": "http://localhost:8080/register", 164 | "protocol": "http", 165 | "host": [ 166 | "localhost" 167 | ], 168 | "port": "8080", 169 | "path": [ 170 | "register" 171 | ] 172 | } 173 | }, 174 | "response": [] 175 | }, 176 | { 177 | "name": "register raw (use after login, for debug)", 178 | "request": { 179 | "method": "POST", 180 | "header": [], 181 | "body": { 182 | "mode": "raw", 183 | "raw": "{\r\n \"User\": {\r\n \"username\": \"neko\",\r\n \"realname\": \"nekonull\"\r\n },\r\n \"Credential\": {\r\n \"pwdHash\": \"2333\"\r\n },\r\n \"tag\": \"api_register\"\r\n}", 184 | "options": { 185 | "raw": { 186 | "language": "json" 187 | } 188 | } 189 | }, 190 | "url": { 191 | "raw": "http://localhost:8080/post", 192 | "protocol": "http", 193 | "host": [ 194 | "localhost" 195 | ], 196 | "port": "8080", 197 | "path": [ 198 | "post" 199 | ] 200 | } 201 | }, 202 | "response": [] 203 | }, 204 | { 205 | "name": "put user", 206 | "request": { 207 | "method": "POST", 208 | "header": [], 209 | "body": { 210 | "mode": "raw", 211 | "raw": "{\r\n \"User\": {\r\n \"bio\": \"edit my bio while adding a friend\",\r\n \"friends+\": [1627761038716], // neko's id\r\n \"id\": 1627761019072 // jerry's id (当前已登陆的用户是 jerry)\r\n },\r\n \"tag\": \"User\"\r\n}", 212 | "options": { 213 | "raw": { 214 | "language": "json" 215 | } 216 | } 217 | }, 218 | "url": { 219 | "raw": "http://localhost:8080/put", 220 | "protocol": "http", 221 | "host": [ 222 | "localhost" 223 | ], 224 | "port": "8080", 225 | "path": [ 226 | "put" 227 | ] 228 | } 229 | }, 230 | "response": [] 231 | }, 232 | { 233 | "name": "get user", 234 | "request": { 235 | "method": "POST", 236 | "header": [], 237 | "body": { 238 | "mode": "raw", 239 | "raw": "{\r\n \"User\": {\r\n \"username\": \"jerry\"\r\n }\r\n}", 240 | "options": { 241 | "raw": { 242 | "language": "json" 243 | } 244 | } 245 | }, 246 | "url": { 247 | "raw": "http://localhost:8080/get", 248 | "protocol": "http", 249 | "host": [ 250 | "localhost" 251 | ], 252 | "port": "8080", 253 | "path": [ 254 | "get" 255 | ] 256 | } 257 | }, 258 | "response": [] 259 | }, 260 | { 261 | "name": "get current login user", 262 | "request": { 263 | "method": "POST", 264 | "header": [], 265 | "body": { 266 | "mode": "raw", 267 | "raw": "{\r\n \"User\": {},\r\n \"@role\": \"OWNER\"\r\n}", 268 | "options": { 269 | "raw": { 270 | "language": "json" 271 | } 272 | } 273 | }, 274 | "url": { 275 | "raw": "http://localhost:8080/get", 276 | "protocol": "http", 277 | "host": [ 278 | "localhost" 279 | ], 280 | "port": "8080", 281 | "path": [ 282 | "get" 283 | ] 284 | } 285 | }, 286 | "response": [] 287 | }, 288 | { 289 | "name": "post todo multi", 290 | "request": { 291 | "method": "POST", 292 | "header": [], 293 | "body": { 294 | "mode": "raw", 295 | "raw": "{\r\n \"Todo[]\": [\r\n {\r\n \"title\":\"hithere\",\r\n \"note\": \"\"\r\n },\r\n {\r\n \"title\": \"multi post a2\",\r\n \"note\": \"\"\r\n }\r\n ],\r\n \"tag\": \"Todo:[]\"\r\n}", 296 | "options": { 297 | "raw": { 298 | "language": "json" 299 | } 300 | } 301 | }, 302 | "url": { 303 | "raw": "http://localhost:8080/post", 304 | "protocol": "http", 305 | "host": [ 306 | "localhost" 307 | ], 308 | "port": "8080", 309 | "path": [ 310 | "post" 311 | ] 312 | } 313 | }, 314 | "response": [] 315 | }, 316 | { 317 | "name": "post todo", 318 | "request": { 319 | "method": "POST", 320 | "header": [], 321 | "body": { 322 | "mode": "raw", 323 | "raw": "{\r\n \"Todo\": {\r\n \"title\": \"yet another todo\",\r\n \"note\": \"to be edit by helper\"\r\n },\r\n \"tag\": \"Todo\"\r\n}", 324 | "options": { 325 | "raw": { 326 | "language": "json" 327 | } 328 | } 329 | }, 330 | "url": { 331 | "raw": "http://localhost:8080/post", 332 | "protocol": "http", 333 | "host": [ 334 | "localhost" 335 | ], 336 | "port": "8080", 337 | "path": [ 338 | "post" 339 | ] 340 | } 341 | }, 342 | "response": [] 343 | }, 344 | { 345 | "name": "get todo by username", 346 | "request": { 347 | "method": "POST", 348 | "header": [], 349 | "body": { 350 | "mode": "raw", 351 | "raw": "{\r\n \"User\": { \"username\": \"jerry\" },\r\n \"[]\": {\r\n \"Todo\": { \"userId@\": \"User/id\" }\r\n }\r\n}", 352 | "options": { 353 | "raw": { 354 | "language": "json" 355 | } 356 | } 357 | }, 358 | "url": { 359 | "raw": "http://localhost:8080/get", 360 | "protocol": "http", 361 | "host": [ 362 | "localhost" 363 | ], 364 | "port": "8080", 365 | "path": [ 366 | "get" 367 | ] 368 | } 369 | }, 370 | "response": [] 371 | }, 372 | { 373 | "name": "get todo by user id", 374 | "request": { 375 | "method": "POST", 376 | "header": [], 377 | "body": { 378 | "mode": "raw", 379 | "raw": "{\r\n \"[]\": {\r\n \"Todo\": { \"userId\": 1627508518581 }\r\n },\r\n \"@role\": \"CIRCLE\"\r\n}", 380 | "options": { 381 | "raw": { 382 | "language": "json" 383 | } 384 | } 385 | }, 386 | "url": { 387 | "raw": "http://localhost:8080/get", 388 | "protocol": "http", 389 | "host": [ 390 | "localhost" 391 | ], 392 | "port": "8080", 393 | "path": [ 394 | "get" 395 | ] 396 | } 397 | }, 398 | "response": [] 399 | }, 400 | { 401 | "name": "get todo of login user", 402 | "request": { 403 | "method": "POST", 404 | "header": [], 405 | "body": { 406 | "mode": "raw", 407 | "raw": "{\r\n \"User\": { },\r\n \"[]\": {\r\n \"Todo\": { \"userId@\": \"User/id\" }\r\n },\r\n \"@role\": \"OWNER\"\r\n}", 408 | "options": { 409 | "raw": { 410 | "language": "json" 411 | } 412 | } 413 | }, 414 | "url": { 415 | "raw": "http://localhost:8080/get", 416 | "protocol": "http", 417 | "host": [ 418 | "localhost" 419 | ], 420 | "port": "8080", 421 | "path": [ 422 | "get" 423 | ] 424 | } 425 | }, 426 | "response": [] 427 | }, 428 | { 429 | "name": "put todo", 430 | "request": { 431 | "method": "POST", 432 | "header": [], 433 | "body": { 434 | "mode": "raw", 435 | "raw": "{\r\n \"Todo\": {\r\n \"id\": 1627761702477,\r\n \"title\": \"yet another todo\",\r\n \"note\": \"good helper\"\r\n },\r\n \"tag\": \"Todo\"\r\n}", 436 | "options": { 437 | "raw": { 438 | "language": "json" 439 | } 440 | } 441 | }, 442 | "url": { 443 | "raw": "http://localhost:8080/put/", 444 | "protocol": "http", 445 | "host": [ 446 | "localhost" 447 | ], 448 | "port": "8080", 449 | "path": [ 450 | "put", 451 | "" 452 | ] 453 | } 454 | }, 455 | "response": [] 456 | }, 457 | { 458 | "name": "put todo multi (Todo:[])", 459 | "request": { 460 | "method": "POST", 461 | "header": [], 462 | "body": { 463 | "mode": "raw", 464 | "raw": "{\r\n \"Todo[]\": [\r\n {\r\n \"id\": 1627794007156,\r\n \"title\": \"edit put multi\",\r\n \"note\": \"good 1\"\r\n },\r\n {\r\n \"id\": 1627794007173,\r\n \"title\": \"edit put multi 2\",\r\n \"note\": \"good 2\"\r\n }\r\n ],\r\n \"tag\": \"Todo:[]\"\r\n}", 465 | "options": { 466 | "raw": { 467 | "language": "json" 468 | } 469 | } 470 | }, 471 | "url": { 472 | "raw": "http://localhost:8080/put/", 473 | "protocol": "http", 474 | "host": [ 475 | "localhost" 476 | ], 477 | "port": "8080", 478 | "path": [ 479 | "put", 480 | "" 481 | ] 482 | } 483 | }, 484 | "response": [] 485 | }, 486 | { 487 | "name": "put todo multi (Todo[])", 488 | "request": { 489 | "method": "POST", 490 | "header": [], 491 | "body": { 492 | "mode": "raw", 493 | "raw": "{\r\n \"Todo\": {\r\n\r\n \"id{}\": [1627794007156,1627794007173],\r\n \"title\": \"edit put multi\",\r\n \"note\": \"good 1\"\r\n\r\n },\r\n \"tag\": \"Todo[]\"\r\n}", 494 | "options": { 495 | "raw": { 496 | "language": "json" 497 | } 498 | } 499 | }, 500 | "url": { 501 | "raw": "http://localhost:8080/put/", 502 | "protocol": "http", 503 | "host": [ 504 | "localhost" 505 | ], 506 | "port": "8080", 507 | "path": [ 508 | "put", 509 | "" 510 | ] 511 | } 512 | }, 513 | "response": [] 514 | }, 515 | { 516 | "name": "delete todo", 517 | "request": { 518 | "method": "POST", 519 | "header": [], 520 | "body": { 521 | "mode": "raw", 522 | "raw": "{\r\n \"Todo\": {\r\n \"id\": 1627566951648\r\n },\r\n \"tag\": \"Todo\"\r\n}", 523 | "options": { 524 | "raw": { 525 | "language": "json" 526 | } 527 | } 528 | }, 529 | "url": { 530 | "raw": "http://localhost:8080/delete", 531 | "protocol": "http", 532 | "host": [ 533 | "localhost" 534 | ], 535 | "port": "8080", 536 | "path": [ 537 | "delete" 538 | ] 539 | } 540 | }, 541 | "response": [] 542 | }, 543 | { 544 | "name": "delete todo multi", 545 | "request": { 546 | "method": "POST", 547 | "header": [], 548 | "body": { 549 | "mode": "raw", 550 | "raw": "{\r\n \"Todo\": {\r\n \"id{}\": [1627794109043, 1627794109050]\r\n },\r\n \"tag\": \"Todo[]\"\r\n}", 551 | "options": { 552 | "raw": { 553 | "language": "json" 554 | } 555 | } 556 | }, 557 | "url": { 558 | "raw": "http://localhost:8080/delete", 559 | "protocol": "http", 560 | "host": [ 561 | "localhost" 562 | ], 563 | "port": "8080", 564 | "path": [ 565 | "delete" 566 | ] 567 | } 568 | }, 569 | "response": [] 570 | }, 571 | { 572 | "name": "add friend", 573 | "request": { 574 | "method": "POST", 575 | "header": [], 576 | "body": { 577 | "mode": "raw", 578 | "raw": "{\r\n \"User\":{\r\n \"id\": 1,\r\n \"friends+\": [1627508518581]\r\n },\r\n \"tag\": \"User\"\r\n}", 579 | "options": { 580 | "raw": { 581 | "language": "json" 582 | } 583 | } 584 | }, 585 | "url": { 586 | "raw": "localhost:8080/put", 587 | "host": [ 588 | "localhost" 589 | ], 590 | "port": "8080", 591 | "path": [ 592 | "put" 593 | ] 594 | } 595 | }, 596 | "response": [] 597 | }, 598 | { 599 | "name": "add todo helper", 600 | "request": { 601 | "method": "POST", 602 | "header": [], 603 | "body": { 604 | "mode": "raw", 605 | "raw": "{\r\n \"Todo\": {\r\n \"id\": 1627565018422,\r\n \"helper+\": [1627508518581]\r\n },\r\n \"tag\": \"helper+\"\r\n}", 606 | "options": { 607 | "raw": { 608 | "language": "json" 609 | } 610 | } 611 | }, 612 | "url": { 613 | "raw": "http://localhost:8080/put/", 614 | "protocol": "http", 615 | "host": [ 616 | "localhost" 617 | ], 618 | "port": "8080", 619 | "path": [ 620 | "put", 621 | "" 622 | ] 623 | } 624 | }, 625 | "response": [] 626 | }, 627 | { 628 | "name": "get todo having helper", 629 | "request": { 630 | "method": "POST", 631 | "header": [], 632 | "body": { 633 | "mode": "raw", 634 | "raw": "{\r\n \r\n \"[]\": {\r\n \"Todo\": {\r\n \"helper<>\": 1627508518581\r\n // \"@uid\": 1627508518581,\r\n // \"check()\": \"isContain(helper,@uid)\"\r\n }\r\n }\r\n}", 635 | "options": { 636 | "raw": { 637 | "language": "json" 638 | } 639 | } 640 | }, 641 | "url": { 642 | "raw": "http://localhost:8080/get", 643 | "protocol": "http", 644 | "host": [ 645 | "localhost" 646 | ], 647 | "port": "8080", 648 | "path": [ 649 | "get" 650 | ] 651 | } 652 | }, 653 | "response": [] 654 | }, 655 | { 656 | "name": "get todo by helper", 657 | "request": { 658 | "method": "POST", 659 | "header": [], 660 | "body": { 661 | "mode": "raw", 662 | "raw": "{\r\n \r\n \"[]\": {\r\n \"Todo\": {\r\n \"helper<>\": 1627508518581\r\n // \"@uid\": 1627508518581,\r\n // \"check()\": \"isContain(helper,@uid)\"\r\n }\r\n }\r\n}", 663 | "options": { 664 | "raw": { 665 | "language": "json" 666 | } 667 | } 668 | }, 669 | "url": { 670 | "raw": "http://localhost:8080/get", 671 | "protocol": "http", 672 | "host": [ 673 | "localhost" 674 | ], 675 | "port": "8080", 676 | "path": [ 677 | "get" 678 | ] 679 | } 680 | }, 681 | "response": [] 682 | }, 683 | { 684 | "name": "function sayhello", 685 | "request": { 686 | "method": "POST", 687 | "header": [], 688 | "body": { 689 | "mode": "raw", 690 | "raw": "{\r\n \"name\": \"jerry\",\r\n \"ref()\": \"sayHello(name)\"\r\n}", 691 | "options": { 692 | "raw": { 693 | "language": "json" 694 | } 695 | } 696 | }, 697 | "url": { 698 | "raw": "http://localhost:8080/get", 699 | "protocol": "http", 700 | "host": [ 701 | "localhost" 702 | ], 703 | "port": "8080", 704 | "path": [ 705 | "get" 706 | ] 707 | } 708 | }, 709 | "response": [] 710 | }, 711 | { 712 | "name": "function getNoteCountAPI", 713 | "request": { 714 | "method": "POST", 715 | "header": [], 716 | "body": { 717 | "mode": "raw", 718 | "raw": "{\r\n \"noteCount()\": \"getNoteCountAPI()\"\r\n}", 719 | "options": { 720 | "raw": { 721 | "language": "json" 722 | } 723 | } 724 | }, 725 | "url": { 726 | "raw": "http://localhost:8080/get", 727 | "protocol": "http", 728 | "host": [ 729 | "localhost" 730 | ], 731 | "port": "8080", 732 | "path": [ 733 | "get" 734 | ] 735 | } 736 | }, 737 | "response": [] 738 | }, 739 | { 740 | "name": "function isUserCanPutTodo", 741 | "request": { 742 | "method": "POST", 743 | "header": [], 744 | "body": { 745 | "mode": "raw", 746 | "raw": "{\r\n \"todoid\": 1627761702477,\r\n \"ref()\": \"isUserCanPutTodo(todoid)\"\r\n // 现在是不可用的,如果要用的话需要在 Request 表对应行把 tag 和 method 设定为 NULL\r\n}", 747 | "options": { 748 | "raw": { 749 | "language": "json" 750 | } 751 | } 752 | }, 753 | "url": { 754 | "raw": "http://localhost:8080/get", 755 | "protocol": "http", 756 | "host": [ 757 | "localhost" 758 | ], 759 | "port": "8080", 760 | "path": [ 761 | "get" 762 | ] 763 | } 764 | }, 765 | "response": [] 766 | }, 767 | { 768 | "name": "function rawSQLAPI", 769 | "request": { 770 | "method": "POST", 771 | "header": [], 772 | "body": { 773 | "mode": "raw", 774 | "raw": "{\r\n \"todoid\": \"1627761702477\",\r\n \"ref()\": \"rawSQLAPI(todoid)\"\r\n // 现在是不可用的,如果要用的话需要在 Request 表对应行把 tag 和 method 设定为 NULL\r\n}", 775 | "options": { 776 | "raw": { 777 | "language": "json" 778 | } 779 | } 780 | }, 781 | "url": { 782 | "raw": "http://localhost:8080/get", 783 | "protocol": "http", 784 | "host": [ 785 | "localhost" 786 | ], 787 | "port": "8080", 788 | "path": [ 789 | "get" 790 | ] 791 | } 792 | }, 793 | "response": [] 794 | }, 795 | { 796 | "name": "get todo by user id (unwrapped)", 797 | "request": { 798 | "method": "POST", 799 | "header": [], 800 | "body": { 801 | "mode": "raw", 802 | "raw": "{\r\n \"Todo[]\": {\r\n \"Todo\": { \"userId\": 1627508518581 }\r\n },\r\n \"@role\": \"CIRCLE\"\r\n}", 803 | "options": { 804 | "raw": { 805 | "language": "json" 806 | } 807 | } 808 | }, 809 | "url": { 810 | "raw": "http://localhost:8080/get", 811 | "protocol": "http", 812 | "host": [ 813 | "localhost" 814 | ], 815 | "port": "8080", 816 | "path": [ 817 | "get" 818 | ] 819 | } 820 | }, 821 | "response": [] 822 | }, 823 | { 824 | "name": "get todo by user id (unwrapped) + show page info", 825 | "request": { 826 | "method": "POST", 827 | "header": [], 828 | "body": { 829 | "mode": "raw", 830 | "raw": "{\r\n \"Todo[]\": {\r\n \"Todo\": {\r\n \"userId\": 1627761019072,\r\n \"@json\": \"helper\"\r\n },\r\n \"count\": 5,\r\n \"page\": 0,\r\n \"query\": 2\r\n },\r\n \"total@\": \"/Todo[]/total\",\r\n \"info@\": \"/Todo[]/info\",\r\n \"format\": true\r\n}", 831 | "options": { 832 | "raw": { 833 | "language": "json" 834 | } 835 | } 836 | }, 837 | "url": { 838 | "raw": "http://localhost:8080/get", 839 | "protocol": "http", 840 | "host": [ 841 | "localhost" 842 | ], 843 | "port": "8080", 844 | "path": [ 845 | "get" 846 | ] 847 | } 848 | }, 849 | "response": [] 850 | } 851 | ] 852 | } -------------------------------------------------------------------------------- /initdb_final.sql: -------------------------------------------------------------------------------- 1 | -- MySQL dump 10.17 Distrib 10.3.16-MariaDB, for Win64 (AMD64) 2 | -- 3 | -- Host: 192.168.99.100 Database: apijson_todo_demo 4 | -- ------------------------------------------------------ 5 | -- Server version 10.5.9-MariaDB-1:10.5.9+maria~focal 6 | 7 | /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; 8 | /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; 9 | /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; 10 | /*!40101 SET NAMES utf8mb4 */; 11 | /*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; 12 | /*!40103 SET TIME_ZONE='+00:00' */; 13 | /*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; 14 | /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; 15 | /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; 16 | /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; 17 | 18 | -- 19 | -- Table structure for table `Access` 20 | -- 21 | 22 | DROP TABLE IF EXISTS `Access`; 23 | /*!40101 SET @saved_cs_client = @@character_set_client */; 24 | /*!40101 SET character_set_client = utf8 */; 25 | CREATE TABLE `Access` ( 26 | `id` bigint(15) NOT NULL AUTO_INCREMENT, 27 | `debug` tinyint(2) NOT NULL DEFAULT 0 COMMENT '是否为调试表,只允许在开发环境使用,测试和线上环境禁用', 28 | `name` varchar(50) NOT NULL COMMENT '实际表名,例如 apijson_user', 29 | `alias` varchar(20) DEFAULT NULL COMMENT '外部调用的表别名,例如 User', 30 | `get` varchar(100) NOT NULL DEFAULT '["UNKNOWN", "LOGIN", "CONTACT", "CIRCLE", "OWNER", "ADMIN"]' COMMENT '允许 get 的角色列表,例如 ["LOGIN", "CONTACT", "CIRCLE", "OWNER"]\n用 JSON 类型不能设置默认值,反正权限对应的需求是明确的,也不需要自动转 JSONArray。\nTODO: 直接 LOGIN,CONTACT,CIRCLE,OWNER 更简单,反正是开发内部用,不需要复杂查询。', 31 | `head` varchar(100) NOT NULL DEFAULT '["UNKNOWN", "LOGIN", "CONTACT", "CIRCLE", "OWNER", "ADMIN"]' COMMENT '允许 head 的角色列表,例如 ["LOGIN", "CONTACT", "CIRCLE", "OWNER"]', 32 | `gets` varchar(100) NOT NULL DEFAULT '["LOGIN", "CONTACT", "CIRCLE", "OWNER", "ADMIN"]' COMMENT '允许 gets 的角色列表,例如 ["LOGIN", "CONTACT", "CIRCLE", "OWNER"]', 33 | `heads` varchar(100) NOT NULL DEFAULT '["LOGIN", "CONTACT", "CIRCLE", "OWNER", "ADMIN"]' COMMENT '允许 heads 的角色列表,例如 ["LOGIN", "CONTACT", "CIRCLE", "OWNER"]', 34 | `post` varchar(100) NOT NULL DEFAULT '["OWNER", "ADMIN"]' COMMENT '允许 post 的角色列表,例如 ["LOGIN", "CONTACT", "CIRCLE", "OWNER"]', 35 | `put` varchar(100) NOT NULL DEFAULT '["OWNER", "ADMIN"]' COMMENT '允许 put 的角色列表,例如 ["LOGIN", "CONTACT", "CIRCLE", "OWNER"]', 36 | `delete` varchar(100) NOT NULL DEFAULT '["OWNER", "ADMIN"]' COMMENT '允许 delete 的角色列表,例如 ["LOGIN", "CONTACT", "CIRCLE", "OWNER"]', 37 | `date` timestamp NOT NULL DEFAULT current_timestamp() COMMENT '创建时间', 38 | `detail` varchar(1000) DEFAULT NULL, 39 | PRIMARY KEY (`id`), 40 | UNIQUE KEY `name_UNIQUE` (`name`), 41 | UNIQUE KEY `alias_UNIQUE` (`alias`) 42 | ) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8 COMMENT='权限配置(必须)'; 43 | /*!40101 SET character_set_client = @saved_cs_client */; 44 | 45 | -- 46 | -- Dumping data for table `Access` 47 | -- 48 | 49 | LOCK TABLES `Access` WRITE; 50 | /*!40000 ALTER TABLE `Access` DISABLE KEYS */; 51 | INSERT INTO `Access` VALUES (2,0,'User',NULL,'[\"LOGIN\", \"CONTACT\", \"CIRCLE\", \"OWNER\", \"ADMIN\"]','[\"LOGIN\", \"CONTACT\", \"CIRCLE\", \"OWNER\", \"ADMIN\"]','[\"UNKNOWN\", \"LOGIN\", \"CONTACT\", \"CIRCLE\", \"OWNER\", \"ADMIN\"]','[\"UNKNOWN\", \"LOGIN\", \"CONTACT\", \"CIRCLE\", \"OWNER\", \"ADMIN\"]','[\"UNKNOWN\",\"LOGIN\",\"OWNER\", \"ADMIN\"]','[\"OWNER\", \"ADMIN\"]','[\"OWNER\", \"ADMIN\"]','2021-07-28 14:02:41','用户公开信息表'),(3,0,'Credential',NULL,'[]','[]','[\"UNKNOWN\",\"OWNER\", \"ADMIN\"]','[\"OWNER\", \"ADMIN\"]','[\"UNKNOWN\",\"LOGIN\",\"OWNER\", \"ADMIN\"]','[\"OWNER\", \"ADMIN\"]','[\"ADMIN\"]','2021-07-28 14:04:01','用户隐私表'),(4,0,'Todo',NULL,'[\"LOGIN\",\"CONTACT\", \"CIRCLE\", \"OWNER\", \"ADMIN\"]','[\"LOGIN\", \"CONTACT\", \"CIRCLE\", \"OWNER\", \"ADMIN\"]','[\"UNKNOWN\", \"LOGIN\", \"CONTACT\", \"CIRCLE\", \"OWNER\", \"ADMIN\"]','[\"UNKNOWN\", \"LOGIN\", \"CONTACT\", \"CIRCLE\", \"OWNER\", \"ADMIN\"]','[\"LOGIN\",\"OWNER\", \"ADMIN\"]','[\"LOGIN\",\"CIRCLE\",\"OWNER\",\"ADMIN\"]','[\"OWNER\", \"ADMIN\"]','2021-07-28 14:02:41','(业务表)待办事项表'),(5,0,'Function',NULL,'[\"UNKNOWN\", \"LOGIN\", \"CONTACT\", \"CIRCLE\", \"OWNER\", \"ADMIN\"]','[\"UNKNOWN\", \"LOGIN\", \"CONTACT\", \"CIRCLE\", \"OWNER\", \"ADMIN\"]','[\"LOGIN\", \"CONTACT\", \"CIRCLE\", \"OWNER\", \"ADMIN\"]','[\"LOGIN\", \"CONTACT\", \"CIRCLE\", \"OWNER\", \"ADMIN\"]','[]','[]','[]','2018-11-28 16:38:15','框架本身需要'); 52 | /*!40000 ALTER TABLE `Access` ENABLE KEYS */; 53 | UNLOCK TABLES; 54 | 55 | -- 56 | -- Table structure for table `Credential` 57 | -- 58 | 59 | DROP TABLE IF EXISTS `Credential`; 60 | /*!40101 SET @saved_cs_client = @@character_set_client */; 61 | /*!40101 SET character_set_client = utf8 */; 62 | CREATE TABLE `Credential` ( 63 | `id` bigint(20) DEFAULT NULL, 64 | `pwdHash` varchar(255) DEFAULT NULL 65 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 66 | /*!40101 SET character_set_client = @saved_cs_client */; 67 | 68 | -- 69 | -- Dumping data for table `Credential` 70 | -- 71 | 72 | LOCK TABLES `Credential` WRITE; 73 | /*!40000 ALTER TABLE `Credential` DISABLE KEYS */; 74 | INSERT INTO `Credential` VALUES (1627761019072,'123456'),(1627761038716,'233233'),(1627761152411,'654321'),(1627761504126,'666666'); 75 | /*!40000 ALTER TABLE `Credential` ENABLE KEYS */; 76 | UNLOCK TABLES; 77 | 78 | -- 79 | -- Table structure for table `Function` 80 | -- 81 | 82 | DROP TABLE IF EXISTS `Function`; 83 | /*!40101 SET @saved_cs_client = @@character_set_client */; 84 | /*!40101 SET character_set_client = utf8 */; 85 | CREATE TABLE `Function` ( 86 | `id` bigint(15) NOT NULL AUTO_INCREMENT, 87 | `debug` tinyint(2) NOT NULL DEFAULT 0 COMMENT '是否为 DEBUG 调试数据,只允许在开发环境使用,测试和线上环境禁用:0-否,1-是。', 88 | `userId` bigint(15) NOT NULL COMMENT '管理员用户Id', 89 | `name` varchar(50) NOT NULL COMMENT '方法名', 90 | `arguments` varchar(100) DEFAULT NULL COMMENT '参数列表,每个参数的类型都是 String。\n用 , 分割的字符串 比 [JSONArray] 更好,例如 array,item ,更直观,还方便拼接函数。', 91 | `demo` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '可用的示例。\nTODO 改成 call,和返回值示例 back 对应。' CHECK (json_valid(`demo`)), 92 | `detail` varchar(1000) NOT NULL COMMENT '详细描述', 93 | `type` varchar(50) NOT NULL DEFAULT 'Object' COMMENT '返回值类型。TODO RemoteFunction 校验 type 和 back', 94 | `version` tinyint(4) NOT NULL DEFAULT 0 COMMENT '允许的最低版本号,只限于GET,HEAD外的操作方法。\nTODO 使用 requestIdList 替代 version,tag,methods', 95 | `tag` varchar(20) DEFAULT NULL COMMENT '允许的标签.\nnull - 允许全部\nTODO 使用 requestIdList 替代 version,tag,methods', 96 | `methods` varchar(50) DEFAULT NULL COMMENT '允许的操作方法。\nnull - 允许全部\nTODO 使用 requestIdList 替代 version,tag,methods', 97 | `date` timestamp NOT NULL DEFAULT current_timestamp() COMMENT '创建时间', 98 | `back` varchar(45) DEFAULT NULL COMMENT '返回值示例', 99 | PRIMARY KEY (`id`) 100 | ) ENGINE=InnoDB AUTO_INCREMENT=15 DEFAULT CHARSET=utf8 COMMENT='远程函数。强制在启动时校验所有demo是否能正常运行通过'; 101 | /*!40101 SET character_set_client = @saved_cs_client */; 102 | 103 | -- 104 | -- Dumping data for table `Function` 105 | -- 106 | 107 | LOCK TABLES `Function` WRITE; 108 | /*!40000 ALTER TABLE `Function` DISABLE KEYS */; 109 | INSERT INTO `Function` VALUES (1,0,0,'sayHello','name','{\"name\": \"test\"}','最简单的远程函数','Object',0,NULL,NULL,'2021-07-28 12:04:27',NULL),(2,0,0,'isUserCanPutTodo','todoId','{\"todoId\": 123}','用来判定todo的写权限。','Object',0,NULL,NULL,'2021-07-28 12:04:27',NULL),(3,0,0,'getNoteCountAPI','','{}','计数当前登录用户的todo数,展示如何在远程函数内部操作db','Object',0,NULL,NULL,'2021-07-28 12:04:27',NULL),(4,0,0,'rawSQLAPI','id','{\"id\": \"_DOCUMENT_ONLY_\"}','展示如何用裸SQL操作','Object',0,NULL,NULL,'2021-07-28 12:04:27',NULL),(10,0,0,'countArray','array','{\"array\": [1, 2, 3]}','(框架启动自检需要)获取数组长度。没写调用键值对,会自动补全 \"result()\": \"countArray(array)\"','Object',0,NULL,NULL,'2018-10-13 08:23:23',NULL),(11,0,0,'isContain','array,value','{\"array\": [1, 2, 3], \"value\": 2}','(框架启动自检需要)判断是否数组包含值。','Object',0,NULL,NULL,'2018-10-13 08:23:23',NULL),(12,0,0,'getFromArray','array,position','{\"array\": [1, 2, 3], \"result()\": \"getFromArray(array,1)\"}','(框架启动自检需要)根据下标获取数组里的值。position 传数字时直接作为值,而不是从所在对象 request 中取值','Object',0,NULL,NULL,'2018-10-13 08:30:31',NULL),(13,0,0,'getFromObject','object,key','{\"key\": \"id\", \"object\": {\"id\": 1}}','(框架启动自检需要)根据键获取对象里的值。','Object',0,NULL,NULL,'2018-10-13 08:30:31',NULL); 110 | /*!40000 ALTER TABLE `Function` ENABLE KEYS */; 111 | UNLOCK TABLES; 112 | 113 | -- 114 | -- Table structure for table `Request` 115 | -- 116 | 117 | DROP TABLE IF EXISTS `Request`; 118 | /*!40101 SET @saved_cs_client = @@character_set_client */; 119 | /*!40101 SET character_set_client = utf8 */; 120 | CREATE TABLE `Request` ( 121 | `id` bigint(15) NOT NULL COMMENT '唯一标识', 122 | `debug` tinyint(2) NOT NULL DEFAULT 0 COMMENT '是否为 DEBUG 调试数据,只允许在开发环境使用,测试和线上环境禁用:0-否,1-是。', 123 | `version` tinyint(4) NOT NULL DEFAULT 1 COMMENT 'GET,HEAD可用任意结构访问任意开放内容,不需要这个字段。\n其它的操作因为写入了结构和内容,所以都需要,按照不同的version选择对应的structure。\n\n自动化版本管理:\nRequest JSON最外层可以传 “version”:Integer 。\n1.未传或 <= 0,用最新版。 “@order”:”version-“\n2.已传且 > 0,用version以上的可用版本的最低版本。 “@order”:”version+”, “version{}”:”>={version}”', 124 | `method` varchar(10) DEFAULT 'GETS' COMMENT '只限于GET,HEAD外的操作方法。', 125 | `tag` varchar(20) NOT NULL COMMENT '标签', 126 | `structure` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '结构。\nTODO 里面的 PUT 改为 UPDATE,避免和请求 PUT 搞混。' CHECK (json_valid(`structure`)), 127 | `detail` varchar(10000) DEFAULT NULL COMMENT '详细说明', 128 | `date` timestamp NULL DEFAULT current_timestamp() COMMENT '创建日期', 129 | PRIMARY KEY (`id`) 130 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='请求参数校验配置(必须)。\n最好编辑完后删除主键,这样就是只读状态,不能随意更改。需要更改就重新加上主键。\n\n每次启动服务器时加载整个表到内存。\n这个表不可省略,model内注解的权限只是客户端能用的,其它可以保证即便服务端代码错误时也不会误删数据。'; 131 | /*!40101 SET character_set_client = @saved_cs_client */; 132 | 133 | -- 134 | -- Dumping data for table `Request` 135 | -- 136 | 137 | LOCK TABLES `Request` WRITE; 138 | /*!40000 ALTER TABLE `Request` DISABLE KEYS */; 139 | INSERT INTO `Request` VALUES (2,0,1,'POST','api_register','{\"User\": {\"MUST\": \"username,realname\", \"REFUSE\": \"id\", \"UNIQUE\": \"username\"}, \"Credential\": {\"MUST\": \"pwdHash\", \"UPDATE\": {\"id@\": \"User/id\"}}}','注意tag名小写开头,则不会被默认映射到表','2021-07-28 18:15:40'),(3,0,1,'PUT','User','{\"REFUSE\": \"username\", \"UPDATE\": {\"@role\": \"OWNER\"}}','user 修改自身数据','2021-07-29 12:49:20'),(4,0,1,'POST','Todo','{\"MUST\": \"title\", \"UPDATE\": {\"@role\": \"OWNER\"}, \"REFUSE\": \"id\"}','增加todo','2021-07-29 13:18:50'),(5,0,1,'PUT','Todo','{\"Todo\":{ \"MUST\":\"id\",\"REFUSE\": \"userId\", \"UPDATE\": {\"checkCanPut-()\": \"isUserCanPutTodo(id)\"}} }','修改todo','2021-07-29 14:05:57'),(6,0,1,'DELETE','Todo','{\"MUST\": \"id\", \"REFUSE\": \"!\", \"INSERT\": {\"@role\": \"OWNER\"}}','删除todo','2021-07-29 14:10:32'),(8,0,1,'PUT','helper+','{\"Todo\": {\"MUST\": \"id,helper+\", \"INSERT\": {\"@role\": \"OWNER\"}}}','增加todo helper','2021-07-29 21:46:34'),(9,0,1,'PUT','helper-','{\"Todo\": {\"MUST\": \"id,helper-\", \"INSERT\": {\"@role\": \"OWNER\"}}}','删除todo helper','2021-07-29 21:46:34'),(10,0,1,'POST','Todo:[]','{\"Todo[]\": [{\"MUST\": \"title\", \"REFUSE\": \"id\"}], \"UPDATE\": {\"@role\": \"OWNER\"}}','批量增加todo','2021-08-01 04:51:31'),(11,0,1,'PUT','Todo:[]','{\"Todo[]\":[{ \"MUST\":\"id\",\"REFUSE\": \"userId\", \"UPDATE\": {\"checkCanPut-()\": \"isUserCanPutTodo(id)\"}}] }','每项单独设置(现在不生效)','2021-08-01 04:51:31'),(12,0,1,'PUT','Todo[]','{\"Todo\":{ \"MUST\":\"id{}\",\"REFUSE\": \"userId\", \"UPDATE\": {\"checkCanPut-()\": \"isUserCanPutTodo(id)\"}} }','指定全部改(现在不生效)','2021-08-01 04:51:31'),(13,0,1,'DELETE','Todo[]','{\"Todo\": {\"MUST\": \"id{}\", \"REFUSE\": \"!\", \"INSERT\": {\"@role\": \"OWNER\"}}}','删除todo','2021-08-01 10:35:15'); 140 | /*!40000 ALTER TABLE `Request` ENABLE KEYS */; 141 | UNLOCK TABLES; 142 | 143 | -- 144 | -- Table structure for table `Todo` 145 | -- 146 | 147 | DROP TABLE IF EXISTS `Todo`; 148 | /*!40101 SET @saved_cs_client = @@character_set_client */; 149 | /*!40101 SET character_set_client = utf8 */; 150 | CREATE TABLE `Todo` ( 151 | `id` bigint(20) DEFAULT NULL, 152 | `userId` bigint(20) DEFAULT NULL, 153 | `title` varchar(255) DEFAULT NULL, 154 | `note` varchar(255) DEFAULT NULL, 155 | `date` timestamp NULL DEFAULT current_timestamp(), 156 | `helper` longtext DEFAULT NULL 157 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 158 | /*!40101 SET character_set_client = @saved_cs_client */; 159 | 160 | -- 161 | -- Dumping data for table `Todo` 162 | -- 163 | 164 | LOCK TABLES `Todo` WRITE; 165 | /*!40000 ALTER TABLE `Todo` DISABLE KEYS */; 166 | INSERT INTO `Todo` VALUES (1627761460691,1627761019072,'new todo','some content here...','2021-07-31 19:57:40',NULL),(1627761702477,1627761019072,'yet another todo','good helper','2021-07-31 20:01:42',NULL),(1627761711192,1627761019072,'yet another todo','good helper','2021-07-31 20:01:51','[1627761504126]'),(1627794007156,1627761019072,'edit put multi','good 1','2021-08-01 05:00:07',NULL),(1627794007173,1627761019072,'edit put multi','good 1','2021-08-01 05:00:07',NULL),(1627794043682,1627761019072,'multi post a1','','2021-08-01 05:00:44',NULL),(1627794043692,1627761019072,'multi post a2','','2021-08-01 05:00:44',NULL); 167 | /*!40000 ALTER TABLE `Todo` ENABLE KEYS */; 168 | UNLOCK TABLES; 169 | 170 | -- 171 | -- Table structure for table `User` 172 | -- 173 | 174 | DROP TABLE IF EXISTS `User`; 175 | /*!40101 SET @saved_cs_client = @@character_set_client */; 176 | /*!40101 SET character_set_client = utf8 */; 177 | CREATE TABLE `User` ( 178 | `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '学校账号(教职工号)', 179 | `username` varchar(255) DEFAULT NULL, 180 | `realname` varchar(255) DEFAULT NULL, 181 | `bio` varchar(255) DEFAULT NULL, 182 | `friends` longtext DEFAULT NULL, 183 | PRIMARY KEY (`id`), 184 | UNIQUE KEY `User_id_uindex` (`id`) 185 | ) ENGINE=InnoDB AUTO_INCREMENT=1627761504127 DEFAULT CHARSET=utf8mb4; 186 | /*!40101 SET character_set_client = @saved_cs_client */; 187 | 188 | -- 189 | -- Dumping data for table `User` 190 | -- 191 | 192 | LOCK TABLES `User` WRITE; 193 | /*!40000 ALTER TABLE `User` DISABLE KEYS */; 194 | INSERT INTO `User` VALUES (1627761019072,'jerry','jerry','edit my bio while adding a friend','[1627761038716]'),(1627761038716,'neko','neko','registered via api',NULL),(1627761152411,'randomguy','randomguy','registered via api',NULL),(1627761504126,'doge','doge','registered via api',NULL); 195 | /*!40000 ALTER TABLE `User` ENABLE KEYS */; 196 | UNLOCK TABLES; 197 | /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; 198 | 199 | /*!40101 SET SQL_MODE=@OLD_SQL_MODE */; 200 | /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; 201 | /*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; 202 | /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; 203 | /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; 204 | /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; 205 | /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; 206 | 207 | -- Dump completed on 2022-08-27 16:54:39 208 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | apijson.demo 7 | apijson-demo 8 | 5.2.0 9 | 10 | APIJSONDemo 11 | Demo project for Testing APIJSON Server based on SpringBoot 12 | 13 | 14 | UTF-8 15 | UTF-8 16 | 1.8 17 | 18 | 19 | 20 | 21 | 22 | javax.activation 23 | activation 24 | 1.1.1 25 | 26 | 27 | 28 | 29 | com.github.APIJSON 30 | apijson-framework 31 | 5.2.0 32 | 33 | 34 | 35 | 36 | mysql 37 | mysql-connector-java 38 | 8.0.29 39 | 40 | 41 | org.postgresql 42 | postgresql 43 | 42.3.4 44 | 45 | 46 | 47 | 48 | 49 | org.springframework.boot 50 | spring-boot-starter-web 51 | 2.5.13 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | org.springframework.boot 60 | spring-boot-maven-plugin 61 | 62 | true 63 | apijson.demo.DemoApplication 64 | 65 | 66 | 67 | 68 | repackage 69 | 70 | 71 | 72 | 73 | 74 | org.apache.maven.plugins 75 | maven-compiler-plugin 76 | 77 | 1.8 78 | 1.8 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | jitpack.io 88 | https://jitpack.io 89 | 90 | true 91 | 92 | 93 | 94 | 95 | spring-snapshots 96 | https://repo.spring.io/snapshot 97 | 98 | true 99 | 100 | 101 | 102 | spring-milestones 103 | https://repo.spring.io/milestone 104 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /src/main/java/apijson/demo/DemoApplication.java: -------------------------------------------------------------------------------- 1 | /*Copyright ©2016 TommyLemon(https://github.com/TommyLemon/APIJSON) 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License.*/ 14 | 15 | package apijson.demo; 16 | 17 | import apijson.Log; 18 | import apijson.demo.config.DemoSQLConfig; 19 | import apijson.demo.config.DemoFunctionParser; 20 | import apijson.orm.FunctionParser; 21 | import org.springframework.boot.SpringApplication; 22 | import org.springframework.boot.autoconfigure.SpringBootApplication; 23 | import org.springframework.context.annotation.Bean; 24 | import org.springframework.context.annotation.Configuration; 25 | import org.springframework.web.servlet.config.annotation.CorsRegistry; 26 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 27 | 28 | import apijson.framework.APIJSONApplication; 29 | import apijson.framework.APIJSONCreator; 30 | import apijson.orm.SQLConfig; 31 | 32 | 33 | /**SpringBootApplication 34 | * 右键这个类 > Run As > Java Application 35 | * @author Lemon 36 | */ 37 | @Configuration 38 | @SpringBootApplication 39 | public class DemoApplication { 40 | 41 | static { 42 | 43 | APIJSONApplication.DEFAULT_APIJSON_CREATOR = new APIJSONCreator() { 44 | @Override 45 | public SQLConfig createSQLConfig() { 46 | return new DemoSQLConfig(); 47 | } 48 | 49 | @Override 50 | public FunctionParser createFunctionParser() { 51 | return new DemoFunctionParser(); 52 | } 53 | }; 54 | 55 | } 56 | 57 | public static void main(String[] args) throws Exception { 58 | SpringApplication.run(DemoApplication.class, args); 59 | 60 | // Log.DEBUG = false; 61 | // APIJSONParser.IS_PRINT_BIG_LOG = false; 62 | 63 | APIJSONApplication.init(true); // 4.4.0 以上需要这句来保证以上 static 代码块中给 DEFAULT_APIJSON_CREATOR 赋值会生效 64 | 65 | 66 | } 67 | 68 | 69 | // 支持 APIAuto 中 JavaScript 代码跨域请求 <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< 70 | 71 | @Bean 72 | public WebMvcConfigurer corsConfigurer() { 73 | return new WebMvcConfigurer() { 74 | @Override 75 | public void addCorsMappings(CorsRegistry registry) { 76 | registry.addMapping("/**") 77 | .allowedOriginPatterns("*") 78 | .allowedMethods("*") 79 | .allowCredentials(true) 80 | .maxAge(3600); 81 | } 82 | }; 83 | } 84 | 85 | // 支持 APIAuto 中 JavaScript 代码跨域请求 >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 86 | 87 | } 88 | -------------------------------------------------------------------------------- /src/main/java/apijson/demo/config/DemoFunctionParser.java: -------------------------------------------------------------------------------- 1 | package apijson.demo.config; 2 | 3 | 4 | import apijson.*; 5 | import apijson.demo.controller.DemoController; 6 | import apijson.demo.model.Todo; 7 | import apijson.demo.model.User; 8 | import apijson.framework.APIJSONConstant; 9 | import apijson.framework.APIJSONFunctionParser; 10 | import apijson.framework.APIJSONParser; 11 | import apijson.framework.APIJSONSQLExecutor; 12 | import apijson.orm.Parser; 13 | import apijson.orm.SQLExecutor; 14 | import apijson.orm.exception.ConditionErrorException; 15 | import com.alibaba.fastjson.JSONArray; 16 | import com.alibaba.fastjson.JSONObject; 17 | import com.alibaba.fastjson.TypeReference; 18 | import com.alibaba.fastjson.serializer.SerializerFeature; 19 | 20 | import javax.servlet.http.HttpSession; 21 | import java.sql.PreparedStatement; 22 | import java.sql.ResultSet; 23 | import java.sql.ResultSetMetaData; 24 | import java.sql.SQLException; 25 | import java.util.*; 26 | 27 | import static apijson.RequestMethod.GET; 28 | import static apijson.RequestMethod.HEAD; 29 | 30 | public class DemoFunctionParser extends APIJSONFunctionParser { 31 | 32 | public DemoFunctionParser() { 33 | this(null, null, 0, null, null); 34 | } 35 | // 展示在远程函数内部可以用 this 拿到的东西 36 | public DemoFunctionParser(RequestMethod method, String tag, int version, JSONObject request, HttpSession session) { 37 | super(method, tag, version, request, session); 38 | } 39 | 40 | /** 41 | * 一个最简单的远程函数示例,返回一个前面拼接了 Hello 的字符串 42 | * @param current 43 | * @param name 44 | * @return 45 | * @throws Exception 46 | */ 47 | public Object sayHello(@NotNull JSONObject current, @NotNull String name) throws Exception{ 48 | // 注意这里参数 name 是 key,不是 value 49 | Object obj = current.get(name); 50 | if (obj == null ){ 51 | throw new IllegalArgumentException(); 52 | } 53 | if (!(obj instanceof String)){ 54 | throw new IllegalArgumentException(); 55 | } 56 | 57 | if (this.getSession() == null){ 58 | return "Hello, inner test"; // 启动时的自动测试 59 | } 60 | // 之后可以用 this.getSession 拿到当前的 HttpSession 61 | 62 | return "Hello, " + obj.toString(); 63 | } 64 | 65 | /** 66 | * 获取当前登录用户的 todo 条数和 title 长度之和 67 | * 在远程函数内,手动构造 APIJSON 请求读写数据库 68 | * 可以下断点看看构造的请求对应的 JSON 到底长什么样 69 | * @param current 70 | * @return 71 | * @throws Exception 72 | */ 73 | public Object getNoteCountAPI(@NotNull JSONObject current) throws Exception{ 74 | 75 | if (this.getSession() == null){ 76 | return "Hello, inner test"; // 这是在启动时的自动测试 77 | } 78 | Long uid = (Long) this.getSession().getAttribute(APIJSONConstant.VISITOR_ID); 79 | if (uid == null) { 80 | throw new IllegalAccessException("user not logged in"); 81 | } 82 | 83 | String TODO_CLASS_NAME = Todo.class.getSimpleName(); 84 | 85 | // 构造一个 HEAD 请求来计数 86 | /* 87 | { 88 | "Todo": { 89 | "userId": 1627761019072, 90 | "@json": "helper" 91 | } 92 | } 93 | */ 94 | 95 | JSONRequest todoItem = new JSONRequest(TODO_CLASS_NAME, new Todo().setUserId(uid)); 96 | // 把 @json 放到 key Todo 里,不然反序列化的时候 helper 会被认为是字符串,从而造成反序列化异常 97 | todoItem.put(TODO_CLASS_NAME, todoItem.getJSONObject(TODO_CLASS_NAME).fluentPut(JSONRequest.KEY_JSON, "helper")); 98 | 99 | 100 | JSONResponse todoHeadResponse = new JSONResponse(new APIJSONParser(HEAD,false).parseResponse(todoItem)); 101 | int todoCount = todoHeadResponse.getJSONResponse(StringUtil.firstCase(TODO_CLASS_NAME, false)).getCount(); 102 | 103 | // 用 GET 得到所有 userId = uid 的 todo 104 | // 假设数据太多不能一次取完,使用分页(query=2,即每次取 2 个) 105 | // 参考请求 get todo by user id (unwrapped) + show page info 106 | /* 107 | { 108 | "Todo[]": { 109 | "Todo": { 110 | "userId": 1, 111 | "@json": "helper" 112 | }, 113 | "count": 0, 114 | "page": 0, 115 | "query": 2 116 | }, 117 | "total@": "/Todo[]/total", 118 | "info@": "/Todo[]/info" 119 | } 120 | */ 121 | 122 | List todos = new ArrayList<>(); 123 | int page = 0; 124 | while (true) { 125 | // 构造分页请求 126 | JSONObject todoRequest = new JSONObject(); 127 | todoItem.put(JSONRequest.KEY_QUERY, JSONRequest.QUERY_ALL); 128 | todoRequest.putAll(todoItem.toArray(2,page, TODO_CLASS_NAME)); // 模拟一个多页请求 129 | todoRequest.put("info@", "/Todo[]/info"); 130 | 131 | // 按照 put key 的顺序输出 JSON,不加这个 MapSortField 的话就会出问题 info@ 可能在前面,造成引用赋值异常,拿不到页数信息 132 | JSONResponse todoResponse = new JSONResponse(new APIJSONParser(GET,false).parseResponse(JSONObject.toJSONString(todoRequest, SerializerFeature.MapSortField))); 133 | 134 | // 获取数据(解析为一个 List) 135 | todos.addAll(todoResponse.getObject(StringUtil.firstCase(TODO_CLASS_NAME,false) + "List", new TypeReference>(){})); 136 | 137 | if (todoResponse.getJSONObject("info").getBooleanValue("more") == false){ // 没有更多了 138 | break; 139 | } 140 | page++; 141 | } 142 | 143 | int titleLengthSum = 0; 144 | for (Todo todo : todos) { 145 | titleLengthSum += todo.getTitle().length(); 146 | } 147 | 148 | JSONObject returnObj = new JSONObject() 149 | .fluentPut("query_uid", uid) 150 | .fluentPut("todo_count_using_HEAD", todoCount) 151 | .fluentPut("title_length_sum", titleLengthSum); 152 | 153 | return returnObj; 154 | } 155 | 156 | /** 157 | * 获取 todo 表中的一行 158 | * 在远程函数中直接用最原始的 SQL 操作 159 | * @param current 160 | * @param id 161 | * @return 162 | * @throws Exception 163 | */ 164 | public Object rawSQLAPI(@NotNull JSONObject current, @NotNull String id) throws Exception{ 165 | Object obj = current.get(id); 166 | if (obj == null ){ 167 | throw new IllegalArgumentException(); 168 | } 169 | if (!(obj instanceof String)){ 170 | throw new IllegalArgumentException(); 171 | } 172 | 173 | // 跳过框架初始化时的远程函数测试(理论上也可以加一个真实的 todoid,这里只是展示可以这样做) 174 | if ("_DOCUMENT_ONLY_".contentEquals(obj.toString())){ 175 | return null; 176 | } 177 | 178 | long todoid = Long.parseLong((String)obj); 179 | 180 | SQLExecutor executor = new APIJSONParser().getSQLExecutor(); 181 | DemoSQLConfig config = new DemoSQLConfig(); 182 | 183 | String query = "SELECT * from " + config.getSQLSchema() + ".Todo where id = ?"; 184 | 185 | Map resultMap = new HashMap<>(); 186 | 187 | // ResultSet -> ResultMap 188 | try (PreparedStatement stmt = executor.getConnection(config).prepareStatement(query)) { 189 | stmt.setLong(1, todoid); 190 | ResultSet rs = stmt.executeQuery(); 191 | ResultSetMetaData metaData = rs.getMetaData(); 192 | while(rs.next()) { 193 | for (int i=1; i<= metaData.getColumnCount(); i++){ 194 | resultMap.put(metaData.getColumnName(i), rs.getObject(i)); 195 | } 196 | } 197 | } catch (SQLException e){ 198 | e.printStackTrace(); 199 | } 200 | 201 | 202 | return resultMap; 203 | } 204 | 205 | /** 206 | * todo 的权限控制,在 POST/PUT 的时候被调用 207 | * 支持单独/批量的 PUT/POST 208 | * 默认策略: 209 | * 如果用户是 todo 的创建者,允许操作 210 | * 如果用户在 todo 的创建者的 friend 中,允许操作 211 | * 如果用户在 todo 的 helper 中,允许操作 212 | * 其他情况下禁止操作 213 | * @param current 214 | * @param todoId 215 | * @return 216 | * @throws Exception 217 | */ 218 | public Object isUserCanPutTodo(@NotNull JSONObject current, @NotNull String todoId) throws Exception{ 219 | Object idObj = current.get(todoId); 220 | Object idListObj = current.get(todoId + "{}"); // PUT tag=Todo[] 时用 221 | if (idObj == null && idListObj == null){ 222 | throw new IllegalArgumentException(); 223 | } 224 | if ((idObj instanceof Number && idListObj == null) || // 正常的单个请求 225 | (idListObj instanceof JSONArray) && idObj == null){ // PUT Todo[] 请求 226 | // continue 227 | } else { 228 | throw new IllegalArgumentException(); 229 | } 230 | 231 | // 启动时的自动测试 232 | // 因为后面需要用户 Session,这里暂时随便返回一些东西让测试通过(不抛异常就行) 233 | if (this.getSession() == null){ 234 | return null; 235 | } 236 | Long uid = (Long) this.getSession().getAttribute(APIJSONConstant.VISITOR_ID); 237 | if (uid == null) { 238 | throw new IllegalAccessException("user not logged in"); 239 | } 240 | 241 | String TODO_CLASS_NAME = Todo.class.getSimpleName(); 242 | 243 | List todoIdList = new ArrayList<>(); 244 | 245 | if (idObj instanceof Number && idListObj == null){ 246 | todoIdList.add(((Number) idObj).longValue()); 247 | } else { 248 | JSONArray idListArray = (JSONArray) idListObj; 249 | for (int i = 0; i < idListArray.size(); i++) { 250 | todoIdList.add(idListArray.getLong(i)); 251 | } 252 | } 253 | 254 | // 检查所有传入请求 255 | for (Long TodoId : todoIdList) { 256 | 257 | // 同时请求 User 和 Todo 258 | // 可以下断点看看构造的请求对应的 JSON 到底长什么样 259 | JSONObject todoRequest = new JSONRequest(); 260 | todoRequest.put(TODO_CLASS_NAME, new apijson.JSONObject(new Todo().setId(TodoId)).setJson("helper")); 261 | JSONObject userRequest = new JSONRequest().fluentPut("id@", "/" + TODO_CLASS_NAME + "/userId") 262 | .fluentPut("@json", "friends"); 263 | todoRequest.put(DemoController.USER_CLASS_NAME, userRequest); 264 | 265 | JSONObject response = new APIJSONParser(GET, false).parseResponse(todoRequest); 266 | 267 | // 内部错误 268 | if (!JSONResponse.isSuccess(response)) { 269 | return APIJSONParser.extendErrorResult(response, new RuntimeException("inner error")); 270 | } 271 | // 如果不存在的话 272 | if (!response.containsKey(TODO_CLASS_NAME)) { 273 | return APIJSONParser.newErrorResult(new ConditionErrorException("to do not exist")); 274 | } 275 | 276 | JSONResponse todoResponse = new JSONResponse(response); 277 | Todo todo = todoResponse.getObject(Todo.class); 278 | User user = todoResponse.getObject(User.class); 279 | System.out.println(todo); 280 | 281 | // return null 是正常 282 | // throw excpetion 是异常 283 | // return 值是进行修改? 284 | 285 | 286 | if (todo.getUserId().equals(uid)) { 287 | // current user is creator 288 | continue; 289 | } else if (user.getFriends().contains(uid)) { 290 | // current user in creator's friend list 291 | continue; 292 | } else if (todo.getHelper() != null && todo.getHelper().contains(uid)) { 293 | // current user in todo's helper list 294 | continue; 295 | } 296 | 297 | // 以上验证都没有通过 298 | throw new IllegalAccessException("user don't have permission to put todo!"); 299 | } 300 | 301 | // 所有的 todoid 都验证通过 302 | return null; 303 | } 304 | } 305 | -------------------------------------------------------------------------------- /src/main/java/apijson/demo/config/DemoSQLConfig.java: -------------------------------------------------------------------------------- 1 | package apijson.demo.config; 2 | 3 | import apijson.RequestMethod; 4 | import apijson.framework.APIJSONSQLConfig; 5 | import apijson.orm.SQLConfig; 6 | 7 | import java.util.Arrays; 8 | 9 | public class DemoSQLConfig extends APIJSONSQLConfig { 10 | 11 | public DemoSQLConfig() {super();} 12 | public DemoSQLConfig(RequestMethod method, String table) {super(method, table);} 13 | 14 | static { 15 | DEFAULT_DATABASE = "MYSQL"; // MYSQL, POSTGRESQL, SQLSERVER, ORACLE, DB2 16 | DEFAULT_SCHEMA = "apijson_todo_demo"; // 数据库的 Schema 名 17 | } 18 | 19 | @Override 20 | public String getDBVersion() { 21 | return "8.0"; 22 | } 23 | 24 | @Override 25 | public String getDBUri() { 26 | return "jdbc:mysql://192.168.99.100:33308?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=UTF-8"; 27 | } 28 | 29 | @Override 30 | public String getDBAccount() { 31 | return "root"; 32 | } 33 | 34 | @Override 35 | public String getDBPassword() { 36 | return "apijson"; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/apijson/demo/config/JSONWebConfig.java: -------------------------------------------------------------------------------- 1 | package apijson.demo.config; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.http.MediaType; 5 | import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer; 6 | import org.springframework.web.servlet.config.annotation.EnableWebMvc; 7 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; 8 | 9 | @Configuration 10 | @EnableWebMvc 11 | public class JSONWebConfig extends WebMvcConfigurerAdapter { 12 | 13 | @Override 14 | public void configureContentNegotiation(ContentNegotiationConfigurer configurer) { 15 | // 设定各个接口默认返回 application/json,以便 Postman 展示 16 | configurer.defaultContentType(MediaType.APPLICATION_JSON); 17 | } 18 | } -------------------------------------------------------------------------------- /src/main/java/apijson/demo/controller/DemoController.java: -------------------------------------------------------------------------------- 1 | /*Copyright ©2016 TommyLemon(https://github.com/TommyLemon/APIJSON) 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License.*/ 14 | 15 | package apijson.demo.controller; 16 | 17 | import javax.servlet.http.HttpSession; 18 | 19 | import apijson.JSONResponse; 20 | import apijson.Log; 21 | import apijson.demo.model.Credential; 22 | import apijson.demo.model.User; 23 | import apijson.framework.APIJSONParser; 24 | import apijson.framework.APIJSONVerifier; 25 | import apijson.framework.BaseModel; 26 | import apijson.orm.JSONRequest; 27 | import apijson.orm.exception.NotExistException; 28 | import com.alibaba.fastjson.JSONObject; 29 | import org.springframework.web.bind.annotation.PostMapping; 30 | import org.springframework.web.bind.annotation.RequestBody; 31 | import org.springframework.web.bind.annotation.RequestMapping; 32 | import org.springframework.web.bind.annotation.RestController; 33 | 34 | import apijson.RequestMethod; 35 | import apijson.framework.APIJSONController; 36 | import apijson.orm.Parser; 37 | 38 | import java.util.Arrays; 39 | 40 | import static apijson.RequestMethod.*; 41 | import static apijson.RequestMethod.DELETE; 42 | import static apijson.framework.APIJSONConstant.*; 43 | 44 | 45 | /**提供入口,转交给 APIJSON 的 Parser 来处理 46 | * @author Lemon 47 | */ 48 | @RestController 49 | @RequestMapping("") 50 | public class DemoController extends APIJSONController { 51 | 52 | @Override 53 | public Parser newParser(HttpSession session, RequestMethod method) { 54 | return super.newParser(session, method).setNeedVerify(true).setNeedVerifyLogin(false); //TODO 这里关闭校验,方便新手快速测试,实际线上项目建议开启 55 | } 56 | 57 | 58 | 59 | /**获取 60 | * @param request 只用String,避免encode后未decode 61 | * @param session 62 | * @return 63 | * @see {@link RequestMethod#GET} 64 | */ 65 | @PostMapping(value = "get") 66 | @Override 67 | public String get(@RequestBody String request, HttpSession session) { 68 | return super.get(request, session); 69 | } 70 | 71 | /**计数 72 | * @param request 只用String,避免encode后未decode 73 | * @param session 74 | * @return 75 | * @see {@link RequestMethod#HEAD} 76 | */ 77 | @PostMapping("head") 78 | @Override 79 | public String head(@RequestBody String request, HttpSession session) { 80 | return super.head(request, session); 81 | } 82 | 83 | /**限制性GET,request和response都非明文,浏览器看不到,用于对安全性要求高的GET请求 84 | * @param request 只用String,避免encode后未decode 85 | * @param session 86 | * @return 87 | * @see {@link RequestMethod#GETS} 88 | */ 89 | @PostMapping("gets") 90 | @Override 91 | public String gets(@RequestBody String request, HttpSession session) { 92 | return super.gets(request, session); 93 | } 94 | 95 | /**限制性HEAD,request和response都非明文,浏览器看不到,用于对安全性要求高的HEAD请求 96 | * @param request 只用String,避免encode后未decode 97 | * @param session 98 | * @return 99 | * @see {@link RequestMethod#HEADS} 100 | */ 101 | @PostMapping("heads") 102 | @Override 103 | public String heads(@RequestBody String request, HttpSession session) { 104 | return super.heads(request, session); 105 | } 106 | 107 | /**新增 108 | * @param request 只用String,避免encode后未decode 109 | * @param session 110 | * @return 111 | * @see {@link RequestMethod#POST} 112 | */ 113 | @PostMapping("post") 114 | @Override 115 | public String post(@RequestBody String request, HttpSession session) { 116 | return super.post(request, session); 117 | } 118 | 119 | /**修改 120 | * @param request 只用String,避免encode后未decode 121 | * @param session 122 | * @return 123 | * @see {@link RequestMethod#PUT} 124 | */ 125 | @PostMapping("put") 126 | @Override 127 | public String put(@RequestBody String request, HttpSession session) { 128 | return super.put(request, session); 129 | } 130 | 131 | /**删除 132 | * @param request 只用String,避免encode后未decode 133 | * @param session 134 | * @return 135 | * @see {@link RequestMethod#DELETE} 136 | */ 137 | @PostMapping("delete") 138 | @Override 139 | public String delete(@RequestBody String request, HttpSession session) { 140 | return super.delete(request, session); 141 | } 142 | 143 | 144 | 145 | //通用接口,非事务型操作 和 简单事务型操作 都可通过这些接口自动化实现>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 146 | 147 | 148 | // 登入、登出、注册 149 | public static final String USER_CLASS_NAME = User.class.getSimpleName(); 150 | public static final String CREDENTIAL_CLASS_NAME = Credential.class.getSimpleName(); 151 | 152 | public static final String LOGIN_ENDPOINT = "login"; 153 | public static final String LOGOUT_ENDPOINT = "logout"; 154 | public static final String REGISTER_ENDPOINT = "register"; 155 | 156 | public static final String KEY_USERNAME = "username"; 157 | public static final String KEY_PASSWORD = "password"; 158 | 159 | @PostMapping(LOGIN_ENDPOINT) 160 | public JSONObject login(@RequestBody String request, HttpSession session) { 161 | 162 | JSONObject requestObject = null; 163 | 164 | String username, password; 165 | 166 | // 框架信息,暂时可以忽略 167 | int version; // 全局默认版本号 168 | Boolean format; // 全局默认格式化配置 169 | JSONObject defaults; // 给每个请求JSON最外层加的字段 170 | 171 | // 提取信息 172 | try { 173 | requestObject = APIJSONParser.parseRequest(request); 174 | 175 | username = requestObject.getString(KEY_USERNAME); 176 | password = requestObject.getString(KEY_PASSWORD); 177 | 178 | version = requestObject.getIntValue(VERSION); 179 | format = requestObject.getBoolean(FORMAT); 180 | defaults = requestObject.getJSONObject(DEFAULTS); 181 | requestObject.remove(VERSION); 182 | requestObject.remove(FORMAT); 183 | requestObject.remove(DEFAULTS); 184 | } catch (Exception e) { 185 | return APIJSONParser.extendErrorResult(requestObject, e); 186 | } 187 | 188 | // 检查用户存在 189 | JSONObject userExistObj = new APIJSONParser(GETS, false).parseResponse( 190 | new JSONRequest(USER_CLASS_NAME, 191 | new apijson.JSONObject( 192 | new User().setUsername(username)) 193 | .setJson("friends") 194 | ) 195 | ); 196 | // 请求中间出错了 197 | if (!JSONResponse.isSuccess(userExistObj)) { 198 | return APIJSONParser.newResult( 199 | userExistObj.getIntValue(JSONResponse.KEY_CODE), 200 | userExistObj.getString(JSONResponse.KEY_MSG)); 201 | } 202 | // 没有获取到 User 203 | if (!userExistObj.containsKey(USER_CLASS_NAME)) { 204 | return APIJSONParser.newErrorResult(new NotExistException("user not exist")); 205 | } 206 | 207 | User user = new JSONResponse(userExistObj).getObject(User.class); 208 | 209 | // 验证密码并获取权限 210 | // 这里是明文密码,实际上可以用 BCrypt 之类的 Encryptor 加密 211 | JSONObject pwdMatchObj = new APIJSONParser(GETS, false).parseResponse( 212 | new JSONRequest( 213 | CREDENTIAL_CLASS_NAME, 214 | new apijson.JSONObject( 215 | new Credential().setId(user.getId()).setPwdHash(password)) 216 | ) 217 | ); 218 | if (!JSONResponse.isSuccess(pwdMatchObj)) { 219 | return APIJSONParser.newResult( 220 | pwdMatchObj.getIntValue(JSONResponse.KEY_CODE), 221 | pwdMatchObj.getString(JSONResponse.KEY_MSG)); 222 | } 223 | if (!pwdMatchObj.containsKey(CREDENTIAL_CLASS_NAME)) { 224 | return APIJSONParser.newErrorResult(new NotExistException("credential not match")); 225 | } 226 | 227 | 228 | // 注册用户 session 229 | super.login(session, user, version, format, defaults); 230 | session.setAttribute(USER_ID, user.getId()); 231 | session.setAttribute(USER_CLASS_NAME, user); 232 | 233 | JSONResponse returnResp = new JSONResponse(userExistObj); 234 | returnResp.put(DEFAULTS, defaults); 235 | 236 | return returnResp; 237 | } 238 | 239 | @PostMapping(LOGOUT_ENDPOINT) 240 | public JSONObject logout(@RequestBody String request, HttpSession session) { 241 | long userId; 242 | try { 243 | userId = APIJSONVerifier.getVisitorId(session);//必须在session.invalidate();前! 244 | Log.d(TAG, "logout userId = " + userId + "; session.getId() = " + (session == null ? null : session.getId())); 245 | // 销毁服务端 session 246 | super.logout(session); 247 | } catch (Exception e) { 248 | return APIJSONParser.newErrorResult(e); 249 | } 250 | 251 | // 返回登出成功 response 252 | JSONObject result = APIJSONParser.newSuccessResult(); 253 | JSONObject user = APIJSONParser.newSuccessResult(); 254 | user.put(ID, userId); 255 | user.put("logout", "success"); 256 | result.put(USER_CLASS_NAME, user); 257 | 258 | return result; 259 | } 260 | 261 | public static final String KEY_REALNAME = "realname"; 262 | public static final String REGISTER_REQ_TAG = "api_register"; 263 | 264 | @PostMapping(REGISTER_ENDPOINT) 265 | public JSONObject register(@RequestBody String request, HttpSession session) { 266 | JSONObject requestObject = null; 267 | 268 | String username, password, realname; 269 | 270 | // 提取信息 271 | try { 272 | requestObject = APIJSONParser.parseRequest(request); 273 | username = requestObject.getString(KEY_USERNAME); 274 | password = requestObject.getString(KEY_PASSWORD); 275 | realname = requestObject.getString(KEY_REALNAME); 276 | } catch (Exception e) { 277 | return APIJSONParser.extendErrorResult(requestObject, e); 278 | } 279 | 280 | // 检查用户名冲突 281 | JSONObject userExistObj = new APIJSONParser(GETS, false).parseResponse( 282 | new JSONRequest(new User().setUsername(username)) 283 | ); 284 | // 请求中间出错了 285 | if (!JSONResponse.isSuccess(userExistObj)) { 286 | return APIJSONParser.newResult( 287 | userExistObj.getIntValue(JSONResponse.KEY_CODE), 288 | userExistObj.getString(JSONResponse.KEY_MSG)); 289 | } 290 | // 已经存在相同用户名 291 | if (userExistObj.containsKey(USER_CLASS_NAME)) { 292 | return APIJSONParser.newErrorResult(new NotExistException("user already exist")); 293 | } 294 | 295 | 296 | // 构建新用户 297 | User user = new User() 298 | .setUsername(username) 299 | .setRealname(realname) 300 | .setBio("registered via api"); 301 | Credential credential = new Credential() 302 | .setPwdHash(password); 303 | 304 | // 序列化的顺序很重要!!!!!! 305 | JSONRequest registerReq = new JSONRequest() 306 | .puts(USER_CLASS_NAME, user) 307 | .puts(CREDENTIAL_CLASS_NAME, credential) 308 | .puts(JSONRequest.KEY_TAG, REGISTER_REQ_TAG); 309 | 310 | 311 | JSONObject tmpReq = new APIJSONParser(POST,false).setNeedVerifyContent(true).parseResponse(registerReq); 312 | JSONResponse registerRsp = new JSONResponse(tmpReq); 313 | 314 | // 验证用户注册成功 315 | user = registerRsp.getObject(User.class); 316 | long userId = user==null ? 0 : BaseModel.value(user.getId()); 317 | credential = registerRsp.getObject(Credential.class); 318 | long userId2 = credential==null ? 0 : BaseModel.value(credential.getId()); 319 | RuntimeException e = null; 320 | System.out.println("userId = " + userId); 321 | System.out.println("userId2 = " + userId2); 322 | if (userId <= 0 || userId != userId2) { 323 | e = new RuntimeException("服务器内部错误!写入User或Privacy失败!" + "userid=" + userId + " userid2=" + userId2); 324 | } 325 | 326 | if (e != null) { //出现错误,回退 327 | new APIJSONParser(DELETE, false).parseResponse( 328 | new JSONRequest(new User().setId(userId)) 329 | ); 330 | new APIJSONParser(DELETE, false).parseResponse( 331 | new JSONRequest(new Credential().setId(userId2)) 332 | ); 333 | throw e; 334 | } 335 | 336 | return registerRsp; 337 | } 338 | 339 | 340 | } -------------------------------------------------------------------------------- /src/main/java/apijson/demo/model/Credential.java: -------------------------------------------------------------------------------- 1 | package apijson.demo.model; 2 | 3 | import java.util.Collections; 4 | import java.util.List; 5 | 6 | public class Credential { 7 | 8 | Long id; 9 | String pwdHash; 10 | 11 | public Long getId() { 12 | return id; 13 | } 14 | 15 | public Credential setId(Long id) { 16 | this.id = id; 17 | return this; 18 | } 19 | 20 | public String getPwdHash() { 21 | return pwdHash; 22 | } 23 | 24 | public Credential setPwdHash(String pwdHash) { 25 | this.pwdHash = pwdHash; 26 | return this; 27 | } 28 | 29 | 30 | @Override 31 | public String toString() { 32 | return "Credential{" + 33 | "id=" + id + 34 | ", pwdHash='" + pwdHash + '\'' + 35 | '}'; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/apijson/demo/model/Todo.java: -------------------------------------------------------------------------------- 1 | package apijson.demo.model; 2 | 3 | import apijson.framework.BaseModel; 4 | 5 | import java.util.List; 6 | 7 | public class Todo extends BaseModel { 8 | 9 | String title; 10 | String note; 11 | List helper; 12 | 13 | public String getTitle() { 14 | return title; 15 | } 16 | 17 | public Todo setTitle(String title) { 18 | this.title = title; 19 | return this; 20 | } 21 | 22 | public String getNote() { 23 | return note; 24 | } 25 | 26 | public Todo setNote(String note) { 27 | this.note = note; 28 | return this; 29 | } 30 | 31 | public List getHelper() { 32 | return helper; 33 | } 34 | 35 | public Todo setHelper(List helper) { 36 | this.helper = helper; 37 | return this; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/apijson/demo/model/User.java: -------------------------------------------------------------------------------- 1 | package apijson.demo.model; 2 | 3 | import apijson.orm.Visitor; 4 | import com.alibaba.fastjson.annotation.JSONField; 5 | 6 | import java.util.List; 7 | 8 | public class User implements Visitor { 9 | Long id; 10 | String username; 11 | String realname; 12 | String bio; 13 | 14 | List friends; 15 | 16 | public Long getId() { 17 | return id; 18 | } 19 | 20 | @JSONField(serialize = false) 21 | @Override 22 | public List getContactIdList() { 23 | // return Collections.emptyList(); 24 | return getFriends(); 25 | } 26 | 27 | public User setId(Long id) { 28 | this.id = id; 29 | return this; 30 | } 31 | 32 | public String getUsername() { 33 | return username; 34 | } 35 | 36 | public User setUsername(String username) { 37 | this.username = username; 38 | return this; 39 | } 40 | 41 | public String getRealname() { 42 | return realname; 43 | } 44 | 45 | public User setRealname(String realname) { 46 | this.realname = realname; 47 | return this; 48 | } 49 | 50 | public String getBio() { 51 | return bio; 52 | } 53 | 54 | public User setBio(String bio) { 55 | this.bio = bio; 56 | return this; 57 | } 58 | 59 | 60 | public List getFriends() { 61 | return friends; 62 | } 63 | 64 | public User setFriends(List friends) { 65 | this.friends = friends; 66 | return this; 67 | } 68 | 69 | 70 | @Override 71 | public String toString() { 72 | return "User{" + 73 | "id=" + id + 74 | ", username='" + username + '\'' + 75 | ", realname='" + realname + '\'' + 76 | ", note='" + bio + '\'' + 77 | '}'; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/APIJSON/apijson_todo_demo/2a578a394cc148d0e81892a9fc9725401747b13c/src/main/resources/application.properties --------------------------------------------------------------------------------