基于 Android 的本地商城订单 App:SQLite + 多角色架构的技术实践(有源码和文档)

摘要:本文以一款面向本地化的"比萨点餐"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 / 123LoginActivity 登录成功后调用 DBUser.isAdmin() 判断角色,进入 MainActivityAdminMainActivity

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

LoginActivityonStart() 阶段刷新图形验证码(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_LOGINREMEMBER_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 定时切页实现"无限轮播",并通过 OnTouchListenerACTION_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。


八、可优化方向

虽然本项目以"教学"为第一目标,但仍可在以下方向继续打磨:

  1. 密码安全 :当前 psw 字段以明文存储,生产环境务必使用 BCryptArgon2 加盐哈希。
  2. 图片加载Stuff.imagePath 当前为可空字段,建议加入"若为空则走默认 drawable"的兜底逻辑(部分代码已实现)。
  3. 网络层接入volley.jar 已就绪,可平滑替换为 OkHttp/Retrofit,将 DAO 抽象为 Repository,UI 层只关心数据流。
  4. 订单状态Record 表可扩展 status 字段(待发货/已发货/已完成/已退款),支持订单生命周期管理。
  5. 购物车多商家cart 表可加入 shopId 字段,支持按商家分组结算。
  6. 包名修正com.example.smallshop 的遗留问题可一次性重命名包为 com.example.order 并同步更新 R 类引用。
  7. MVVM 化 :将 Fragment 内大量 setOnClickListener 与数据加载逻辑迁移至 ViewModel + LiveData,降低 UI 与数据耦合。

相关推荐
自己的九又四分之三站台1 小时前
RasterLite2 栅格数据处理函数全量参考:从 Coverage 创建到像元、金字塔与地图输出
sqlite·地理信息
智恒百亿1 小时前
5090八卡算力服务器的技术架构与AI应用场景分析
服务器·人工智能·架构
Scott9999HH1 小时前
2026 年大模型 RAG 架构与多 Agent 协同下的 AI 搜索流量重构:企业级 GEO 技术底层与深度工程解析
人工智能·重构·架构
Quincy_Freak1 小时前
银河麒麟终端数据处理标准化实践:SQLiteGo 效能评估与落地路径
sqlite·数据库管理·大数据分析·银河麒麟·aarch64·sqlitego
有颜有货10 小时前
一文搞懂企业五大架构
架构·数据架构·业务架构·技术架构·代码架构
又见情义11 小时前
RK3568 Android 13 板载驱动适配-USB
android·arm开发·驱动开发
delta_hell12 小时前
【阅读源码-Android】动画之Choreographer-2
android·源码·choreographer
mmsx12 小时前
基于 Android 的博物馆文化展示与预约 App —— 设计与实现(有源码和文档)
android·app·源码·文档
cellurw13 小时前
从虚拟化音频架构到板端故障定位:车载 Audio 阶段性工程实践整理
架构·音视频