基于 Android 的本地商城订单 App:SQLite + 多角色架构的技术实践(有源码和文档)
摘要本文以一款面向本地化的比萨点餐Android 商城 ApporderApp为案例围绕纯本地 SQLite 多角色路由 ViewPager/Fragment这一典型教学型商城架构展开从需求拆解、技术选型、数据库设计、关键模块实现到完整业务流程全景还原一个轻量级电商 App 的工程化实现路径适合作为 Android 初学者的商城类项目参考。一、项目概述1.1 项目背景移动端商城类应用是 Android 学习的经典实战场景。然而对于初学者而言直接对接后端服务、处理网络异常、解析 JSON 数据会显著抬高学习曲线。本项目有意做减法——剥离网络层将商品、用户、订单、购物车全部交由SQLite 本地数据库持久化让学习者能聚焦于 Android 自身的 UI 架构、列表/适配器、Activity 间通信、角色路由等核心知识点同时完整保留商城类应用浏览 → 加购 → 结算 → 订单的业务闭环。1.2 功能矩阵功能模块普通用户管理员用户体系注册 / 登录 / 验证码 / 记住密码 / 自动登录同左预置账号 admin/123商品浏览首页轮播 搜索 商品列表 详情同左 发布商品入口购物车加入购物车 / 多选 / 合计 / 结算—订单立即购买 / 我的订单按时间倒序查看全部订单个人中心个人信息 / 修改密码 / 关于 / 退出登录同左1.3 应用截图图 1 — 登录页含图形验证码、自动登录、记住密码登录页采用极简布局顶部为 Logo 区中部为用户名/密码/验证码输入区底部为自动登录/记住密码/注册以及主操作按钮。验证码由后台随机生成 4 位字符a-z 0-9每次进入onStart()时刷新。图 2 — 注册页带头像选择 性别单选注册页支持通过 PictureSelector 选择头像相机/相册用户名、性别、手机号、地址、密码、确认密码均做必填校验且两次密码必须一致。图 3 — 普通用户首页轮播图 搜索 商品列表首页由三部分组成顶部搜索栏、中部 4 秒自动轮播的 ViewPager 横幅、底部 ListView 商品列表。商品图片、价格、分类一目了然点击进入详情页。管理员登录时右下角显示浮动按钮发布新商品。图 4 — 商品详情页加入购物车 / 立即购买详情页提供两种购买路径加入购物车将商品 ID 与当前用户名写入cart表立即购买则直接将一条Record记录写入订单表跳过购物车环节。图 5 — 购物车多选 合计 结算购物车采用自定义CartAdapter支持条目勾选底部动态显示已选项总价点击结算将所选项批量生成订单记录。图 6 — 订单记录普通用户视角我的订单按时间倒序展示下单时记录的收货地址会一同写入订单便于后续追溯。图 7 — 管理员发布商品页管理员通过该页填写商品名称、详情、分类、价格点击发布后由DBStuff.add()写入Stuff表。图 8 — 管理员查看所有订单管理员首页底部导航将购物车替换为所有订单点击后展示平台全部用户的下单情况用户名 商品 价格 收货地址。图 9 — 我的个人中心我的页面以卡片 列表项的组合形式聚合个人信息、我的订单、账号安全、退出登录、关于五大入口。图 10 — 修改密码页独立密码修改页提交时调用DBUser.updatePsw()更新当前用户密码。图 11 — 个人信息编辑页用户可在该页编辑头像、性别、手机号、地址提交后写回users表。二、技术栈与依赖2.1 基础配置配置项值Application IDcom.example.ordercompileSdkVersion30targetSdkVersion30minSdkVersion21覆盖 Android 5.0Java 文件数33 个主色薄荷绿#1FCBA0风格2.2 核心依赖// build.gradle 核心依赖implementationandroidx.appcompat:appcompat:1.2.0implementationandroidx.recyclerview:recyclerview:1.1.0implementationandroidx.viewpager:viewpager:1.0.0implementationcom.google.android.material:material:1.2.1implementationcom.github.bumptech.glide:glide:4.11.0implementationjp.wasabeef:glide-transformations:3.3.0implementationcom.github.LuckSiege.PictureSelector:picture_library:v2.6.0// libs/ 本地依赖implementationfiles(libs/volley.jar)// 预留网络能力实际未使用重要说明build.gradle中引入了volley.jar但业务代码中并未真正发起网络请求——所有数据商品、用户、订单、购物车均通过本地 SQLite 持久化。这是有意为之的设计取舍聚焦 Android 端核心能力学习预留网络层以备扩展。三、架构设计3.1 整体架构项目采用Activity Fragment DAO三层组织没有引入任何 DI 框架或 MVP/MVVM 模式保持代码直接、易读UI 层 Activity11 个/ Fragment6 个/ Adapter5 个 │ 业务层 DBHelper单例 SQLiteOpenHelper ├── DBUser 用户表 CRUD 登录校验 ├── DBStuff 商品表 CRUD ├── DBRecord 订单表 CRUD └── DBCart 购物车表 CRUD │ 数据层 entity.User / Stuff / Record utils.SPUtils登录态/记住密码3.2 包结构com.example.order/ ├── activity/ 11 个 Activity │ ├── OpenActivity 欢迎/启动页LAUNCHER │ ├── LoginActivity 登录含图形验证码、自动登录、记住密码 │ ├── RegisterActivity 注册含头像选择 │ ├── MainActivity 普通用户主页BottomNavigation 容器 │ ├── AdminMainActivity 管理员主页BottomNavigation 容器 │ ├── DetailActivity 商品详情 │ ├── PublishProductActivity 管理员发布商品 │ ├── MyInfoActivity 个人信息编辑 │ ├── MyRecordActivity 我的订单 │ ├── PasswordActivity 修改密码 │ └── AboutActivity 关于 ├── fragment/ 6 个 Fragment │ ├── BlankFragment_Main 普通用户首页轮播搜索列表 │ ├── BlankFragment_Cart 普通用户购物车 │ ├── BlankFragment_me 普通用户我的 │ ├── FragmentMainAdmin 管理员首页含发布入口 │ ├── FragmentCartAdmin 管理员全部订单 │ └── FragmentMeAdmin 管理员我的 ├── adapter/ 5 个 Adapter │ ├── StuffAdapter 商品列表 │ ├── CartAdapter 购物车含勾选与合计 │ ├── RecordAdapter 我的订单 │ ├── AdminAllRecordAdapter 全部订单 │ └── AdPagerAdapter 首页轮播 ├── database/ 5 个 DAO │ ├── DBHelper SQLiteOpenHelper 单例 │ ├── DBUser │ ├── DBStuff │ ├── DBRecord │ └── DBCart ├── entity/ 3 个实体 │ ├── User │ ├── Stuff │ └── Record └── utils/ 3 个工具类 ├── SPUtils SharedPreferences 封装 ├── UserTypeEnum 角色枚举 └── GlideEngine PictureSelector 引擎3.3 多角色路由应用没有服务端下发角色而是在登录后通过本地UserTypeEnum字段决定进入哪个主页public void init(){ //初始化管理员数据 DBUser user new DBUser(context); if (!user.checkUser(admin)) { User user1 new User(admin, 123, 广东省广州市, 男, 17620885471, 管理员, UserTypeEnum.Admin.getDesc()); user.add(user1); } DBUser.user user.get(SPUtils.getName(context)); }DBHelper.init()在应用首次创建时写入管理员预置账号admin / 123。LoginActivity登录成功后调用DBUser.isAdmin()判断角色进入MainActivity或AdminMainActivityif (db.check(account, password)) {//用户存在 if (DBUser.isAdmin()){ Intent intent new Intent(LoginActivity.this, AdminMainActivity.class); startActivity(intent); }else { Intent intent new Intent(LoginActivity.this, MainActivity.class); startActivity(intent); } finish(); } else {//账号不存在 Toast.makeText(LoginActivity.this, 登录失败用户名或密码错误, Toast.LENGTH_SHORT).show(); }设计取舍项目AndroidManifest.xml中声明的packagecom.example.smallshop而业务代码中import com.example.smallshop.R——这是历史遗留的小问题重命名包名后未更新资源引用。教学项目中不影响运行但生产项目务必修正。四、数据库设计数据库名orderApp版本1无迁移onUpgrade为空。所有表结构在DBHelper.onCreate()一次性建好。4.1 建表 SQLOverride public void onCreate(SQLiteDatabase sqLiteDatabase) { String sqlcreate table users(name text primary key,psw text not null,address text not null,sex text not null,phone text not null,photo text not null ,userType text not null); sqLiteDatabase.execSQL(sql); sqlcreate table cart(id text not null,username text not null,PRIMARY KEY(id,username)); sqLiteDatabase.execSQL(sql); sqlcreate table Record(username text not null,id text not null,name text not null,price text not null,address text not null); sqLiteDatabase.execSQL(sql); sql CREATE TABLE Stuff (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, title TEXT NOT NULL, kind TEXT NOT NULL, price TEXT NOT NULL, imagePath); sqLiteDatabase.execSQL(sql); }4.2 表结构说明users用户表字段类型约束说明nametextPRIMARY KEY用户名pswtextNOT NULL密码明文教学型 DemoaddresstextNOT NULL地址sextextNOT NULL性别phonetextNOT NULL手机号phototextNOT NULL头像路径userTypetextNOT NULL用户类型普通用户/管理员Stuff商品表字段类型约束说明idintegerPRIMARY KEY AUTOINCREMENT商品 IDnametextNOT NULL名称titletextNOT NULL详情描述kindtextNOT NULL分类如披萨pricetextNOT NULL价格imagePathtext可空本地图片路径默认走 drawablecart购物车表字段类型约束说明idtext联合主键商品 IDusernametext联合主键所属用户联合主键(id, username)保证同一商品在同一用户购物车中不重复。Record订单表字段类型说明usernametext下单用户idtext商品 IDnametext商品名称下单时快照pricetext下单时价格addresstext收货地址下单时快照订单表采用下单快照模式商品名称、价格、地址均在订单生成时写入避免后续商品改价或用户改地址后历史订单失真。五、关键模块实现5.1 启动流程与登录态OpenActivity为 LAUNCHER负责读取SPUtils中保存的登录态与记住密码偏好决定下一步跳转OpenActivity ├── 已登录且是 Admin → AdminMainActivity ├── 已登录普通用户 → MainActivity └── 未登录 → LoginActivityLoginActivity在onStart()阶段刷新图形验证码4 位a-z0-9随机串并根据REMEMBER_PASSWORD是否勾选回填历史账号密码String getNumber() { String str abcdefghijklmnopqrstuvwxyz0123456789; Random random new Random(); StringBuffer sb new StringBuffer(); for (int i 0; i 4; i) { int number random.nextInt(36); sb.append(str.charAt(number)); } return sb.toString(); }登录校验逻辑用户名/密码/验证码三项均非空 → 验证码匹配 →db.check(account, password)校验users表 → 根据DBUser.isAdmin()路由至不同主页。SPUtils同时记录AUTO_LOGIN与REMEMBER_PASSWORD下次启动时OpenActivity据此决定是否自动跳转主页与回填密码。5.2 首页ViewPager 轮播 搜索 商品列表BlankFragment_Main是普通用户首页同时承载三段式内容13 秒自动轮播public class BlankFragment_Main extends Fragment { private ViewPager mViewPager; private Handler mHandler new Handler(); private Runnable mRunnable new Runnable() { Override public void run() { int currentItem mViewPager.getCurrentItem(); int itemCount mViewPager.getAdapter().getCount(); int nextItem (currentItem 1) % itemCount; mViewPager.setCurrentItem(nextItem); mHandler.postDelayed(this,3000); // 3秒后再次调用 } };通过Handler定时切页实现无限轮播并通过OnTouchListener在ACTION_DOWN暂停、ACTION_UP/CANCEL重启 1 秒后再次轮播避免用户触摸期间画面跳动。2搜索过滤void setSearch(String str){ DBStuff dbnew DBStuff(getContext()); ArrayListStuff arrayTempdb.getAll(); ArrayListStuff arraynew ArrayList(); for (Stuff t: arrayTemp) { if( t.getName().contains(str) ||t.getTitle().contains(str)){ array.add(t); } } if(array.size()0){ Toast.makeText(getContext(), 未搜索到关键字商品, Toast.LENGTH_SHORT).show(); arrayarrayTemp; } StuffAdapter adapternew StuffAdapter(getContext(),R.layout.layout_stuff,array); listview.setAdapter(adapter);搜索逻辑朴素但有效取出Stuff表全部数据遍历匹配名称或描述命中即加入结果集。零结果时给出 Toast 提示并回退到原始列表。3商品列表 ListView 点击进入详情StuffAdapter采用经典 BaseAdapter 模式左侧为商品缩略图Glide 加载右侧为名称 详情摘要 价格 分类标签。点击通过setOnItemClickListener将商品 ID 传给DetailActivitylistview.setOnItemClickListener(new AdapterView.OnItemClickListener() { Override public void onItemClick(AdapterView? adapterView, View view, int i, long l) { String s( finalArray.get(i)).getName(); //Toast.makeText(getContext(), names, Toast.LENGTH_SHORT).show(); Intent intent new Intent(); intent.putExtra(id,( finalArray.get(i)).getId()); intent.setClass(getContext() , DetailActivity.class); startActivity(intent); } });5.3 商品表 DAO 设计DBStuff是商品相关操作的统一入口提供add / getAll / getById / delete四类方法public boolean add(Stuff s) { ContentValues values new ContentValues(); values.put(name, s.getName()); values.put(title, s.getTitle()); values.put(kind, s.getKind()); values.put(price, s.getPrice()); values.put(imagePath, s.getImagePath()); long i dp.insert(Stuff, null, values); if (i 0) { Log.d(, 插入成功); return true; } Log.d(, 插入失败); return false; } public ArrayListStuff getAll() { ArrayListStuff array new ArrayList(); Cursor cursor dp.query(Stuff, null, null, null, null, null, null,null); while (cursor.moveToNext()) { SuppressLint(Range) int id cursor.getInt(cursor.getColumnIndex(id)); SuppressLint(Range) String name cursor.getString(cursor.getColumnIndex(name)); SuppressLint(Range) String title cursor.getString(cursor.getColumnIndex(title)); SuppressLint(Range) String kind cursor.getString(cursor.getColumnIndex(kind)); SuppressLint(Range) String price cursor.getString(cursor.getColumnIndex(price)); SuppressLint(Range) String imagePath cursor.getString(cursor.getColumnIndex(imagePath)); Stuff u new Stuff(String.valueOf(id), name, title, kind, price,imagePath); array.add(u); } return array; }getById通过遍历Cursor找到目标 ID 后构造Stuff返回可在优化阶段改为query(..., id?, new String[]{_id }, ...)。delete同样使用占位符防止 SQL 注入。5.4 购物车与订单CartAdapter内置CheckBox勾选状态维护每次勾选变化时回调OnCheckChangeListener由BlankFragment_Cart重新计算合计并刷新底部金额 TextView。结算时遍历已勾选项为每条记录调用DBRecord.add()写入Record表同时从cart表中删除对应行。Record实体记录下单快照——即使后续商品下架、用户修改地址历史订单信息依然完整可追溯。5.5 管理员发布商品PublishProductActivity提供名称、详情、分类、价格四类文本输入发布按钮触发DBStuff.add()写入数据。返回上一页后FragmentMainAdmin.onStart()重新调用set()刷新列表管理员可见新商品。首页右下角浮动按钮是发布入口仅当用户角色为 Admin 时才显示通过DBUser.isAdmin()判断。5.6 角色与全局状态UserTypeEnum定义三类用户角色普通用户/管理员/院长但院长角色实际未使用DBUser.user静态字段缓存当前登录用户信息应用内全局可见避免重复查询数据库。六、业务流程串讲6.1 下单流程DetailActivity加入购物车 / 立即购买 ├─ 加购DBCart.add(id, username) → BlankFragment_Cart └─ 即购DBRecord.add(快照) → MyRecordActivity BlankFragment_Cart多选 → 结算 └─ 遍历勾选项DBRecord.add(快照) × N DBCart.delete(id, username) × N6.2 管理员发布流程AdminMainActivity → FragmentMainAdmin首页 → 浮动 按钮 → PublishProductActivity → DBStuff.add(stuff) → 返回 → onStart() 重新 set()6.3 启动流程OpenActivityLAUNCHER ├─ SPUtils 记录已登录 │ ├─ isAdmintrue → AdminMainActivity │ └─ isAdminfalse → MainActivity └─ 未登录 → LoginActivity ├─ 自动登录勾选 → 直跳主页 ├─ 记住密码勾选 → 回填密码 └─ 验证通过 → DBUser.isAdmin() 路由七、权限与清单AndroidManifest.xml声明以下权限教学项目按需申请即可权限用途实际使用情况INTERNET网络预留当前未使用CAMERA头像/商品图拍照已接入 PictureSelectorREAD_EXTERNAL_STORAGE相册选图已接入 PictureSelectorCALL_PHONE联系卖家声明但未使用12 个 ActivityOpenActivity为 LAUNCHER。无自定义 Application 类无 Service 与 Receiver。八、可优化方向虽然本项目以教学为第一目标但仍可在以下方向继续打磨密码安全当前psw字段以明文存储生产环境务必使用BCrypt或Argon2加盐哈希。图片加载Stuff.imagePath当前为可空字段建议加入若为空则走默认 drawable的兜底逻辑部分代码已实现。网络层接入volley.jar已就绪可平滑替换为 OkHttp/Retrofit将 DAO 抽象为 RepositoryUI 层只关心数据流。订单状态Record表可扩展status字段待发货/已发货/已完成/已退款支持订单生命周期管理。购物车多商家cart表可加入shopId字段支持按商家分组结算。包名修正com.example.smallshop的遗留问题可一次性重命名包为com.example.order并同步更新R类引用。MVVM 化将Fragment内大量setOnClickListener与数据加载逻辑迁移至ViewModel LiveData降低 UI 与数据耦合。