摘要:本文以一款面向本地化的"比萨点餐"Android 商城 App(orderApp)为案例,围绕"纯本地 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 ID | com.example.order |
| compileSdkVersion | 30 |
| targetSdkVersion | 30 |
| minSdkVersion | 21(覆盖 Android 5.0+) |
| Java 文件数 | 33 个 |
| 主色 | 薄荷绿 #1FCBA0 风格 |
2.2 核心依赖
groovy
// build.gradle 核心依赖
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.recyclerview:recyclerview:1.1.0'
implementation 'androidx.viewpager:viewpager:1.0.0'
implementation 'com.google.android.material:material:1.2.1'
implementation 'com.github.bumptech.glide:glide:4.11.0'
implementation 'jp.wasabeef:glide-transformations:3.3.0'
implementation 'com.github.LuckSiege.PictureSelector:picture_library:v2.6.0'
// libs/ 本地依赖
implementation files('libs/volley.jar') // 预留网络能力(实际未使用)
重要说明 :
build.gradle中引入了volley.jar,但业务代码中并未真正发起网络请求------所有数据(商品、用户、订单、购物车)均通过本地 SQLite 持久化。这是有意为之的设计取舍:聚焦 Android 端核心能力学习,预留网络层以备扩展。
三、架构设计
3.1 整体架构
项目采用 Activity + Fragment + DAO 三层组织,没有引入任何 DI 框架或 MVP/MVVM 模式,保持代码直接、易读:
UI 层: Activity(11 个)/ Fragment(6 个)/ Adapter(5 个)
│
业务层: DBHelper(单例 SQLiteOpenHelper)
├── DBUser 用户表 CRUD + 登录校验
├── DBStuff 商品表 CRUD
├── DBRecord 订单表 CRUD
└── DBCart 购物车表 CRUD
│
数据层: entity.User / Stuff / Record + utils.SPUtils(登录态/记住密码)
3.2 包结构
1:92:graduate/orderApp/README-orderApp-商城订单应用.md
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 字段决定进入哪个主页:
30:38:graduate/orderApp/src/main/java/com/example/order/database/DBHelper.java
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 或 AdminMainActivity:
119:135:graduate/orderApp/src/main/java/com/example/order/activity/LoginActivity.java
if (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中声明的package="com.example.smallshop",而业务代码中import com.example.smallshop.R------这是历史遗留的小问题,重命名包名后未更新资源引用。教学项目中不影响运行,但生产项目务必修正。
四、数据库设计
数据库名:orderApp,版本 1,无迁移(onUpgrade 为空)。所有表结构在 DBHelper.onCreate() 一次性建好。
4.1 建表 SQL
44:60:graduate/orderApp/src/main/java/com/example/order/database/DBHelper.java
@Override
public void onCreate(SQLiteDatabase sqLiteDatabase) {
String sql="create 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);
sql="create table cart(id text not null,username text not null,PRIMARY KEY(id,username))";
sqLiteDatabase.execSQL(sql);
sql="create 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(用户表)
| 字段 | 类型 | 约束 | 说明 |
|---|---|---|---|
| name | text | PRIMARY KEY | 用户名 |
| psw | text | NOT NULL | 密码(明文,教学型 Demo) |
| address | text | NOT NULL | 地址 |
| sex | text | NOT NULL | 性别 |
| phone | text | NOT NULL | 手机号 |
| photo | text | NOT NULL | 头像路径 |
| userType | text | NOT NULL | 用户类型(普通用户/管理员) |
Stuff(商品表)
| 字段 | 类型 | 约束 | 说明 |
|---|---|---|---|
| id | integer | PRIMARY KEY AUTOINCREMENT | 商品 ID |
| name | text | NOT NULL | 名称 |
| title | text | NOT NULL | 详情描述 |
| kind | text | NOT NULL | 分类(如"披萨") |
| price | text | NOT NULL | 价格 |
| imagePath | text | 可空 | 本地图片路径(默认走 drawable) |
cart(购物车表)
| 字段 | 类型 | 约束 | 说明 |
|---|---|---|---|
| id | text | 联合主键 | 商品 ID |
| username | text | 联合主键 | 所属用户 |
联合主键
(id, username)保证同一商品在同一用户购物车中不重复。
Record(订单表)
| 字段 | 类型 | 说明 |
|---|---|---|
| username | text | 下单用户 |
| id | text | 商品 ID |
| name | text | 商品名称(下单时快照) |
| price | text | 下单时价格 |
| address | text | 收货地址(下单时快照) |
订单表采用"下单快照"模式:商品名称、价格、地址均在订单生成时写入,避免后续商品改价或用户改地址后历史订单失真。
五、关键模块实现
5.1 启动流程与登录态
OpenActivity 为 LAUNCHER,负责读取 SPUtils 中保存的登录态与"记住密码"偏好,决定下一步跳转:
OpenActivity
├── 已登录且是 Admin → AdminMainActivity
├── 已登录普通用户 → MainActivity
└── 未登录 → LoginActivity
LoginActivity 在 onStart() 阶段刷新图形验证码(4 位 a-z0-9 随机串),并根据 REMEMBER_PASSWORD 是否勾选回填历史账号密码:
34:43:graduate/orderApp/src/main/java/com/example/order/activity/LoginActivity.java
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 是普通用户首页,同时承载三段式内容:
1)3 秒自动轮播
32:44:graduate/orderApp/src/main/java/com/example/order/fragment/BlankFragment_Main.java
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)搜索过滤
112:128:graduate/orderApp/src/main/java/com/example/order/fragment/BlankFragment_Main.java
void setSearch(String str){
DBStuff db=new DBStuff(getContext());
ArrayList<Stuff> arrayTemp=db.getAll();
ArrayList<Stuff> array=new 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();
array=arrayTemp;
}
StuffAdapter adapter=new StuffAdapter(getContext(),R.layout.layout_stuff,array);
listview.setAdapter(adapter);
搜索逻辑朴素但有效:取出 Stuff 表全部数据,遍历匹配名称或描述,命中即加入结果集。零结果时给出 Toast 提示并回退到原始列表。
3)商品列表 ListView + 点击进入详情
StuffAdapter 采用经典 BaseAdapter 模式,左侧为商品缩略图(Glide 加载),右侧为名称 + 详情摘要 + 价格 + 分类标签。点击通过 setOnItemClickListener 将商品 ID 传给 DetailActivity:
129:160:graduate/orderApp/src/main/java/com/example/order/fragment/BlankFragment_Main.java
listview.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(), "name:"+s, 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 四类方法:
21:53:graduate/orderApp/src/main/java/com/example/order/database/DBStuff.java
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 ArrayList<Stuff> getAll() {
ArrayList<Stuff> 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) × N
6.2 管理员发布流程
AdminMainActivity
→ FragmentMainAdmin(首页)
→ 浮动 "+" 按钮
→ PublishProductActivity
→ DBStuff.add(stuff)
→ 返回 → onStart() 重新 set()
6.3 启动流程
OpenActivity(LAUNCHER)
├─ SPUtils 记录"已登录"
│ ├─ isAdmin=true → AdminMainActivity
│ └─ isAdmin=false → MainActivity
└─ 未登录
→ LoginActivity
├─ 自动登录勾选 → 直跳主页
├─ 记住密码勾选 → 回填密码
└─ 验证通过 → DBUser.isAdmin() 路由
七、权限与清单
AndroidManifest.xml 声明以下权限(教学项目按需申请即可):
| 权限 | 用途 | 实际使用情况 |
|---|---|---|
INTERNET |
网络预留 | 当前未使用 |
CAMERA |
头像/商品图拍照 | 已接入 PictureSelector |
READ_EXTERNAL_STORAGE |
相册选图 | 已接入 PictureSelector |
CALL_PHONE |
联系卖家 | 声明但未使用 |
12 个 Activity,OpenActivity 为 LAUNCHER。无自定义 Application 类,无 Service 与 Receiver。
八、可优化方向
虽然本项目以"教学"为第一目标,但仍可在以下方向继续打磨:
- 密码安全 :当前
psw字段以明文存储,生产环境务必使用BCrypt或Argon2加盐哈希。 - 图片加载 :
Stuff.imagePath当前为可空字段,建议加入"若为空则走默认 drawable"的兜底逻辑(部分代码已实现)。 - 网络层接入 :
volley.jar已就绪,可平滑替换为 OkHttp/Retrofit,将 DAO 抽象为 Repository,UI 层只关心数据流。 - 订单状态 :
Record表可扩展status字段(待发货/已发货/已完成/已退款),支持订单生命周期管理。 - 购物车多商家 :
cart表可加入shopId字段,支持按商家分组结算。 - 包名修正 :
com.example.smallshop的遗留问题可一次性重命名包为com.example.order并同步更新R类引用。 - MVVM 化 :将
Fragment内大量setOnClickListener与数据加载逻辑迁移至ViewModel + LiveData,降低 UI 与数据耦合。